mirror of
https://github.com/safishamsi/graphify.git
synced 2026-07-12 18:37:12 +00:00
54825b6a1c
#1635: the windows skill variant declared `name: graphify-windows`, but `graphify install --platform windows` writes it to ~/.claude/skills/graphify/ SKILL.md and Claude Code requires the folder name to equal the frontmatter `name` — the suffix broke discovery. platforms.toml now sets name = "graphify" (regenerated + re-blessed). #1646: the OpenCode (and Kilo) plugin prepended its reminder with `&&`, which Windows PowerShell 5.1 rejects as a statement separator, breaking the first bash command of every session. Switched to `;` (valid in PowerShell 5.1, Bash, POSIX). #1657: the GRAPH_REPORT.md "Import Cycles" section printed "None detected" on documents-only corpora where imports don't exist — now gated on code nodes / import edges being present. The other two items in that issue (mojibake in manifest/report, stdout encoding) are already handled on current v8: both files are written UTF-8 and main() reconfigures stdout/stderr to UTF-8. Full suite: 2909 passed, 3 skipped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
138 lines
6.7 KiB
Python
138 lines
6.7 KiB
Python
import json
|
|
from pathlib import Path
|
|
from graphify.build import build_from_json
|
|
from graphify.cluster import cluster, score_all
|
|
from graphify.analyze import god_nodes, surprising_connections
|
|
from graphify.report import generate
|
|
|
|
FIXTURES = Path(__file__).parent / "fixtures"
|
|
|
|
def make_inputs():
|
|
extraction = json.loads((FIXTURES / "extraction.json").read_text())
|
|
G = build_from_json(extraction)
|
|
communities = cluster(G)
|
|
cohesion = score_all(G, communities)
|
|
labels = {cid: f"Community {cid}" for cid in communities}
|
|
gods = god_nodes(G)
|
|
surprises = surprising_connections(G)
|
|
detection = {"total_files": 4, "total_words": 62400, "needs_graph": True, "warning": None}
|
|
tokens = {"input": extraction["input_tokens"], "output": extraction["output_tokens"]}
|
|
return G, communities, cohesion, labels, gods, surprises, detection, tokens
|
|
|
|
def test_report_contains_header():
|
|
G, communities, cohesion, labels, gods, surprises, detection, tokens = make_inputs()
|
|
report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, "./project")
|
|
assert "# Graph Report" in report
|
|
|
|
def test_report_contains_corpus_check():
|
|
G, communities, cohesion, labels, gods, surprises, detection, tokens = make_inputs()
|
|
report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, "./project")
|
|
assert "## Corpus Check" in report
|
|
|
|
def test_report_contains_god_nodes():
|
|
G, communities, cohesion, labels, gods, surprises, detection, tokens = make_inputs()
|
|
report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, "./project")
|
|
assert "## God Nodes" in report
|
|
|
|
def test_report_contains_surprising_connections():
|
|
G, communities, cohesion, labels, gods, surprises, detection, tokens = make_inputs()
|
|
report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, "./project")
|
|
assert "## Surprising Connections" in report
|
|
|
|
def test_report_contains_communities():
|
|
G, communities, cohesion, labels, gods, surprises, detection, tokens = make_inputs()
|
|
report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, "./project")
|
|
assert "## Communities" in report
|
|
|
|
def test_report_contains_ambiguous_section():
|
|
G, communities, cohesion, labels, gods, surprises, detection, tokens = make_inputs()
|
|
report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, "./project")
|
|
assert "## Ambiguous Edges" in report
|
|
|
|
def test_report_shows_token_cost():
|
|
G, communities, cohesion, labels, gods, surprises, detection, tokens = make_inputs()
|
|
report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, "./project")
|
|
assert "Token cost" in report
|
|
assert "1,200" in report
|
|
|
|
def test_report_shows_raw_cohesion_scores():
|
|
G, communities, cohesion, labels, gods, surprises, detection, tokens = make_inputs()
|
|
report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, "./project", min_community_size=1)
|
|
assert "Cohesion:" in report
|
|
assert "✓" not in report
|
|
assert "⚠" not in report
|
|
|
|
|
|
# --- work-memory lessons section ----------------------------------------------
|
|
|
|
def test_report_work_memory_section_present_with_overlay_and_dead_ends():
|
|
"""When a work-memory overlay (preferred sources) and query-scoped dead-ends
|
|
are supplied, the report grows a `## Work-memory lessons` section listing the
|
|
preferred sources and, separately, the dead-ends as question -> nodes."""
|
|
G, communities, cohesion, labels, gods, surprises, detection, tokens = make_inputs()
|
|
learning = {
|
|
"overlay": {
|
|
"auth_login": {"status": "preferred", "uses": 3, "score": 2.4,
|
|
"label": "login()", "stale": False},
|
|
"redis": {"status": "tentative", "uses": 1, "score": 0.5,
|
|
"label": "RedisClient", "stale": False},
|
|
},
|
|
"dead_ends": [
|
|
{"question": "does it use websockets?", "nodes": ["WSServer"], "date": "2026-05-01"},
|
|
],
|
|
}
|
|
report = generate(G, communities, cohesion, labels, gods, surprises, detection,
|
|
tokens, "./project", learning=learning)
|
|
assert "## Work-memory lessons" in report
|
|
assert "**Preferred sources**" in report
|
|
assert "`login()`" in report
|
|
# Tentative is not listed in the report's preferred block.
|
|
assert "RedisClient" not in report
|
|
# Dead-ends are query-scoped: question -> nodes, NOT a node-level status.
|
|
assert "**Known dead ends**" in report
|
|
assert "does it use websockets?" in report
|
|
assert "`WSServer`" in report
|
|
|
|
|
|
def test_report_work_memory_section_absent_without_overlay():
|
|
"""No learning input => no section; report identical to pre-feature."""
|
|
G, communities, cohesion, labels, gods, surprises, detection, tokens = make_inputs()
|
|
before = generate(G, communities, cohesion, labels, gods, surprises, detection,
|
|
tokens, "./project")
|
|
assert "## Work-memory lessons" not in before
|
|
# Explicit empty learning also omits the section.
|
|
empty = generate(G, communities, cohesion, labels, gods, surprises, detection,
|
|
tokens, "./project", learning={"overlay": {}, "dead_ends": []})
|
|
assert "## Work-memory lessons" not in empty
|
|
assert before == empty
|
|
|
|
|
|
def test_import_cycles_section_present_for_code_corpus():
|
|
# #1657: the fixture is a code corpus, so the Import Cycles section shows.
|
|
G, communities, cohesion, labels, gods, surprises, detection, tokens = make_inputs()
|
|
report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, "./project")
|
|
assert "## Import Cycles" in report
|
|
|
|
|
|
def test_import_cycles_section_absent_for_documents_only_corpus():
|
|
# #1657: a documents-only corpus has no imports; the section is pure noise
|
|
# ("None detected") and must be suppressed.
|
|
extraction = {
|
|
"nodes": [
|
|
{"id": "d1", "label": "intro.md", "file_type": "document"},
|
|
{"id": "d2", "label": "guide.md", "file_type": "document"},
|
|
],
|
|
"edges": [{"source": "d1", "target": "d2", "relation": "references"}],
|
|
"input_tokens": 0, "output_tokens": 0,
|
|
}
|
|
G = build_from_json(extraction)
|
|
communities = cluster(G)
|
|
cohesion = score_all(G, communities)
|
|
labels = {cid: f"Community {cid}" for cid in communities}
|
|
gods = god_nodes(G)
|
|
surprises = surprising_connections(G)
|
|
detection = {"total_files": 2, "total_words": 100, "needs_graph": True, "warning": None}
|
|
tokens = {"input": 0, "output": 0}
|
|
report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, "./project")
|
|
assert "## Import Cycles" not in report
|