diff --git a/graphify/cli.py b/graphify/cli.py index 4b18c7fb0..aaf9ead82 100644 --- a/graphify/cli.py +++ b/graphify/cli.py @@ -1479,7 +1479,8 @@ def dispatch_command(cmd: str) -> None: print( "Usage: graphify diagnose multigraph " "[--graph path] [--json] [--max-examples N] " - "[--directed] [--undirected] [--extract-path path]", + "[--directed] [--undirected] [--extract-path path] " + "[--fail-on-noncanonical]", file=sys.stderr, ) sys.exit(1) @@ -1490,6 +1491,10 @@ def dispatch_command(cmd: str) -> None: direction_flag: str | None = None json_output = False extract_path: Path | None = None + # Opt-in, not the default: existing scripts/CI that already run this + # command must keep getting exit 0 on a non-canonical (but structurally + # loadable) graph unless they explicitly ask to gate on it. + fail_on_noncanonical = False i = 3 while i < len(sys.argv): @@ -1539,6 +1544,8 @@ def dispatch_command(cmd: str) -> None: print("error: --extract-path requires a path", file=sys.stderr) sys.exit(1) extract_path = Path(sys.argv[i]) + elif arg == "--fail-on-noncanonical": + fail_on_noncanonical = True else: print(f"error: unknown diagnose option {arg}", file=sys.stderr) sys.exit(1) @@ -1567,6 +1574,9 @@ def dispatch_command(cmd: str) -> None: else: print(format_diagnostic_report(summary)) + if fail_on_noncanonical and not summary.get("canonical", True): + sys.exit(1) + elif cmd == "add": if len(sys.argv) < 3: print( diff --git a/graphify/diagnostics.py b/graphify/diagnostics.py index fcb9a11cf..185093368 100644 --- a/graphify/diagnostics.py +++ b/graphify/diagnostics.py @@ -11,6 +11,7 @@ import networkx as nx +from graphify.validate import validate_extraction _SUPPRESSION_DECL_RE = re.compile(r"^\s*(?Pseen_[A-Za-z0-9_]+)\s*[:=]") _TYPE_TUPLE_RE = re.compile(r"set\[tuple\[(?P[^\]]+)\]\]") @@ -153,6 +154,143 @@ def scan_producer_suppression_sites(path: str | Path) -> dict[str, Any]: } +# Relations where direction is citer -> citee between whole-file document/paper +# nodes (extraction-spec.md's "references"/"cites" direction rule for docs, +# mirroring the "calls" direction rule for code). Deliberately excludes "calls" +# and other relation types, whose direction semantics differ. +_CITATION_RELATIONS = frozenset({"references", "cites"}) +_WHOLE_FILE_TYPES = frozenset({"document", "paper"}) + + +def _find_edge_direction_suspects( + extraction: dict[str, Any], node_ids: set[str] +) -> list[dict[str, Any]]: + """Flag references/cites edges whose own source_file matches the TARGET + node's file rather than the SOURCE node's - the extraction-spec.md + self-check for a reversed citation edge (the file that made the assertion + is the target, meaning the target cited the source, not the other way + round). + + High precision by construction: a node's source_file is where that node + was authored; an edge's source_file is where the assertion was found. When + those disagree in exactly this way, the edge was very likely recorded + backwards - the bug class that inflated a real corpus's ARCHITECTURE.md/ + PRODUCT.md "god node" degree with edges actually asserted by the ADRs + citing them, not by the docs themselves. Scoped to document/paper whole- + file nodes on both ends, matching the spec rule this check enforces. + """ + nodes = extraction.get("nodes", []) + if not isinstance(nodes, list): + return [] + file_by_id: dict[str, str] = {} + type_by_id: dict[str, str] = {} + ids_by_file: dict[str, set[str]] = defaultdict(set) + for node in nodes: + if not isinstance(node, dict): + continue + nid = node.get("id") + sf = node.get("source_file") + if not isinstance(nid, str) or not isinstance(sf, str) or not sf: + continue + file_by_id[nid] = sf + type_by_id[nid] = node.get("file_type") + ids_by_file[sf].add(nid) + + suspects: list[dict[str, Any]] = [] + for edge in _edge_list(extraction): + if not isinstance(edge, dict) or edge.get("relation") not in _CITATION_RELATIONS: + continue + source = edge.get("source", edge.get("from")) + target = edge.get("target", edge.get("to")) + edge_source_file = edge.get("source_file") + if not ( + isinstance(source, str) and isinstance(target, str) + and source in node_ids and target in node_ids + and source in file_by_id # source node's own file must be known + and type_by_id.get(source) in _WHOLE_FILE_TYPES + and type_by_id.get(target) in _WHOLE_FILE_TYPES + and isinstance(edge_source_file, str) and edge_source_file + ): + continue + if file_by_id[source] == edge_source_file: + continue # matches the source node's own file - direction looks right. + if target in ids_by_file.get(edge_source_file, ()): + suspects.append( + { + "source": source, + "target": target, + "relation": edge["relation"], + "edge_source_file": edge_source_file, + "source_node_file": file_by_id[source], + } + ) + return suspects + + +def _find_duplicate_whole_file_candidates( + extraction: dict[str, Any], +) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: + """Flag document/paper nodes that probably represent the same real file + under two different ids - a canonical-identity violation. + + Returns (hard, soft): + - hard: id B == id A + "_document" (or "_file"/"_page") - the exact + whole-file-node suffix a subagent must never invent (extraction-spec.md + "Whole-file nodes"), almost never a coincidence. + - soft: labels look like the same file (one is the other's exact prefix + up to a " - "/" -- "/" (" separator, e.g. "ARCHITECTURE.md" vs + "ARCHITECTURE.md - Avenoria Technical Architecture") on two nodes with + different source_file - plausible but not certain, kept separate from + `hard` so it never blocks canonical status on its own. + """ + nodes = [ + n for n in extraction.get("nodes", []) + if isinstance(n, dict) and n.get("file_type") in _WHOLE_FILE_TYPES + and isinstance(n.get("id"), str) and isinstance(n.get("label"), str) + ] + by_id = {n["id"]: n for n in nodes} + + hard: list[dict[str, Any]] = [] + hard_pairs: set[tuple[str, str]] = set() + for n in nodes: + for suffix in ("_document", "_file", "_page"): + candidate = n["id"] + suffix + other = by_id.get(candidate) + if other is None: + continue + pair = tuple(sorted((n["id"], other["id"]))) + if pair in hard_pairs: + continue + hard_pairs.add(pair) + hard.append( + {"node_a": n["id"], "node_b": other["id"], "reason": f"id suffix {suffix!r}"} + ) + + soft: list[dict[str, Any]] = [] + seen_soft: set[tuple[str, str]] = set() + separators = (" - ", " -- ", " (") + for n in nodes: + for m in nodes: + if n["id"] >= m["id"]: + continue + pair = (n["id"], m["id"]) + if pair in hard_pairs or n.get("source_file") == m.get("source_file"): + continue # already caught by the id-suffix check, or same file + # (e.g. a heading node) - not a whole-file duplicate. + shorter, longer = sorted((n["label"], m["label"]), key=len) + if not longer.startswith(shorter): + continue + rest = longer[len(shorter):] + if not any(rest.startswith(sep) for sep in separators): + continue + if pair in seen_soft: + continue + seen_soft.add(pair) + soft.append({"node_a": n["id"], "node_b": m["id"], "reason": "label prefix"}) + + return hard, soft + + def diagnose_extraction( extraction: dict[str, Any], *, @@ -161,12 +299,27 @@ def diagnose_extraction( max_examples: int = 5, extract_path: str | Path | None = None, ) -> dict[str, Any]: - """Summarize same-endpoint edge-collapse risk for one JSON graph/extraction dict.""" + """Summarize same-endpoint edge-collapse risk for one JSON graph/extraction dict. + + Also runs the graph-validity gate this function's docstring didn't used to + cover: schema errors, reversed citation edges, and probable duplicate + whole-file nodes. The `canonical` field is the answer to "can architectural + metrics (centrality, clustering, dependency analysis) computed from this + graph be trusted, or are they informational only until the issues below are + fixed?" A non-canonical graph is not corrupt or unusable - it can still be + built and browsed - but its structural metrics may not reflect the real + architecture and should be caveated accordingly. + """ from graphify.build import build_from_json node_ids = _node_ids(extraction) raw_edges = _edge_list(extraction) canonical_edges = [_canonical_edge(edge) for edge in raw_edges] + schema_errors = validate_extraction(extraction) + edge_direction_suspects = _find_edge_direction_suspects(extraction, node_ids) + duplicate_node_candidates, duplicate_node_candidates_soft = ( + _find_duplicate_whole_file_candidates(extraction) + ) # Code-typed semantic nodes the extractor could not verify against the source # it read (#1949): likely-inferred (or hallucinated) symbols surfaced from a @@ -245,7 +398,36 @@ def diagnose_extraction( Path(extract_path) if extract_path else Path(__file__).with_name("extract.py") ) + # What blocks `canonical`: schema errors, dangling/missing edge endpoints, + # reversed citation edges, and hard (id-suffix) duplicate whole-file nodes. + # What does NOT block it (informational only, per the report/warnings + # below): edge-collapse counts, self-loops, and the soft (label-prefix) + # duplicate candidates - each has a plausible benign explanation on its + # own, so they are visibility, not proof. + canonical_issues: list[str] = [] + if schema_errors: + canonical_issues.append(f"{len(schema_errors)} schema error(s)") + if missing_endpoint_edges: + canonical_issues.append(f"{missing_endpoint_edges} edge(s) with a missing endpoint") + if dangling_endpoint_edges: + canonical_issues.append(f"{dangling_endpoint_edges} edge(s) with a dangling endpoint") + if edge_direction_suspects: + canonical_issues.append( + f"{len(edge_direction_suspects)} references/cites edge(s) with a likely-reversed direction" + ) + if duplicate_node_candidates: + canonical_issues.append( + f"{len(duplicate_node_candidates)} probable duplicate whole-file node pair(s)" + ) + canonical = not canonical_issues + return { + "canonical": canonical, + "canonical_issues": canonical_issues, + "schema_errors": schema_errors, + "edge_direction_suspects": edge_direction_suspects, + "duplicate_node_candidates": duplicate_node_candidates, + "duplicate_node_candidates_soft": duplicate_node_candidates_soft, "node_count": len(node_ids), "unverified_node_count": unverified_node_count, "raw_edge_count": len(raw_edges), @@ -347,8 +529,17 @@ def format_diagnostic_json(summary: dict[str, Any]) -> dict[str, Any]: def format_diagnostic_report(summary: dict[str, Any]) -> str: suppression = summary.get("producer_suppression", {}) + canonical = summary.get("canonical", True) + verdict = "CANONICAL" if canonical else "NON-CANONICAL" lines = [ "[graphify] MultiDiGraph edge-collapse diagnostic", + f"verdict: {verdict}" + + ( + "" + if canonical + else " - treat centrality/clustering/dependency metrics as informational " + "only until the issues below are fixed" + ), f"input: {summary.get('input_path', '')}", "input_stage: provided JSON (normal graph.json is post-build)", f"effective_directed: {summary.get('effective_directed', '')}", @@ -358,6 +549,10 @@ def format_diagnostic_report(summary: dict[str, Any]) -> str: f"valid_candidate_edges: {summary['valid_candidate_edges']}", f"missing_endpoint_edges: {summary['missing_endpoint_edges']}", f"dangling_endpoint_edges: {summary['dangling_endpoint_edges']}", + f"schema_errors: {len(summary.get('schema_errors', []))}", + f"edge_direction_suspects: {len(summary.get('edge_direction_suspects', []))}", + f"duplicate_node_candidates: {len(summary.get('duplicate_node_candidates', []))}" + f" (+{len(summary.get('duplicate_node_candidates_soft', []))} soft)", f"self_loop_edges: {summary['self_loop_edges']}", f"exact_duplicate_edges: {summary['exact_duplicate_edges']}", f"directed_unique_endpoint_pairs: {summary['directed_unique_endpoint_pairs']}", @@ -379,6 +574,26 @@ def format_diagnostic_report(summary: dict[str, Any]) -> str: f"post_build_edges: {summary['post_build_edge_count']}", f"producer_suppression_sites: {suppression.get('total_sites', 0)}", ] + if summary.get("schema_errors"): + lines.append("schema_errors:") + for error in summary["schema_errors"][:8]: + lines.append(f" - {error}") + if summary.get("edge_direction_suspects"): + lines.append("edge_direction_suspects (likely reversed references/cites):") + for s in summary["edge_direction_suspects"][:8]: + lines.append( + f" - {s['source']} --{s['relation']}--> {s['target']} " + f"(edge asserted in {s['edge_source_file']!r}, which is {s['target']}'s own file, " + f"not {s['source']}'s {s['source_node_file']!r})" + ) + if summary.get("duplicate_node_candidates"): + lines.append("duplicate_node_candidates (probable same-file split, id suffix):") + for d in summary["duplicate_node_candidates"][:8]: + lines.append(f" - {d['node_a']} <-> {d['node_b']} ({d['reason']})") + if summary.get("duplicate_node_candidates_soft"): + lines.append("duplicate_node_candidates_soft (label prefix, needs a human glance):") + for d in summary["duplicate_node_candidates_soft"][:8]: + lines.append(f" - {d['node_a']} <-> {d['node_b']} ({d['reason']})") if summary.get("post_build_error"): lines.append(f"post_build_error: {summary['post_build_error']}") if suppression.get("error"): diff --git a/graphify/skill-agents.md b/graphify/skill-agents.md index 190827d9a..099fd00cd 100644 --- a/graphify/skill-agents.md +++ b/graphify/skill-agents.md @@ -452,7 +452,7 @@ Replace INPUT_PATH with the actual path. ### Step 4.5 - Graph health check (read-only integrity gate) -A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, and self-loops — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. +A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, self-loops, schema errors, likely-reversed citation edges, and probable duplicate whole-file nodes (canonical-identity violations) — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. ```bash $(cat graphify-out/.graphify_python) -c " @@ -471,11 +471,19 @@ flags = [f'{summary[k]} {label}' for k, label in ( ('undirected_same_endpoint_collapsed_edges', 'collapsed (undirected) edges'), ) if summary.get(k, 0)] print('GRAPH HEALTH WARNING: ' + '; '.join(flags) + ' - graph may be incomplete/corrupt.' if flags else 'Graph health: OK (no dangling/missing/collapsed edges).') +is_canonical = summary.get('canonical', True) +issues_text = '; '.join(summary.get('canonical_issues', [])) +canonical_line = 'CANONICAL: ' + str(is_canonical) +if not is_canonical: + canonical_line += ' - ' + issues_text +print(canonical_line) " ``` Substitute `IS_DIRECTED` and `INPUT_PATH` as in Step 4. If a `GRAPH HEALTH WARNING` prints, surface it in the final summary (do not abort — the graph is still usable, but the integrity issue must be visible, per the Honesty Rules). +**If `CANONICAL: False`:** the graph has a structural defect stronger than a collapse warning — a schema error, a dangling/missing edge endpoint, a references/cites edge that is very likely drawn backwards, or two nodes that are probably the same real file under two different ids (see `duplicate_node_candidates` in the report). Say so plainly in the final summary, and while it stays non-canonical: treat god-node/centrality rankings, community/clustering results, and dependency-direction claims as **informational only**, not settled fact — a node's inflated apparent importance may be an artifact of exactly this kind of split, not real architectural coupling. Do not silently present god-node degree or "X is the most central node" as fact against a non-canonical graph; name the specific defect instead. `duplicate_node_candidates_soft` (label-prefix matches without the id-suffix pattern) does not by itself flip `canonical` to False — treat it as a lead worth a glance, not a proven defect. + ### Step 5 - Label communities Read `graphify-out/.graphify_analysis.json`. For each community key, look at its node labels and write a 2-5 word plain-language name (e.g. "Attention Mechanism", "Training Pipeline", "Data Loading"). diff --git a/graphify/skill-amp.md b/graphify/skill-amp.md index 190827d9a..099fd00cd 100644 --- a/graphify/skill-amp.md +++ b/graphify/skill-amp.md @@ -452,7 +452,7 @@ Replace INPUT_PATH with the actual path. ### Step 4.5 - Graph health check (read-only integrity gate) -A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, and self-loops — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. +A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, self-loops, schema errors, likely-reversed citation edges, and probable duplicate whole-file nodes (canonical-identity violations) — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. ```bash $(cat graphify-out/.graphify_python) -c " @@ -471,11 +471,19 @@ flags = [f'{summary[k]} {label}' for k, label in ( ('undirected_same_endpoint_collapsed_edges', 'collapsed (undirected) edges'), ) if summary.get(k, 0)] print('GRAPH HEALTH WARNING: ' + '; '.join(flags) + ' - graph may be incomplete/corrupt.' if flags else 'Graph health: OK (no dangling/missing/collapsed edges).') +is_canonical = summary.get('canonical', True) +issues_text = '; '.join(summary.get('canonical_issues', [])) +canonical_line = 'CANONICAL: ' + str(is_canonical) +if not is_canonical: + canonical_line += ' - ' + issues_text +print(canonical_line) " ``` Substitute `IS_DIRECTED` and `INPUT_PATH` as in Step 4. If a `GRAPH HEALTH WARNING` prints, surface it in the final summary (do not abort — the graph is still usable, but the integrity issue must be visible, per the Honesty Rules). +**If `CANONICAL: False`:** the graph has a structural defect stronger than a collapse warning — a schema error, a dangling/missing edge endpoint, a references/cites edge that is very likely drawn backwards, or two nodes that are probably the same real file under two different ids (see `duplicate_node_candidates` in the report). Say so plainly in the final summary, and while it stays non-canonical: treat god-node/centrality rankings, community/clustering results, and dependency-direction claims as **informational only**, not settled fact — a node's inflated apparent importance may be an artifact of exactly this kind of split, not real architectural coupling. Do not silently present god-node degree or "X is the most central node" as fact against a non-canonical graph; name the specific defect instead. `duplicate_node_candidates_soft` (label-prefix matches without the id-suffix pattern) does not by itself flip `canonical` to False — treat it as a lead worth a glance, not a proven defect. + ### Step 5 - Label communities Read `graphify-out/.graphify_analysis.json`. For each community key, look at its node labels and write a 2-5 word plain-language name (e.g. "Attention Mechanism", "Training Pipeline", "Data Loading"). diff --git a/graphify/skill-claw.md b/graphify/skill-claw.md index abd2811d2..715aa76a7 100644 --- a/graphify/skill-claw.md +++ b/graphify/skill-claw.md @@ -455,7 +455,7 @@ Replace INPUT_PATH with the actual path. ### Step 4.5 - Graph health check (read-only integrity gate) -A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, and self-loops — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. +A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, self-loops, schema errors, likely-reversed citation edges, and probable duplicate whole-file nodes (canonical-identity violations) — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. ```bash $(cat graphify-out/.graphify_python) -c " @@ -474,11 +474,19 @@ flags = [f'{summary[k]} {label}' for k, label in ( ('undirected_same_endpoint_collapsed_edges', 'collapsed (undirected) edges'), ) if summary.get(k, 0)] print('GRAPH HEALTH WARNING: ' + '; '.join(flags) + ' - graph may be incomplete/corrupt.' if flags else 'Graph health: OK (no dangling/missing/collapsed edges).') +is_canonical = summary.get('canonical', True) +issues_text = '; '.join(summary.get('canonical_issues', [])) +canonical_line = 'CANONICAL: ' + str(is_canonical) +if not is_canonical: + canonical_line += ' - ' + issues_text +print(canonical_line) " ``` Substitute `IS_DIRECTED` and `INPUT_PATH` as in Step 4. If a `GRAPH HEALTH WARNING` prints, surface it in the final summary (do not abort — the graph is still usable, but the integrity issue must be visible, per the Honesty Rules). +**If `CANONICAL: False`:** the graph has a structural defect stronger than a collapse warning — a schema error, a dangling/missing edge endpoint, a references/cites edge that is very likely drawn backwards, or two nodes that are probably the same real file under two different ids (see `duplicate_node_candidates` in the report). Say so plainly in the final summary, and while it stays non-canonical: treat god-node/centrality rankings, community/clustering results, and dependency-direction claims as **informational only**, not settled fact — a node's inflated apparent importance may be an artifact of exactly this kind of split, not real architectural coupling. Do not silently present god-node degree or "X is the most central node" as fact against a non-canonical graph; name the specific defect instead. `duplicate_node_candidates_soft` (label-prefix matches without the id-suffix pattern) does not by itself flip `canonical` to False — treat it as a lead worth a glance, not a proven defect. + ### Step 5 - Label communities Read `graphify-out/.graphify_analysis.json`. For each community key, look at its node labels and write a 2-5 word plain-language name (e.g. "Attention Mechanism", "Training Pipeline", "Data Loading"). diff --git a/graphify/skill-codex.md b/graphify/skill-codex.md index af3f723c7..16d694abc 100644 --- a/graphify/skill-codex.md +++ b/graphify/skill-codex.md @@ -452,7 +452,7 @@ Replace INPUT_PATH with the actual path. ### Step 4.5 - Graph health check (read-only integrity gate) -A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, and self-loops — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. +A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, self-loops, schema errors, likely-reversed citation edges, and probable duplicate whole-file nodes (canonical-identity violations) — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. ```bash $(cat graphify-out/.graphify_python) -c " @@ -471,11 +471,19 @@ flags = [f'{summary[k]} {label}' for k, label in ( ('undirected_same_endpoint_collapsed_edges', 'collapsed (undirected) edges'), ) if summary.get(k, 0)] print('GRAPH HEALTH WARNING: ' + '; '.join(flags) + ' - graph may be incomplete/corrupt.' if flags else 'Graph health: OK (no dangling/missing/collapsed edges).') +is_canonical = summary.get('canonical', True) +issues_text = '; '.join(summary.get('canonical_issues', [])) +canonical_line = 'CANONICAL: ' + str(is_canonical) +if not is_canonical: + canonical_line += ' - ' + issues_text +print(canonical_line) " ``` Substitute `IS_DIRECTED` and `INPUT_PATH` as in Step 4. If a `GRAPH HEALTH WARNING` prints, surface it in the final summary (do not abort — the graph is still usable, but the integrity issue must be visible, per the Honesty Rules). +**If `CANONICAL: False`:** the graph has a structural defect stronger than a collapse warning — a schema error, a dangling/missing edge endpoint, a references/cites edge that is very likely drawn backwards, or two nodes that are probably the same real file under two different ids (see `duplicate_node_candidates` in the report). Say so plainly in the final summary, and while it stays non-canonical: treat god-node/centrality rankings, community/clustering results, and dependency-direction claims as **informational only**, not settled fact — a node's inflated apparent importance may be an artifact of exactly this kind of split, not real architectural coupling. Do not silently present god-node degree or "X is the most central node" as fact against a non-canonical graph; name the specific defect instead. `duplicate_node_candidates_soft` (label-prefix matches without the id-suffix pattern) does not by itself flip `canonical` to False — treat it as a lead worth a glance, not a proven defect. + ### Step 5 - Label communities Read `graphify-out/.graphify_analysis.json`. For each community key, look at its node labels and write a 2-5 word plain-language name (e.g. "Attention Mechanism", "Training Pipeline", "Data Loading"). diff --git a/graphify/skill-copilot.md b/graphify/skill-copilot.md index abd2811d2..715aa76a7 100644 --- a/graphify/skill-copilot.md +++ b/graphify/skill-copilot.md @@ -455,7 +455,7 @@ Replace INPUT_PATH with the actual path. ### Step 4.5 - Graph health check (read-only integrity gate) -A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, and self-loops — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. +A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, self-loops, schema errors, likely-reversed citation edges, and probable duplicate whole-file nodes (canonical-identity violations) — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. ```bash $(cat graphify-out/.graphify_python) -c " @@ -474,11 +474,19 @@ flags = [f'{summary[k]} {label}' for k, label in ( ('undirected_same_endpoint_collapsed_edges', 'collapsed (undirected) edges'), ) if summary.get(k, 0)] print('GRAPH HEALTH WARNING: ' + '; '.join(flags) + ' - graph may be incomplete/corrupt.' if flags else 'Graph health: OK (no dangling/missing/collapsed edges).') +is_canonical = summary.get('canonical', True) +issues_text = '; '.join(summary.get('canonical_issues', [])) +canonical_line = 'CANONICAL: ' + str(is_canonical) +if not is_canonical: + canonical_line += ' - ' + issues_text +print(canonical_line) " ``` Substitute `IS_DIRECTED` and `INPUT_PATH` as in Step 4. If a `GRAPH HEALTH WARNING` prints, surface it in the final summary (do not abort — the graph is still usable, but the integrity issue must be visible, per the Honesty Rules). +**If `CANONICAL: False`:** the graph has a structural defect stronger than a collapse warning — a schema error, a dangling/missing edge endpoint, a references/cites edge that is very likely drawn backwards, or two nodes that are probably the same real file under two different ids (see `duplicate_node_candidates` in the report). Say so plainly in the final summary, and while it stays non-canonical: treat god-node/centrality rankings, community/clustering results, and dependency-direction claims as **informational only**, not settled fact — a node's inflated apparent importance may be an artifact of exactly this kind of split, not real architectural coupling. Do not silently present god-node degree or "X is the most central node" as fact against a non-canonical graph; name the specific defect instead. `duplicate_node_candidates_soft` (label-prefix matches without the id-suffix pattern) does not by itself flip `canonical` to False — treat it as a lead worth a glance, not a proven defect. + ### Step 5 - Label communities Read `graphify-out/.graphify_analysis.json`. For each community key, look at its node labels and write a 2-5 word plain-language name (e.g. "Attention Mechanism", "Training Pipeline", "Data Loading"). diff --git a/graphify/skill-droid.md b/graphify/skill-droid.md index fd148d485..fd5e3796e 100644 --- a/graphify/skill-droid.md +++ b/graphify/skill-droid.md @@ -452,7 +452,7 @@ Replace INPUT_PATH with the actual path. ### Step 4.5 - Graph health check (read-only integrity gate) -A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, and self-loops — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. +A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, self-loops, schema errors, likely-reversed citation edges, and probable duplicate whole-file nodes (canonical-identity violations) — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. ```bash $(cat graphify-out/.graphify_python) -c " @@ -471,11 +471,19 @@ flags = [f'{summary[k]} {label}' for k, label in ( ('undirected_same_endpoint_collapsed_edges', 'collapsed (undirected) edges'), ) if summary.get(k, 0)] print('GRAPH HEALTH WARNING: ' + '; '.join(flags) + ' - graph may be incomplete/corrupt.' if flags else 'Graph health: OK (no dangling/missing/collapsed edges).') +is_canonical = summary.get('canonical', True) +issues_text = '; '.join(summary.get('canonical_issues', [])) +canonical_line = 'CANONICAL: ' + str(is_canonical) +if not is_canonical: + canonical_line += ' - ' + issues_text +print(canonical_line) " ``` Substitute `IS_DIRECTED` and `INPUT_PATH` as in Step 4. If a `GRAPH HEALTH WARNING` prints, surface it in the final summary (do not abort — the graph is still usable, but the integrity issue must be visible, per the Honesty Rules). +**If `CANONICAL: False`:** the graph has a structural defect stronger than a collapse warning — a schema error, a dangling/missing edge endpoint, a references/cites edge that is very likely drawn backwards, or two nodes that are probably the same real file under two different ids (see `duplicate_node_candidates` in the report). Say so plainly in the final summary, and while it stays non-canonical: treat god-node/centrality rankings, community/clustering results, and dependency-direction claims as **informational only**, not settled fact — a node's inflated apparent importance may be an artifact of exactly this kind of split, not real architectural coupling. Do not silently present god-node degree or "X is the most central node" as fact against a non-canonical graph; name the specific defect instead. `duplicate_node_candidates_soft` (label-prefix matches without the id-suffix pattern) does not by itself flip `canonical` to False — treat it as a lead worth a glance, not a proven defect. + ### Step 5 - Label communities Read `graphify-out/.graphify_analysis.json`. For each community key, look at its node labels and write a 2-5 word plain-language name (e.g. "Attention Mechanism", "Training Pipeline", "Data Loading"). diff --git a/graphify/skill-kilo.md b/graphify/skill-kilo.md index 3e70b050a..6925ca543 100644 --- a/graphify/skill-kilo.md +++ b/graphify/skill-kilo.md @@ -455,7 +455,7 @@ Replace INPUT_PATH with the actual path. ### Step 4.5 - Graph health check (read-only integrity gate) -A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, and self-loops — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. +A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, self-loops, schema errors, likely-reversed citation edges, and probable duplicate whole-file nodes (canonical-identity violations) — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. ```bash $(cat graphify-out/.graphify_python) -c " @@ -474,11 +474,19 @@ flags = [f'{summary[k]} {label}' for k, label in ( ('undirected_same_endpoint_collapsed_edges', 'collapsed (undirected) edges'), ) if summary.get(k, 0)] print('GRAPH HEALTH WARNING: ' + '; '.join(flags) + ' - graph may be incomplete/corrupt.' if flags else 'Graph health: OK (no dangling/missing/collapsed edges).') +is_canonical = summary.get('canonical', True) +issues_text = '; '.join(summary.get('canonical_issues', [])) +canonical_line = 'CANONICAL: ' + str(is_canonical) +if not is_canonical: + canonical_line += ' - ' + issues_text +print(canonical_line) " ``` Substitute `IS_DIRECTED` and `INPUT_PATH` as in Step 4. If a `GRAPH HEALTH WARNING` prints, surface it in the final summary (do not abort — the graph is still usable, but the integrity issue must be visible, per the Honesty Rules). +**If `CANONICAL: False`:** the graph has a structural defect stronger than a collapse warning — a schema error, a dangling/missing edge endpoint, a references/cites edge that is very likely drawn backwards, or two nodes that are probably the same real file under two different ids (see `duplicate_node_candidates` in the report). Say so plainly in the final summary, and while it stays non-canonical: treat god-node/centrality rankings, community/clustering results, and dependency-direction claims as **informational only**, not settled fact — a node's inflated apparent importance may be an artifact of exactly this kind of split, not real architectural coupling. Do not silently present god-node degree or "X is the most central node" as fact against a non-canonical graph; name the specific defect instead. `duplicate_node_candidates_soft` (label-prefix matches without the id-suffix pattern) does not by itself flip `canonical` to False — treat it as a lead worth a glance, not a proven defect. + ### Step 5 - Label communities Read `graphify-out/.graphify_analysis.json`. For each community key, look at its node labels and write a 2-5 word plain-language name (e.g. "Attention Mechanism", "Training Pipeline", "Data Loading"). diff --git a/graphify/skill-kiro.md b/graphify/skill-kiro.md index abd2811d2..715aa76a7 100644 --- a/graphify/skill-kiro.md +++ b/graphify/skill-kiro.md @@ -455,7 +455,7 @@ Replace INPUT_PATH with the actual path. ### Step 4.5 - Graph health check (read-only integrity gate) -A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, and self-loops — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. +A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, self-loops, schema errors, likely-reversed citation edges, and probable duplicate whole-file nodes (canonical-identity violations) — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. ```bash $(cat graphify-out/.graphify_python) -c " @@ -474,11 +474,19 @@ flags = [f'{summary[k]} {label}' for k, label in ( ('undirected_same_endpoint_collapsed_edges', 'collapsed (undirected) edges'), ) if summary.get(k, 0)] print('GRAPH HEALTH WARNING: ' + '; '.join(flags) + ' - graph may be incomplete/corrupt.' if flags else 'Graph health: OK (no dangling/missing/collapsed edges).') +is_canonical = summary.get('canonical', True) +issues_text = '; '.join(summary.get('canonical_issues', [])) +canonical_line = 'CANONICAL: ' + str(is_canonical) +if not is_canonical: + canonical_line += ' - ' + issues_text +print(canonical_line) " ``` Substitute `IS_DIRECTED` and `INPUT_PATH` as in Step 4. If a `GRAPH HEALTH WARNING` prints, surface it in the final summary (do not abort — the graph is still usable, but the integrity issue must be visible, per the Honesty Rules). +**If `CANONICAL: False`:** the graph has a structural defect stronger than a collapse warning — a schema error, a dangling/missing edge endpoint, a references/cites edge that is very likely drawn backwards, or two nodes that are probably the same real file under two different ids (see `duplicate_node_candidates` in the report). Say so plainly in the final summary, and while it stays non-canonical: treat god-node/centrality rankings, community/clustering results, and dependency-direction claims as **informational only**, not settled fact — a node's inflated apparent importance may be an artifact of exactly this kind of split, not real architectural coupling. Do not silently present god-node degree or "X is the most central node" as fact against a non-canonical graph; name the specific defect instead. `duplicate_node_candidates_soft` (label-prefix matches without the id-suffix pattern) does not by itself flip `canonical` to False — treat it as a lead worth a glance, not a proven defect. + ### Step 5 - Label communities Read `graphify-out/.graphify_analysis.json`. For each community key, look at its node labels and write a 2-5 word plain-language name (e.g. "Attention Mechanism", "Training Pipeline", "Data Loading"). diff --git a/graphify/skill-opencode.md b/graphify/skill-opencode.md index 91ced6067..7c8a0a39a 100644 --- a/graphify/skill-opencode.md +++ b/graphify/skill-opencode.md @@ -447,7 +447,7 @@ Replace INPUT_PATH with the actual path. ### Step 4.5 - Graph health check (read-only integrity gate) -A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, and self-loops — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. +A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, self-loops, schema errors, likely-reversed citation edges, and probable duplicate whole-file nodes (canonical-identity violations) — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. ```bash $(cat graphify-out/.graphify_python) -c " @@ -466,11 +466,19 @@ flags = [f'{summary[k]} {label}' for k, label in ( ('undirected_same_endpoint_collapsed_edges', 'collapsed (undirected) edges'), ) if summary.get(k, 0)] print('GRAPH HEALTH WARNING: ' + '; '.join(flags) + ' - graph may be incomplete/corrupt.' if flags else 'Graph health: OK (no dangling/missing/collapsed edges).') +is_canonical = summary.get('canonical', True) +issues_text = '; '.join(summary.get('canonical_issues', [])) +canonical_line = 'CANONICAL: ' + str(is_canonical) +if not is_canonical: + canonical_line += ' - ' + issues_text +print(canonical_line) " ``` Substitute `IS_DIRECTED` and `INPUT_PATH` as in Step 4. If a `GRAPH HEALTH WARNING` prints, surface it in the final summary (do not abort — the graph is still usable, but the integrity issue must be visible, per the Honesty Rules). +**If `CANONICAL: False`:** the graph has a structural defect stronger than a collapse warning — a schema error, a dangling/missing edge endpoint, a references/cites edge that is very likely drawn backwards, or two nodes that are probably the same real file under two different ids (see `duplicate_node_candidates` in the report). Say so plainly in the final summary, and while it stays non-canonical: treat god-node/centrality rankings, community/clustering results, and dependency-direction claims as **informational only**, not settled fact — a node's inflated apparent importance may be an artifact of exactly this kind of split, not real architectural coupling. Do not silently present god-node degree or "X is the most central node" as fact against a non-canonical graph; name the specific defect instead. `duplicate_node_candidates_soft` (label-prefix matches without the id-suffix pattern) does not by itself flip `canonical` to False — treat it as a lead worth a glance, not a proven defect. + ### Step 5 - Label communities Read `graphify-out/.graphify_analysis.json`. For each community key, look at its node labels and write a 2-5 word plain-language name (e.g. "Attention Mechanism", "Training Pipeline", "Data Loading"). diff --git a/graphify/skill-pi.md b/graphify/skill-pi.md index abd2811d2..715aa76a7 100644 --- a/graphify/skill-pi.md +++ b/graphify/skill-pi.md @@ -455,7 +455,7 @@ Replace INPUT_PATH with the actual path. ### Step 4.5 - Graph health check (read-only integrity gate) -A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, and self-loops — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. +A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, self-loops, schema errors, likely-reversed citation edges, and probable duplicate whole-file nodes (canonical-identity violations) — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. ```bash $(cat graphify-out/.graphify_python) -c " @@ -474,11 +474,19 @@ flags = [f'{summary[k]} {label}' for k, label in ( ('undirected_same_endpoint_collapsed_edges', 'collapsed (undirected) edges'), ) if summary.get(k, 0)] print('GRAPH HEALTH WARNING: ' + '; '.join(flags) + ' - graph may be incomplete/corrupt.' if flags else 'Graph health: OK (no dangling/missing/collapsed edges).') +is_canonical = summary.get('canonical', True) +issues_text = '; '.join(summary.get('canonical_issues', [])) +canonical_line = 'CANONICAL: ' + str(is_canonical) +if not is_canonical: + canonical_line += ' - ' + issues_text +print(canonical_line) " ``` Substitute `IS_DIRECTED` and `INPUT_PATH` as in Step 4. If a `GRAPH HEALTH WARNING` prints, surface it in the final summary (do not abort — the graph is still usable, but the integrity issue must be visible, per the Honesty Rules). +**If `CANONICAL: False`:** the graph has a structural defect stronger than a collapse warning — a schema error, a dangling/missing edge endpoint, a references/cites edge that is very likely drawn backwards, or two nodes that are probably the same real file under two different ids (see `duplicate_node_candidates` in the report). Say so plainly in the final summary, and while it stays non-canonical: treat god-node/centrality rankings, community/clustering results, and dependency-direction claims as **informational only**, not settled fact — a node's inflated apparent importance may be an artifact of exactly this kind of split, not real architectural coupling. Do not silently present god-node degree or "X is the most central node" as fact against a non-canonical graph; name the specific defect instead. `duplicate_node_candidates_soft` (label-prefix matches without the id-suffix pattern) does not by itself flip `canonical` to False — treat it as a lead worth a glance, not a proven defect. + ### Step 5 - Label communities Read `graphify-out/.graphify_analysis.json`. For each community key, look at its node labels and write a 2-5 word plain-language name (e.g. "Attention Mechanism", "Training Pipeline", "Data Loading"). diff --git a/graphify/skill-trae.md b/graphify/skill-trae.md index 050667bc2..5b5b69872 100644 --- a/graphify/skill-trae.md +++ b/graphify/skill-trae.md @@ -453,7 +453,7 @@ Replace INPUT_PATH with the actual path. ### Step 4.5 - Graph health check (read-only integrity gate) -A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, and self-loops — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. +A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, self-loops, schema errors, likely-reversed citation edges, and probable duplicate whole-file nodes (canonical-identity violations) — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. ```bash $(cat graphify-out/.graphify_python) -c " @@ -472,11 +472,19 @@ flags = [f'{summary[k]} {label}' for k, label in ( ('undirected_same_endpoint_collapsed_edges', 'collapsed (undirected) edges'), ) if summary.get(k, 0)] print('GRAPH HEALTH WARNING: ' + '; '.join(flags) + ' - graph may be incomplete/corrupt.' if flags else 'Graph health: OK (no dangling/missing/collapsed edges).') +is_canonical = summary.get('canonical', True) +issues_text = '; '.join(summary.get('canonical_issues', [])) +canonical_line = 'CANONICAL: ' + str(is_canonical) +if not is_canonical: + canonical_line += ' - ' + issues_text +print(canonical_line) " ``` Substitute `IS_DIRECTED` and `INPUT_PATH` as in Step 4. If a `GRAPH HEALTH WARNING` prints, surface it in the final summary (do not abort — the graph is still usable, but the integrity issue must be visible, per the Honesty Rules). +**If `CANONICAL: False`:** the graph has a structural defect stronger than a collapse warning — a schema error, a dangling/missing edge endpoint, a references/cites edge that is very likely drawn backwards, or two nodes that are probably the same real file under two different ids (see `duplicate_node_candidates` in the report). Say so plainly in the final summary, and while it stays non-canonical: treat god-node/centrality rankings, community/clustering results, and dependency-direction claims as **informational only**, not settled fact — a node's inflated apparent importance may be an artifact of exactly this kind of split, not real architectural coupling. Do not silently present god-node degree or "X is the most central node" as fact against a non-canonical graph; name the specific defect instead. `duplicate_node_candidates_soft` (label-prefix matches without the id-suffix pattern) does not by itself flip `canonical` to False — treat it as a lead worth a glance, not a proven defect. + ### Step 5 - Label communities Read `graphify-out/.graphify_analysis.json`. For each community key, look at its node labels and write a 2-5 word plain-language name (e.g. "Attention Mechanism", "Training Pipeline", "Data Loading"). diff --git a/graphify/skill-vscode.md b/graphify/skill-vscode.md index 20c7c0835..92fb27cec 100644 --- a/graphify/skill-vscode.md +++ b/graphify/skill-vscode.md @@ -451,7 +451,7 @@ Replace INPUT_PATH with the actual path. ### Step 4.5 - Graph health check (read-only integrity gate) -A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, and self-loops — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. +A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, self-loops, schema errors, likely-reversed citation edges, and probable duplicate whole-file nodes (canonical-identity violations) — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. ```bash $(cat graphify-out/.graphify_python) -c " @@ -470,11 +470,19 @@ flags = [f'{summary[k]} {label}' for k, label in ( ('undirected_same_endpoint_collapsed_edges', 'collapsed (undirected) edges'), ) if summary.get(k, 0)] print('GRAPH HEALTH WARNING: ' + '; '.join(flags) + ' - graph may be incomplete/corrupt.' if flags else 'Graph health: OK (no dangling/missing/collapsed edges).') +is_canonical = summary.get('canonical', True) +issues_text = '; '.join(summary.get('canonical_issues', [])) +canonical_line = 'CANONICAL: ' + str(is_canonical) +if not is_canonical: + canonical_line += ' - ' + issues_text +print(canonical_line) " ``` Substitute `IS_DIRECTED` and `INPUT_PATH` as in Step 4. If a `GRAPH HEALTH WARNING` prints, surface it in the final summary (do not abort — the graph is still usable, but the integrity issue must be visible, per the Honesty Rules). +**If `CANONICAL: False`:** the graph has a structural defect stronger than a collapse warning — a schema error, a dangling/missing edge endpoint, a references/cites edge that is very likely drawn backwards, or two nodes that are probably the same real file under two different ids (see `duplicate_node_candidates` in the report). Say so plainly in the final summary, and while it stays non-canonical: treat god-node/centrality rankings, community/clustering results, and dependency-direction claims as **informational only**, not settled fact — a node's inflated apparent importance may be an artifact of exactly this kind of split, not real architectural coupling. Do not silently present god-node degree or "X is the most central node" as fact against a non-canonical graph; name the specific defect instead. `duplicate_node_candidates_soft` (label-prefix matches without the id-suffix pattern) does not by itself flip `canonical` to False — treat it as a lead worth a glance, not a proven defect. + ### Step 5 - Label communities Read `graphify-out/.graphify_analysis.json`. For each community key, look at its node labels and write a 2-5 word plain-language name (e.g. "Attention Mechanism", "Training Pipeline", "Data Loading"). diff --git a/graphify/skill-windows.md b/graphify/skill-windows.md index d631821ec..8ee54e72f 100644 --- a/graphify/skill-windows.md +++ b/graphify/skill-windows.md @@ -477,7 +477,7 @@ Replace INPUT_PATH with the actual path. ### Step 4.5 - Graph health check (read-only integrity gate) -A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, and self-loops — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. +A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, self-loops, schema errors, likely-reversed citation edges, and probable duplicate whole-file nodes (canonical-identity violations) — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. ```powershell @' @@ -496,11 +496,19 @@ flags = [f'{summary[k]} {label}' for k, label in ( ('undirected_same_endpoint_collapsed_edges', 'collapsed (undirected) edges'), ) if summary.get(k, 0)] print('GRAPH HEALTH WARNING: ' + '; '.join(flags) + ' - graph may be incomplete/corrupt.' if flags else 'Graph health: OK (no dangling/missing/collapsed edges).') +is_canonical = summary.get('canonical', True) +issues_text = '; '.join(summary.get('canonical_issues', [])) +canonical_line = 'CANONICAL: ' + str(is_canonical) +if not is_canonical: + canonical_line += ' - ' + issues_text +print(canonical_line) '@ | & (Get-Content graphify-out\.graphify_python) - ``` Substitute `IS_DIRECTED` and `INPUT_PATH` as in Step 4. If a `GRAPH HEALTH WARNING` prints, surface it in the final summary (do not abort — the graph is still usable, but the integrity issue must be visible, per the Honesty Rules). +**If `CANONICAL: False`:** the graph has a structural defect stronger than a collapse warning — a schema error, a dangling/missing edge endpoint, a references/cites edge that is very likely drawn backwards, or two nodes that are probably the same real file under two different ids (see `duplicate_node_candidates` in the report). Say so plainly in the final summary, and while it stays non-canonical: treat god-node/centrality rankings, community/clustering results, and dependency-direction claims as **informational only**, not settled fact — a node's inflated apparent importance may be an artifact of exactly this kind of split, not real architectural coupling. Do not silently present god-node degree or "X is the most central node" as fact against a non-canonical graph; name the specific defect instead. `duplicate_node_candidates_soft` (label-prefix matches without the id-suffix pattern) does not by itself flip `canonical` to False — treat it as a lead worth a glance, not a proven defect. + ### Step 5 - Label communities Read `graphify-out/.graphify_analysis.json`. For each community key, look at its node labels and write a 2-5 word plain-language name (e.g. "Attention Mechanism", "Training Pipeline", "Data Loading"). diff --git a/graphify/skill.md b/graphify/skill.md index abd2811d2..715aa76a7 100644 --- a/graphify/skill.md +++ b/graphify/skill.md @@ -455,7 +455,7 @@ Replace INPUT_PATH with the actual path. ### Step 4.5 - Graph health check (read-only integrity gate) -A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, and self-loops — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. +A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, self-loops, schema errors, likely-reversed citation edges, and probable duplicate whole-file nodes (canonical-identity violations) — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. ```bash $(cat graphify-out/.graphify_python) -c " @@ -474,11 +474,19 @@ flags = [f'{summary[k]} {label}' for k, label in ( ('undirected_same_endpoint_collapsed_edges', 'collapsed (undirected) edges'), ) if summary.get(k, 0)] print('GRAPH HEALTH WARNING: ' + '; '.join(flags) + ' - graph may be incomplete/corrupt.' if flags else 'Graph health: OK (no dangling/missing/collapsed edges).') +is_canonical = summary.get('canonical', True) +issues_text = '; '.join(summary.get('canonical_issues', [])) +canonical_line = 'CANONICAL: ' + str(is_canonical) +if not is_canonical: + canonical_line += ' - ' + issues_text +print(canonical_line) " ``` Substitute `IS_DIRECTED` and `INPUT_PATH` as in Step 4. If a `GRAPH HEALTH WARNING` prints, surface it in the final summary (do not abort — the graph is still usable, but the integrity issue must be visible, per the Honesty Rules). +**If `CANONICAL: False`:** the graph has a structural defect stronger than a collapse warning — a schema error, a dangling/missing edge endpoint, a references/cites edge that is very likely drawn backwards, or two nodes that are probably the same real file under two different ids (see `duplicate_node_candidates` in the report). Say so plainly in the final summary, and while it stays non-canonical: treat god-node/centrality rankings, community/clustering results, and dependency-direction claims as **informational only**, not settled fact — a node's inflated apparent importance may be an artifact of exactly this kind of split, not real architectural coupling. Do not silently present god-node degree or "X is the most central node" as fact against a non-canonical graph; name the specific defect instead. `duplicate_node_candidates_soft` (label-prefix matches without the id-suffix pattern) does not by itself flip `canonical` to False — treat it as a lead worth a glance, not a proven defect. + ### Step 5 - Label communities Read `graphify-out/.graphify_analysis.json`. For each community key, look at its node labels and write a 2-5 word plain-language name (e.g. "Attention Mechanism", "Training Pipeline", "Data Loading"). diff --git a/graphify/validate.py b/graphify/validate.py index bab3ddc7c..cddfb4402 100644 --- a/graphify/validate.py +++ b/graphify/validate.py @@ -5,6 +5,17 @@ VALID_CONFIDENCES = {"EXTRACTED", "INFERRED", "AMBIGUOUS"} REQUIRED_NODE_FIELDS = {"id", "label", "file_type", "source_file"} REQUIRED_EDGE_FIELDS = {"source", "target", "relation", "confidence", "source_file"} +# build.py's hyperedge handling (member normalization, G.graph["hyperedges"]) +# only ever reads id/label/nodes - relation/confidence/source_file are part of +# the LLM extraction contract (extraction-spec.md) but not load-bearing at +# build time, so they stay optional-but-validated-if-present below rather than +# required, matching what a hand-constructed or non-LLM-produced hyperedge +# (e.g. build_from_json's own alias-normalization tests) actually needs. +REQUIRED_HYPEREDGE_FIELDS = {"id", "label", "nodes"} +# extraction-spec.md: "if 3 or more nodes clearly participate together" - a +# hyperedge under 3 members is a plain edge that should have been modeled as +# one, not a schema-valid hyperedge. +MIN_HYPEREDGE_NODES = 3 def validate_extraction(data: dict) -> list[str]: @@ -84,6 +95,53 @@ def validate_extraction(data: dict) -> list[str]: if unmatched: errors.append(f"Edge {i} {endpoint} '{val}' does not match any node id") + # Hyperedges - optional (older extractions predate them), but a "hyperedges" + # key that IS present must be schema-valid; nothing checked this before. + if "hyperedges" in data: + hyperedge_list = data["hyperedges"] + if not isinstance(hyperedge_list, list): + errors.append("'hyperedges' must be a list") + else: + for i, hyperedge in enumerate(hyperedge_list): + if not isinstance(hyperedge, dict): + errors.append(f"Hyperedge {i} must be an object") + continue + for field in REQUIRED_HYPEREDGE_FIELDS: + if field not in hyperedge: + errors.append( + f"Hyperedge {i} (id={hyperedge.get('id', '?')!r}) " + f"missing required field '{field}'" + ) + if "confidence" in hyperedge and hyperedge["confidence"] not in VALID_CONFIDENCES: + errors.append( + f"Hyperedge {i} has invalid confidence '{hyperedge['confidence']}' " + f"- must be one of {sorted(VALID_CONFIDENCES)}" + ) + member_ids = hyperedge.get("nodes") + if member_ids is None: + continue + if not isinstance(member_ids, list): + errors.append(f"Hyperedge {i} 'nodes' must be a list") + continue + if len(member_ids) < MIN_HYPEREDGE_NODES: + errors.append( + f"Hyperedge {i} (id={hyperedge.get('id', '?')!r}) has " + f"{len(member_ids)} member node(s) - hyperedges require at least " + f"{MIN_HYPEREDGE_NODES} (a 2-node group is a plain edge)" + ) + for member in member_ids: + try: + unmatched = bool(node_ids) and member not in node_ids + except TypeError: + errors.append( + f"Hyperedge {i} member {member!r} is non-hashable - must be a string" + ) + continue + if unmatched: + errors.append( + f"Hyperedge {i} member '{member}' does not match any node id" + ) + return errors diff --git a/tests/test_graph_validation.py b/tests/test_graph_validation.py new file mode 100644 index 000000000..7aa5feaf9 --- /dev/null +++ b/tests/test_graph_validation.py @@ -0,0 +1,479 @@ +"""Tests for the post-build validation gate: canonical node identity, citation- +edge direction, hyperedge schema, and deterministic graph generation. + +This closes a real gap found while running /graphify on a 48-file docs corpus: +extraction independently produced `docs_architecture` and +`docs_architecture_document` for one file (ARCHITECTURE.md), inflating its +apparent "god node" degree and misattributing which document was doing the +citing. `diagnose_extraction`'s `canonical` verdict is meant to make that kind +of defect visible and machine-checkable instead of requiring a by-hand trace. +""" +from __future__ import annotations + +import json +from pathlib import Path + +import graphify.__main__ as mainmod +from graphify.build import build_from_json +from graphify.cluster import cluster +from graphify.diagnostics import diagnose_extraction, format_diagnostic_report +from graphify.export import to_json +from graphify.validate import validate_extraction + + +def _doc_node(node_id: str, label: str, source_file: str, file_type: str = "document") -> dict: + return {"id": node_id, "label": label, "file_type": file_type, "source_file": source_file} + + +def _edge(source: str, target: str, source_file: str, relation: str = "references") -> dict: + return { + "source": source, + "target": target, + "relation": relation, + "confidence": "EXTRACTED", + "source_file": source_file, + } + + +# --- hyperedge schema validation ------------------------------------------- + + +def test_validate_extraction_accepts_wellformed_hyperedge(): + data = { + "nodes": [ + _doc_node("a", "A", "a.md"), + _doc_node("b", "B", "b.md"), + _doc_node("c", "C", "c.md"), + ], + "edges": [], + "hyperedges": [ + { + "id": "grp", + "label": "Group", + "nodes": ["a", "b", "c"], + "relation": "participate_in", + "confidence": "INFERRED", + "confidence_score": 0.75, + "source_file": "a.md", + } + ], + } + assert validate_extraction(data) == [] + + +def test_validate_extraction_rejects_hyperedge_missing_fields(): + data = { + "nodes": [_doc_node("a", "A", "a.md")], + "edges": [], + "hyperedges": [{"id": "grp", "nodes": ["a"]}], + } + errors = validate_extraction(data) + assert any("missing required field" in e for e in errors) + + +def test_validate_extraction_rejects_hyperedge_under_three_nodes(): + data = { + "nodes": [_doc_node("a", "A", "a.md"), _doc_node("b", "B", "b.md")], + "edges": [], + "hyperedges": [ + { + "id": "grp", + "label": "Group", + "nodes": ["a", "b"], + "relation": "participate_in", + "confidence": "EXTRACTED", + "source_file": "a.md", + } + ], + } + errors = validate_extraction(data) + assert any("at least 3" in e for e in errors) + + +def test_validate_extraction_rejects_hyperedge_dangling_member(): + data = { + "nodes": [_doc_node("a", "A", "a.md"), _doc_node("b", "B", "b.md")], + "edges": [], + "hyperedges": [ + { + "id": "grp", + "label": "Group", + "nodes": ["a", "b", "ghost"], + "relation": "participate_in", + "confidence": "EXTRACTED", + "source_file": "a.md", + } + ], + } + errors = validate_extraction(data) + assert any("does not match any node id" in e for e in errors) + + +def test_validate_extraction_still_passes_without_hyperedges_key(): + """Older extractions that predate hyperedges must still validate clean.""" + data = { + "nodes": [_doc_node("a", "A", "a.md")], + "edges": [], + } + assert validate_extraction(data) == [] + + +# --- canonical node identity (duplicate whole-file nodes) ------------------- + + +def test_diagnose_flags_document_suffix_split_as_hard_duplicate(): + """Direct regression for the ARCHITECTURE.md split found in the wild.""" + extraction = { + "nodes": [ + _doc_node("docs_architecture", "ARCHITECTURE.md", "ARCHITECTURE.md"), + _doc_node( + "docs_architecture_document", + "ARCHITECTURE.md - Avenoria Technical Architecture", + "adr/001-example.md", + ), + ], + "edges": [], + "hyperedges": [], + } + summary = diagnose_extraction(extraction, directed=False) + assert summary["canonical"] is False + assert summary["duplicate_node_candidates"] == [ + { + "node_a": "docs_architecture", + "node_b": "docs_architecture_document", + "reason": "id suffix '_document'", + } + ] + assert any("duplicate whole-file" in issue for issue in summary["canonical_issues"]) + + +def test_diagnose_flags_label_prefix_as_soft_duplicate_only(): + """A label-prefix match without the id-suffix pattern is informational + (soft) and must not, on its own, flip canonical to False - it is a + plausible signal, not proof (e.g. it could legitimately be two distinct, + unrelated files whose titles happen to share a prefix).""" + extraction = { + "nodes": [ + _doc_node("docs_api_v1", "API.md", "api-v1/API.md"), + _doc_node("docs_api_v2", "API.md - v2 addendum", "api-v2/API.md"), + ], + "edges": [], + "hyperedges": [], + } + summary = diagnose_extraction(extraction, directed=False) + assert summary["duplicate_node_candidates"] == [] + assert len(summary["duplicate_node_candidates_soft"]) == 1 + assert summary["canonical"] is True + + +def test_diagnose_does_not_flag_heading_node_of_same_file_as_duplicate(): + """A heading node deliberately shares source_file with its parent + whole-file node and often repeats/extends its label - this must never be + mistaken for a cross-file duplicate.""" + extraction = { + "nodes": [ + _doc_node("docs_readme", "README.md", "README.md"), + _doc_node("docs_readme_overview", "README.md Overview", "README.md"), + ], + "edges": [], + "hyperedges": [], + } + summary = diagnose_extraction(extraction, directed=False) + assert summary["duplicate_node_candidates"] == [] + assert summary["duplicate_node_candidates_soft"] == [] + assert summary["canonical"] is True + + +def test_diagnose_does_not_flag_unrelated_documents(): + extraction = { + "nodes": [ + _doc_node("docs_api", "API.md", "API.md"), + _doc_node("docs_database", "DATABASE.md", "DATABASE.md"), + ], + "edges": [], + "hyperedges": [], + } + summary = diagnose_extraction(extraction, directed=False) + assert summary["duplicate_node_candidates"] == [] + assert summary["duplicate_node_candidates_soft"] == [] + assert summary["canonical"] is True + + +# --- citation edge direction ------------------------------------------- + + +def test_diagnose_flags_reversed_citation_edge(): + """The edge's own source_file matches the TARGET's file, not the + SOURCE's - the extraction-spec.md self-check for a reversed references/ + cites edge between two whole-file document nodes.""" + extraction = { + "nodes": [ + _doc_node("docs_architecture_document", "ARCHITECTURE.md - long form", "adr/001.md"), + _doc_node("docs_adr_012", "ADR-012", "adr/012.md"), + ], + "edges": [_edge("docs_architecture_document", "docs_adr_012", "adr/012.md")], + "hyperedges": [], + } + summary = diagnose_extraction(extraction, directed=False) + assert summary["canonical"] is False + assert len(summary["edge_direction_suspects"]) == 1 + suspect = summary["edge_direction_suspects"][0] + assert suspect["source"] == "docs_architecture_document" + assert suspect["target"] == "docs_adr_012" + + +def test_diagnose_does_not_flag_correctly_directed_citation_edge(): + extraction = { + "nodes": [ + _doc_node("docs_adr_012", "ADR-012", "adr/012.md"), + _doc_node("docs_architecture", "ARCHITECTURE.md", "ARCHITECTURE.md"), + ], + "edges": [_edge("docs_adr_012", "docs_architecture", "adr/012.md")], + "hyperedges": [], + } + summary = diagnose_extraction(extraction, directed=False) + assert summary["edge_direction_suspects"] == [] + assert summary["canonical"] is True + + +def test_diagnose_ignores_direction_for_non_citation_relations(): + """The direction heuristic is scoped to references/cites (the relations + extraction-spec.md gives an explicit citer->citee rule for) - it must not + misfire on `calls`, whose direction semantics are already covered by a + different, existing spec rule and are checked elsewhere.""" + extraction = { + "nodes": [ + _doc_node("docs_a", "A", "a.md", file_type="document"), + _doc_node("docs_b", "B", "b.md", file_type="document"), + ], + "edges": [_edge("docs_a", "docs_b", "b.md", relation="calls")], + "hyperedges": [], + } + summary = diagnose_extraction(extraction, directed=False) + assert summary["edge_direction_suspects"] == [] + + +def test_diagnose_ignores_direction_when_endpoint_is_not_whole_file_typed(): + """Concept/rationale nodes don't carry the same "this node IS a file" + semantics a document/paper node does, so the heuristic must not apply to + them - only whole-file <-> whole-file citation edges are in scope.""" + extraction = { + "nodes": [ + _doc_node("docs_a", "A", "a.md", file_type="document"), + _doc_node("docs_a_concept", "A Concept", "b.md", file_type="concept"), + ], + "edges": [_edge("docs_a", "docs_a_concept", "b.md")], + "hyperedges": [], + } + summary = diagnose_extraction(extraction, directed=False) + assert summary["edge_direction_suspects"] == [] + + +# --- overall canonical verdict + report formatting -------------------------- + + +def test_canonical_true_for_clean_extraction(): + extraction = { + "nodes": [ + _doc_node("docs_adr_012", "ADR-012", "adr/012.md"), + _doc_node("docs_architecture", "ARCHITECTURE.md", "ARCHITECTURE.md"), + ], + "edges": [_edge("docs_adr_012", "docs_architecture", "adr/012.md")], + "hyperedges": [], + } + summary = diagnose_extraction(extraction, directed=False) + assert summary["canonical"] is True + assert summary["canonical_issues"] == [] + + +def test_canonical_false_for_dangling_edge_endpoint(): + """Pre-existing dangling-endpoint detection now also gates `canonical`, + not just the edge-collapse counters it always fed.""" + extraction = { + "nodes": [_doc_node("docs_a", "A", "a.md")], + "edges": [_edge("docs_a", "ghost", "a.md")], + "hyperedges": [], + } + summary = diagnose_extraction(extraction, directed=False) + assert summary["canonical"] is False + assert any("dangling" in issue for issue in summary["canonical_issues"]) + + +def test_canonical_false_for_schema_error(): + extraction = { + "nodes": [{"id": "a", "label": "A", "file_type": "not-a-real-type", "source_file": "a.md"}], + "edges": [], + "hyperedges": [], + } + summary = diagnose_extraction(extraction, directed=False) + assert summary["canonical"] is False + assert summary["schema_errors"] + assert any("schema error" in issue for issue in summary["canonical_issues"]) + + +def test_format_diagnostic_report_prints_noncanonical_verdict_banner(): + extraction = { + "nodes": [ + _doc_node("docs_architecture", "ARCHITECTURE.md", "ARCHITECTURE.md"), + _doc_node( + "docs_architecture_document", + "ARCHITECTURE.md - long form", + "adr/001.md", + ), + ], + "edges": [], + "hyperedges": [], + } + summary = diagnose_extraction(extraction, directed=False) + report = format_diagnostic_report(summary) + assert "verdict: NON-CANONICAL" in report + assert "informational only" in report + assert "docs_architecture <-> docs_architecture_document" in report + + +def test_format_diagnostic_report_prints_canonical_verdict_banner(): + extraction = { + "nodes": [_doc_node("docs_a", "A", "a.md")], + "edges": [], + "hyperedges": [], + } + summary = diagnose_extraction(extraction, directed=False) + report = format_diagnostic_report(summary) + assert "verdict: CANONICAL" in report + assert "NON-CANONICAL" not in report + + +# --- CLI gate ---------------------------------------------------------------- + + +def test_diagnose_multigraph_cli_fail_on_noncanonical_exits_nonzero( + monkeypatch, tmp_path: Path, capsys +) -> None: + graph_path = tmp_path / "graph.json" + payload = { + "nodes": [ + _doc_node("docs_architecture", "ARCHITECTURE.md", "ARCHITECTURE.md"), + _doc_node( + "docs_architecture_document", "ARCHITECTURE.md - long form", "adr/001.md" + ), + ], + "edges": [], + } + graph_path.write_text(json.dumps(payload), encoding="utf-8") + monkeypatch.setattr(mainmod, "_check_skill_version", lambda _: None) + monkeypatch.setattr( + mainmod.sys, + "argv", + [ + "graphify", "diagnose", "multigraph", "--graph", str(graph_path), + "--fail-on-noncanonical", + ], + ) + + try: + mainmod.main() + exited = False + code = 0 + except SystemExit as exc: + exited = True + code = exc.code + + assert exited is True + assert code == 1 + assert "verdict: NON-CANONICAL" in capsys.readouterr().out + + +def test_diagnose_multigraph_cli_noncanonical_still_exits_zero_by_default( + monkeypatch, tmp_path: Path, capsys +) -> None: + """Backward compatibility: existing scripts/CI calling this command without + the new flag must keep getting exit 0, even on a non-canonical graph.""" + graph_path = tmp_path / "graph.json" + payload = { + "nodes": [ + _doc_node("docs_architecture", "ARCHITECTURE.md", "ARCHITECTURE.md"), + _doc_node( + "docs_architecture_document", "ARCHITECTURE.md - long form", "adr/001.md" + ), + ], + "edges": [], + } + graph_path.write_text(json.dumps(payload), encoding="utf-8") + monkeypatch.setattr(mainmod, "_check_skill_version", lambda _: None) + monkeypatch.setattr( + mainmod.sys, + "argv", + ["graphify", "diagnose", "multigraph", "--graph", str(graph_path)], + ) + + mainmod.main() # must not raise SystemExit + + assert "verdict: NON-CANONICAL" in capsys.readouterr().out + + +# --- deterministic graph generation ----------------------------------------- + + +def _sample_extraction() -> dict: + return { + "nodes": [ + _doc_node("docs_a", "A.md", "A.md"), + _doc_node("docs_b", "B.md", "B.md"), + _doc_node("docs_c", "C.md", "C.md"), + _doc_node("docs_d", "D.md", "D.md"), + { + "id": "docs_a_concept_one", "label": "Concept One", "file_type": "concept", + "source_file": "A.md", + }, + { + "id": "docs_b_concept_two", "label": "Concept Two", "file_type": "concept", + "source_file": "B.md", + }, + ], + "edges": [ + _edge("docs_a", "docs_b", "A.md"), + _edge("docs_b", "docs_c", "B.md"), + _edge("docs_c", "docs_d", "C.md"), + _edge("docs_a", "docs_c", "A.md", relation="cites"), + { + "source": "docs_a", "target": "docs_a_concept_one", "relation": "references", + "confidence": "EXTRACTED", "source_file": "A.md", + }, + { + "source": "docs_b", "target": "docs_b_concept_two", "relation": "references", + "confidence": "EXTRACTED", "source_file": "B.md", + }, + { + "source": "docs_a_concept_one", "target": "docs_b_concept_two", + "relation": "semantically_similar_to", "confidence": "INFERRED", + "confidence_score": 0.75, "source_file": "A.md", + }, + ], + "hyperedges": [], + } + + +def test_build_and_cluster_are_deterministic_across_repeated_runs(tmp_path: Path): + """Same extraction JSON -> identical graph.json, run twice from scratch. + + Covers the property the "same repository -> identical graph" ask is really + about: node/edge sets, and community assignment (Louvain is seeded, but + that guarantee had no end-to-end regression test locking it in).""" + import copy + + outputs = [] + for i in range(2): + extraction = copy.deepcopy(_sample_extraction()) + graph = build_from_json(extraction, root=".", directed=False) + communities = cluster(graph) + out_path = tmp_path / f"graph_{i}.json" + wrote = to_json(graph, communities, str(out_path), force=True, built_at_commit="test") + assert wrote is True + outputs.append(json.loads(out_path.read_text(encoding="utf-8"))) + + def _strip_volatile(data: dict) -> dict: + # built_at_commit is pinned above; nothing else should vary, but keep + # this explicit so a future volatile field doesn't silently mask drift. + return {k: v for k, v in data.items() if k != "built_at_commit"} + + assert _strip_volatile(outputs[0]) == _strip_volatile(outputs[1]) diff --git a/tools/skillgen/expected/graphify__skill-agents.md b/tools/skillgen/expected/graphify__skill-agents.md index 190827d9a..099fd00cd 100644 --- a/tools/skillgen/expected/graphify__skill-agents.md +++ b/tools/skillgen/expected/graphify__skill-agents.md @@ -452,7 +452,7 @@ Replace INPUT_PATH with the actual path. ### Step 4.5 - Graph health check (read-only integrity gate) -A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, and self-loops — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. +A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, self-loops, schema errors, likely-reversed citation edges, and probable duplicate whole-file nodes (canonical-identity violations) — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. ```bash $(cat graphify-out/.graphify_python) -c " @@ -471,11 +471,19 @@ flags = [f'{summary[k]} {label}' for k, label in ( ('undirected_same_endpoint_collapsed_edges', 'collapsed (undirected) edges'), ) if summary.get(k, 0)] print('GRAPH HEALTH WARNING: ' + '; '.join(flags) + ' - graph may be incomplete/corrupt.' if flags else 'Graph health: OK (no dangling/missing/collapsed edges).') +is_canonical = summary.get('canonical', True) +issues_text = '; '.join(summary.get('canonical_issues', [])) +canonical_line = 'CANONICAL: ' + str(is_canonical) +if not is_canonical: + canonical_line += ' - ' + issues_text +print(canonical_line) " ``` Substitute `IS_DIRECTED` and `INPUT_PATH` as in Step 4. If a `GRAPH HEALTH WARNING` prints, surface it in the final summary (do not abort — the graph is still usable, but the integrity issue must be visible, per the Honesty Rules). +**If `CANONICAL: False`:** the graph has a structural defect stronger than a collapse warning — a schema error, a dangling/missing edge endpoint, a references/cites edge that is very likely drawn backwards, or two nodes that are probably the same real file under two different ids (see `duplicate_node_candidates` in the report). Say so plainly in the final summary, and while it stays non-canonical: treat god-node/centrality rankings, community/clustering results, and dependency-direction claims as **informational only**, not settled fact — a node's inflated apparent importance may be an artifact of exactly this kind of split, not real architectural coupling. Do not silently present god-node degree or "X is the most central node" as fact against a non-canonical graph; name the specific defect instead. `duplicate_node_candidates_soft` (label-prefix matches without the id-suffix pattern) does not by itself flip `canonical` to False — treat it as a lead worth a glance, not a proven defect. + ### Step 5 - Label communities Read `graphify-out/.graphify_analysis.json`. For each community key, look at its node labels and write a 2-5 word plain-language name (e.g. "Attention Mechanism", "Training Pipeline", "Data Loading"). diff --git a/tools/skillgen/expected/graphify__skill-amp.md b/tools/skillgen/expected/graphify__skill-amp.md index 190827d9a..099fd00cd 100644 --- a/tools/skillgen/expected/graphify__skill-amp.md +++ b/tools/skillgen/expected/graphify__skill-amp.md @@ -452,7 +452,7 @@ Replace INPUT_PATH with the actual path. ### Step 4.5 - Graph health check (read-only integrity gate) -A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, and self-loops — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. +A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, self-loops, schema errors, likely-reversed citation edges, and probable duplicate whole-file nodes (canonical-identity violations) — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. ```bash $(cat graphify-out/.graphify_python) -c " @@ -471,11 +471,19 @@ flags = [f'{summary[k]} {label}' for k, label in ( ('undirected_same_endpoint_collapsed_edges', 'collapsed (undirected) edges'), ) if summary.get(k, 0)] print('GRAPH HEALTH WARNING: ' + '; '.join(flags) + ' - graph may be incomplete/corrupt.' if flags else 'Graph health: OK (no dangling/missing/collapsed edges).') +is_canonical = summary.get('canonical', True) +issues_text = '; '.join(summary.get('canonical_issues', [])) +canonical_line = 'CANONICAL: ' + str(is_canonical) +if not is_canonical: + canonical_line += ' - ' + issues_text +print(canonical_line) " ``` Substitute `IS_DIRECTED` and `INPUT_PATH` as in Step 4. If a `GRAPH HEALTH WARNING` prints, surface it in the final summary (do not abort — the graph is still usable, but the integrity issue must be visible, per the Honesty Rules). +**If `CANONICAL: False`:** the graph has a structural defect stronger than a collapse warning — a schema error, a dangling/missing edge endpoint, a references/cites edge that is very likely drawn backwards, or two nodes that are probably the same real file under two different ids (see `duplicate_node_candidates` in the report). Say so plainly in the final summary, and while it stays non-canonical: treat god-node/centrality rankings, community/clustering results, and dependency-direction claims as **informational only**, not settled fact — a node's inflated apparent importance may be an artifact of exactly this kind of split, not real architectural coupling. Do not silently present god-node degree or "X is the most central node" as fact against a non-canonical graph; name the specific defect instead. `duplicate_node_candidates_soft` (label-prefix matches without the id-suffix pattern) does not by itself flip `canonical` to False — treat it as a lead worth a glance, not a proven defect. + ### Step 5 - Label communities Read `graphify-out/.graphify_analysis.json`. For each community key, look at its node labels and write a 2-5 word plain-language name (e.g. "Attention Mechanism", "Training Pipeline", "Data Loading"). diff --git a/tools/skillgen/expected/graphify__skill-claw.md b/tools/skillgen/expected/graphify__skill-claw.md index abd2811d2..715aa76a7 100644 --- a/tools/skillgen/expected/graphify__skill-claw.md +++ b/tools/skillgen/expected/graphify__skill-claw.md @@ -455,7 +455,7 @@ Replace INPUT_PATH with the actual path. ### Step 4.5 - Graph health check (read-only integrity gate) -A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, and self-loops — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. +A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, self-loops, schema errors, likely-reversed citation edges, and probable duplicate whole-file nodes (canonical-identity violations) — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. ```bash $(cat graphify-out/.graphify_python) -c " @@ -474,11 +474,19 @@ flags = [f'{summary[k]} {label}' for k, label in ( ('undirected_same_endpoint_collapsed_edges', 'collapsed (undirected) edges'), ) if summary.get(k, 0)] print('GRAPH HEALTH WARNING: ' + '; '.join(flags) + ' - graph may be incomplete/corrupt.' if flags else 'Graph health: OK (no dangling/missing/collapsed edges).') +is_canonical = summary.get('canonical', True) +issues_text = '; '.join(summary.get('canonical_issues', [])) +canonical_line = 'CANONICAL: ' + str(is_canonical) +if not is_canonical: + canonical_line += ' - ' + issues_text +print(canonical_line) " ``` Substitute `IS_DIRECTED` and `INPUT_PATH` as in Step 4. If a `GRAPH HEALTH WARNING` prints, surface it in the final summary (do not abort — the graph is still usable, but the integrity issue must be visible, per the Honesty Rules). +**If `CANONICAL: False`:** the graph has a structural defect stronger than a collapse warning — a schema error, a dangling/missing edge endpoint, a references/cites edge that is very likely drawn backwards, or two nodes that are probably the same real file under two different ids (see `duplicate_node_candidates` in the report). Say so plainly in the final summary, and while it stays non-canonical: treat god-node/centrality rankings, community/clustering results, and dependency-direction claims as **informational only**, not settled fact — a node's inflated apparent importance may be an artifact of exactly this kind of split, not real architectural coupling. Do not silently present god-node degree or "X is the most central node" as fact against a non-canonical graph; name the specific defect instead. `duplicate_node_candidates_soft` (label-prefix matches without the id-suffix pattern) does not by itself flip `canonical` to False — treat it as a lead worth a glance, not a proven defect. + ### Step 5 - Label communities Read `graphify-out/.graphify_analysis.json`. For each community key, look at its node labels and write a 2-5 word plain-language name (e.g. "Attention Mechanism", "Training Pipeline", "Data Loading"). diff --git a/tools/skillgen/expected/graphify__skill-codex.md b/tools/skillgen/expected/graphify__skill-codex.md index af3f723c7..16d694abc 100644 --- a/tools/skillgen/expected/graphify__skill-codex.md +++ b/tools/skillgen/expected/graphify__skill-codex.md @@ -452,7 +452,7 @@ Replace INPUT_PATH with the actual path. ### Step 4.5 - Graph health check (read-only integrity gate) -A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, and self-loops — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. +A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, self-loops, schema errors, likely-reversed citation edges, and probable duplicate whole-file nodes (canonical-identity violations) — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. ```bash $(cat graphify-out/.graphify_python) -c " @@ -471,11 +471,19 @@ flags = [f'{summary[k]} {label}' for k, label in ( ('undirected_same_endpoint_collapsed_edges', 'collapsed (undirected) edges'), ) if summary.get(k, 0)] print('GRAPH HEALTH WARNING: ' + '; '.join(flags) + ' - graph may be incomplete/corrupt.' if flags else 'Graph health: OK (no dangling/missing/collapsed edges).') +is_canonical = summary.get('canonical', True) +issues_text = '; '.join(summary.get('canonical_issues', [])) +canonical_line = 'CANONICAL: ' + str(is_canonical) +if not is_canonical: + canonical_line += ' - ' + issues_text +print(canonical_line) " ``` Substitute `IS_DIRECTED` and `INPUT_PATH` as in Step 4. If a `GRAPH HEALTH WARNING` prints, surface it in the final summary (do not abort — the graph is still usable, but the integrity issue must be visible, per the Honesty Rules). +**If `CANONICAL: False`:** the graph has a structural defect stronger than a collapse warning — a schema error, a dangling/missing edge endpoint, a references/cites edge that is very likely drawn backwards, or two nodes that are probably the same real file under two different ids (see `duplicate_node_candidates` in the report). Say so plainly in the final summary, and while it stays non-canonical: treat god-node/centrality rankings, community/clustering results, and dependency-direction claims as **informational only**, not settled fact — a node's inflated apparent importance may be an artifact of exactly this kind of split, not real architectural coupling. Do not silently present god-node degree or "X is the most central node" as fact against a non-canonical graph; name the specific defect instead. `duplicate_node_candidates_soft` (label-prefix matches without the id-suffix pattern) does not by itself flip `canonical` to False — treat it as a lead worth a glance, not a proven defect. + ### Step 5 - Label communities Read `graphify-out/.graphify_analysis.json`. For each community key, look at its node labels and write a 2-5 word plain-language name (e.g. "Attention Mechanism", "Training Pipeline", "Data Loading"). diff --git a/tools/skillgen/expected/graphify__skill-copilot.md b/tools/skillgen/expected/graphify__skill-copilot.md index abd2811d2..715aa76a7 100644 --- a/tools/skillgen/expected/graphify__skill-copilot.md +++ b/tools/skillgen/expected/graphify__skill-copilot.md @@ -455,7 +455,7 @@ Replace INPUT_PATH with the actual path. ### Step 4.5 - Graph health check (read-only integrity gate) -A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, and self-loops — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. +A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, self-loops, schema errors, likely-reversed citation edges, and probable duplicate whole-file nodes (canonical-identity violations) — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. ```bash $(cat graphify-out/.graphify_python) -c " @@ -474,11 +474,19 @@ flags = [f'{summary[k]} {label}' for k, label in ( ('undirected_same_endpoint_collapsed_edges', 'collapsed (undirected) edges'), ) if summary.get(k, 0)] print('GRAPH HEALTH WARNING: ' + '; '.join(flags) + ' - graph may be incomplete/corrupt.' if flags else 'Graph health: OK (no dangling/missing/collapsed edges).') +is_canonical = summary.get('canonical', True) +issues_text = '; '.join(summary.get('canonical_issues', [])) +canonical_line = 'CANONICAL: ' + str(is_canonical) +if not is_canonical: + canonical_line += ' - ' + issues_text +print(canonical_line) " ``` Substitute `IS_DIRECTED` and `INPUT_PATH` as in Step 4. If a `GRAPH HEALTH WARNING` prints, surface it in the final summary (do not abort — the graph is still usable, but the integrity issue must be visible, per the Honesty Rules). +**If `CANONICAL: False`:** the graph has a structural defect stronger than a collapse warning — a schema error, a dangling/missing edge endpoint, a references/cites edge that is very likely drawn backwards, or two nodes that are probably the same real file under two different ids (see `duplicate_node_candidates` in the report). Say so plainly in the final summary, and while it stays non-canonical: treat god-node/centrality rankings, community/clustering results, and dependency-direction claims as **informational only**, not settled fact — a node's inflated apparent importance may be an artifact of exactly this kind of split, not real architectural coupling. Do not silently present god-node degree or "X is the most central node" as fact against a non-canonical graph; name the specific defect instead. `duplicate_node_candidates_soft` (label-prefix matches without the id-suffix pattern) does not by itself flip `canonical` to False — treat it as a lead worth a glance, not a proven defect. + ### Step 5 - Label communities Read `graphify-out/.graphify_analysis.json`. For each community key, look at its node labels and write a 2-5 word plain-language name (e.g. "Attention Mechanism", "Training Pipeline", "Data Loading"). diff --git a/tools/skillgen/expected/graphify__skill-droid.md b/tools/skillgen/expected/graphify__skill-droid.md index fd148d485..fd5e3796e 100644 --- a/tools/skillgen/expected/graphify__skill-droid.md +++ b/tools/skillgen/expected/graphify__skill-droid.md @@ -452,7 +452,7 @@ Replace INPUT_PATH with the actual path. ### Step 4.5 - Graph health check (read-only integrity gate) -A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, and self-loops — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. +A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, self-loops, schema errors, likely-reversed citation edges, and probable duplicate whole-file nodes (canonical-identity violations) — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. ```bash $(cat graphify-out/.graphify_python) -c " @@ -471,11 +471,19 @@ flags = [f'{summary[k]} {label}' for k, label in ( ('undirected_same_endpoint_collapsed_edges', 'collapsed (undirected) edges'), ) if summary.get(k, 0)] print('GRAPH HEALTH WARNING: ' + '; '.join(flags) + ' - graph may be incomplete/corrupt.' if flags else 'Graph health: OK (no dangling/missing/collapsed edges).') +is_canonical = summary.get('canonical', True) +issues_text = '; '.join(summary.get('canonical_issues', [])) +canonical_line = 'CANONICAL: ' + str(is_canonical) +if not is_canonical: + canonical_line += ' - ' + issues_text +print(canonical_line) " ``` Substitute `IS_DIRECTED` and `INPUT_PATH` as in Step 4. If a `GRAPH HEALTH WARNING` prints, surface it in the final summary (do not abort — the graph is still usable, but the integrity issue must be visible, per the Honesty Rules). +**If `CANONICAL: False`:** the graph has a structural defect stronger than a collapse warning — a schema error, a dangling/missing edge endpoint, a references/cites edge that is very likely drawn backwards, or two nodes that are probably the same real file under two different ids (see `duplicate_node_candidates` in the report). Say so plainly in the final summary, and while it stays non-canonical: treat god-node/centrality rankings, community/clustering results, and dependency-direction claims as **informational only**, not settled fact — a node's inflated apparent importance may be an artifact of exactly this kind of split, not real architectural coupling. Do not silently present god-node degree or "X is the most central node" as fact against a non-canonical graph; name the specific defect instead. `duplicate_node_candidates_soft` (label-prefix matches without the id-suffix pattern) does not by itself flip `canonical` to False — treat it as a lead worth a glance, not a proven defect. + ### Step 5 - Label communities Read `graphify-out/.graphify_analysis.json`. For each community key, look at its node labels and write a 2-5 word plain-language name (e.g. "Attention Mechanism", "Training Pipeline", "Data Loading"). diff --git a/tools/skillgen/expected/graphify__skill-kilo.md b/tools/skillgen/expected/graphify__skill-kilo.md index 3e70b050a..6925ca543 100644 --- a/tools/skillgen/expected/graphify__skill-kilo.md +++ b/tools/skillgen/expected/graphify__skill-kilo.md @@ -455,7 +455,7 @@ Replace INPUT_PATH with the actual path. ### Step 4.5 - Graph health check (read-only integrity gate) -A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, and self-loops — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. +A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, self-loops, schema errors, likely-reversed citation edges, and probable duplicate whole-file nodes (canonical-identity violations) — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. ```bash $(cat graphify-out/.graphify_python) -c " @@ -474,11 +474,19 @@ flags = [f'{summary[k]} {label}' for k, label in ( ('undirected_same_endpoint_collapsed_edges', 'collapsed (undirected) edges'), ) if summary.get(k, 0)] print('GRAPH HEALTH WARNING: ' + '; '.join(flags) + ' - graph may be incomplete/corrupt.' if flags else 'Graph health: OK (no dangling/missing/collapsed edges).') +is_canonical = summary.get('canonical', True) +issues_text = '; '.join(summary.get('canonical_issues', [])) +canonical_line = 'CANONICAL: ' + str(is_canonical) +if not is_canonical: + canonical_line += ' - ' + issues_text +print(canonical_line) " ``` Substitute `IS_DIRECTED` and `INPUT_PATH` as in Step 4. If a `GRAPH HEALTH WARNING` prints, surface it in the final summary (do not abort — the graph is still usable, but the integrity issue must be visible, per the Honesty Rules). +**If `CANONICAL: False`:** the graph has a structural defect stronger than a collapse warning — a schema error, a dangling/missing edge endpoint, a references/cites edge that is very likely drawn backwards, or two nodes that are probably the same real file under two different ids (see `duplicate_node_candidates` in the report). Say so plainly in the final summary, and while it stays non-canonical: treat god-node/centrality rankings, community/clustering results, and dependency-direction claims as **informational only**, not settled fact — a node's inflated apparent importance may be an artifact of exactly this kind of split, not real architectural coupling. Do not silently present god-node degree or "X is the most central node" as fact against a non-canonical graph; name the specific defect instead. `duplicate_node_candidates_soft` (label-prefix matches without the id-suffix pattern) does not by itself flip `canonical` to False — treat it as a lead worth a glance, not a proven defect. + ### Step 5 - Label communities Read `graphify-out/.graphify_analysis.json`. For each community key, look at its node labels and write a 2-5 word plain-language name (e.g. "Attention Mechanism", "Training Pipeline", "Data Loading"). diff --git a/tools/skillgen/expected/graphify__skill-kiro.md b/tools/skillgen/expected/graphify__skill-kiro.md index abd2811d2..715aa76a7 100644 --- a/tools/skillgen/expected/graphify__skill-kiro.md +++ b/tools/skillgen/expected/graphify__skill-kiro.md @@ -455,7 +455,7 @@ Replace INPUT_PATH with the actual path. ### Step 4.5 - Graph health check (read-only integrity gate) -A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, and self-loops — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. +A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, self-loops, schema errors, likely-reversed citation edges, and probable duplicate whole-file nodes (canonical-identity violations) — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. ```bash $(cat graphify-out/.graphify_python) -c " @@ -474,11 +474,19 @@ flags = [f'{summary[k]} {label}' for k, label in ( ('undirected_same_endpoint_collapsed_edges', 'collapsed (undirected) edges'), ) if summary.get(k, 0)] print('GRAPH HEALTH WARNING: ' + '; '.join(flags) + ' - graph may be incomplete/corrupt.' if flags else 'Graph health: OK (no dangling/missing/collapsed edges).') +is_canonical = summary.get('canonical', True) +issues_text = '; '.join(summary.get('canonical_issues', [])) +canonical_line = 'CANONICAL: ' + str(is_canonical) +if not is_canonical: + canonical_line += ' - ' + issues_text +print(canonical_line) " ``` Substitute `IS_DIRECTED` and `INPUT_PATH` as in Step 4. If a `GRAPH HEALTH WARNING` prints, surface it in the final summary (do not abort — the graph is still usable, but the integrity issue must be visible, per the Honesty Rules). +**If `CANONICAL: False`:** the graph has a structural defect stronger than a collapse warning — a schema error, a dangling/missing edge endpoint, a references/cites edge that is very likely drawn backwards, or two nodes that are probably the same real file under two different ids (see `duplicate_node_candidates` in the report). Say so plainly in the final summary, and while it stays non-canonical: treat god-node/centrality rankings, community/clustering results, and dependency-direction claims as **informational only**, not settled fact — a node's inflated apparent importance may be an artifact of exactly this kind of split, not real architectural coupling. Do not silently present god-node degree or "X is the most central node" as fact against a non-canonical graph; name the specific defect instead. `duplicate_node_candidates_soft` (label-prefix matches without the id-suffix pattern) does not by itself flip `canonical` to False — treat it as a lead worth a glance, not a proven defect. + ### Step 5 - Label communities Read `graphify-out/.graphify_analysis.json`. For each community key, look at its node labels and write a 2-5 word plain-language name (e.g. "Attention Mechanism", "Training Pipeline", "Data Loading"). diff --git a/tools/skillgen/expected/graphify__skill-opencode.md b/tools/skillgen/expected/graphify__skill-opencode.md index 91ced6067..7c8a0a39a 100644 --- a/tools/skillgen/expected/graphify__skill-opencode.md +++ b/tools/skillgen/expected/graphify__skill-opencode.md @@ -447,7 +447,7 @@ Replace INPUT_PATH with the actual path. ### Step 4.5 - Graph health check (read-only integrity gate) -A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, and self-loops — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. +A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, self-loops, schema errors, likely-reversed citation edges, and probable duplicate whole-file nodes (canonical-identity violations) — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. ```bash $(cat graphify-out/.graphify_python) -c " @@ -466,11 +466,19 @@ flags = [f'{summary[k]} {label}' for k, label in ( ('undirected_same_endpoint_collapsed_edges', 'collapsed (undirected) edges'), ) if summary.get(k, 0)] print('GRAPH HEALTH WARNING: ' + '; '.join(flags) + ' - graph may be incomplete/corrupt.' if flags else 'Graph health: OK (no dangling/missing/collapsed edges).') +is_canonical = summary.get('canonical', True) +issues_text = '; '.join(summary.get('canonical_issues', [])) +canonical_line = 'CANONICAL: ' + str(is_canonical) +if not is_canonical: + canonical_line += ' - ' + issues_text +print(canonical_line) " ``` Substitute `IS_DIRECTED` and `INPUT_PATH` as in Step 4. If a `GRAPH HEALTH WARNING` prints, surface it in the final summary (do not abort — the graph is still usable, but the integrity issue must be visible, per the Honesty Rules). +**If `CANONICAL: False`:** the graph has a structural defect stronger than a collapse warning — a schema error, a dangling/missing edge endpoint, a references/cites edge that is very likely drawn backwards, or two nodes that are probably the same real file under two different ids (see `duplicate_node_candidates` in the report). Say so plainly in the final summary, and while it stays non-canonical: treat god-node/centrality rankings, community/clustering results, and dependency-direction claims as **informational only**, not settled fact — a node's inflated apparent importance may be an artifact of exactly this kind of split, not real architectural coupling. Do not silently present god-node degree or "X is the most central node" as fact against a non-canonical graph; name the specific defect instead. `duplicate_node_candidates_soft` (label-prefix matches without the id-suffix pattern) does not by itself flip `canonical` to False — treat it as a lead worth a glance, not a proven defect. + ### Step 5 - Label communities Read `graphify-out/.graphify_analysis.json`. For each community key, look at its node labels and write a 2-5 word plain-language name (e.g. "Attention Mechanism", "Training Pipeline", "Data Loading"). diff --git a/tools/skillgen/expected/graphify__skill-pi.md b/tools/skillgen/expected/graphify__skill-pi.md index abd2811d2..715aa76a7 100644 --- a/tools/skillgen/expected/graphify__skill-pi.md +++ b/tools/skillgen/expected/graphify__skill-pi.md @@ -455,7 +455,7 @@ Replace INPUT_PATH with the actual path. ### Step 4.5 - Graph health check (read-only integrity gate) -A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, and self-loops — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. +A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, self-loops, schema errors, likely-reversed citation edges, and probable duplicate whole-file nodes (canonical-identity violations) — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. ```bash $(cat graphify-out/.graphify_python) -c " @@ -474,11 +474,19 @@ flags = [f'{summary[k]} {label}' for k, label in ( ('undirected_same_endpoint_collapsed_edges', 'collapsed (undirected) edges'), ) if summary.get(k, 0)] print('GRAPH HEALTH WARNING: ' + '; '.join(flags) + ' - graph may be incomplete/corrupt.' if flags else 'Graph health: OK (no dangling/missing/collapsed edges).') +is_canonical = summary.get('canonical', True) +issues_text = '; '.join(summary.get('canonical_issues', [])) +canonical_line = 'CANONICAL: ' + str(is_canonical) +if not is_canonical: + canonical_line += ' - ' + issues_text +print(canonical_line) " ``` Substitute `IS_DIRECTED` and `INPUT_PATH` as in Step 4. If a `GRAPH HEALTH WARNING` prints, surface it in the final summary (do not abort — the graph is still usable, but the integrity issue must be visible, per the Honesty Rules). +**If `CANONICAL: False`:** the graph has a structural defect stronger than a collapse warning — a schema error, a dangling/missing edge endpoint, a references/cites edge that is very likely drawn backwards, or two nodes that are probably the same real file under two different ids (see `duplicate_node_candidates` in the report). Say so plainly in the final summary, and while it stays non-canonical: treat god-node/centrality rankings, community/clustering results, and dependency-direction claims as **informational only**, not settled fact — a node's inflated apparent importance may be an artifact of exactly this kind of split, not real architectural coupling. Do not silently present god-node degree or "X is the most central node" as fact against a non-canonical graph; name the specific defect instead. `duplicate_node_candidates_soft` (label-prefix matches without the id-suffix pattern) does not by itself flip `canonical` to False — treat it as a lead worth a glance, not a proven defect. + ### Step 5 - Label communities Read `graphify-out/.graphify_analysis.json`. For each community key, look at its node labels and write a 2-5 word plain-language name (e.g. "Attention Mechanism", "Training Pipeline", "Data Loading"). diff --git a/tools/skillgen/expected/graphify__skill-trae.md b/tools/skillgen/expected/graphify__skill-trae.md index 050667bc2..5b5b69872 100644 --- a/tools/skillgen/expected/graphify__skill-trae.md +++ b/tools/skillgen/expected/graphify__skill-trae.md @@ -453,7 +453,7 @@ Replace INPUT_PATH with the actual path. ### Step 4.5 - Graph health check (read-only integrity gate) -A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, and self-loops — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. +A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, self-loops, schema errors, likely-reversed citation edges, and probable duplicate whole-file nodes (canonical-identity violations) — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. ```bash $(cat graphify-out/.graphify_python) -c " @@ -472,11 +472,19 @@ flags = [f'{summary[k]} {label}' for k, label in ( ('undirected_same_endpoint_collapsed_edges', 'collapsed (undirected) edges'), ) if summary.get(k, 0)] print('GRAPH HEALTH WARNING: ' + '; '.join(flags) + ' - graph may be incomplete/corrupt.' if flags else 'Graph health: OK (no dangling/missing/collapsed edges).') +is_canonical = summary.get('canonical', True) +issues_text = '; '.join(summary.get('canonical_issues', [])) +canonical_line = 'CANONICAL: ' + str(is_canonical) +if not is_canonical: + canonical_line += ' - ' + issues_text +print(canonical_line) " ``` Substitute `IS_DIRECTED` and `INPUT_PATH` as in Step 4. If a `GRAPH HEALTH WARNING` prints, surface it in the final summary (do not abort — the graph is still usable, but the integrity issue must be visible, per the Honesty Rules). +**If `CANONICAL: False`:** the graph has a structural defect stronger than a collapse warning — a schema error, a dangling/missing edge endpoint, a references/cites edge that is very likely drawn backwards, or two nodes that are probably the same real file under two different ids (see `duplicate_node_candidates` in the report). Say so plainly in the final summary, and while it stays non-canonical: treat god-node/centrality rankings, community/clustering results, and dependency-direction claims as **informational only**, not settled fact — a node's inflated apparent importance may be an artifact of exactly this kind of split, not real architectural coupling. Do not silently present god-node degree or "X is the most central node" as fact against a non-canonical graph; name the specific defect instead. `duplicate_node_candidates_soft` (label-prefix matches without the id-suffix pattern) does not by itself flip `canonical` to False — treat it as a lead worth a glance, not a proven defect. + ### Step 5 - Label communities Read `graphify-out/.graphify_analysis.json`. For each community key, look at its node labels and write a 2-5 word plain-language name (e.g. "Attention Mechanism", "Training Pipeline", "Data Loading"). diff --git a/tools/skillgen/expected/graphify__skill-vscode.md b/tools/skillgen/expected/graphify__skill-vscode.md index 20c7c0835..92fb27cec 100644 --- a/tools/skillgen/expected/graphify__skill-vscode.md +++ b/tools/skillgen/expected/graphify__skill-vscode.md @@ -451,7 +451,7 @@ Replace INPUT_PATH with the actual path. ### Step 4.5 - Graph health check (read-only integrity gate) -A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, and self-loops — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. +A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, self-loops, schema errors, likely-reversed citation edges, and probable duplicate whole-file nodes (canonical-identity violations) — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. ```bash $(cat graphify-out/.graphify_python) -c " @@ -470,11 +470,19 @@ flags = [f'{summary[k]} {label}' for k, label in ( ('undirected_same_endpoint_collapsed_edges', 'collapsed (undirected) edges'), ) if summary.get(k, 0)] print('GRAPH HEALTH WARNING: ' + '; '.join(flags) + ' - graph may be incomplete/corrupt.' if flags else 'Graph health: OK (no dangling/missing/collapsed edges).') +is_canonical = summary.get('canonical', True) +issues_text = '; '.join(summary.get('canonical_issues', [])) +canonical_line = 'CANONICAL: ' + str(is_canonical) +if not is_canonical: + canonical_line += ' - ' + issues_text +print(canonical_line) " ``` Substitute `IS_DIRECTED` and `INPUT_PATH` as in Step 4. If a `GRAPH HEALTH WARNING` prints, surface it in the final summary (do not abort — the graph is still usable, but the integrity issue must be visible, per the Honesty Rules). +**If `CANONICAL: False`:** the graph has a structural defect stronger than a collapse warning — a schema error, a dangling/missing edge endpoint, a references/cites edge that is very likely drawn backwards, or two nodes that are probably the same real file under two different ids (see `duplicate_node_candidates` in the report). Say so plainly in the final summary, and while it stays non-canonical: treat god-node/centrality rankings, community/clustering results, and dependency-direction claims as **informational only**, not settled fact — a node's inflated apparent importance may be an artifact of exactly this kind of split, not real architectural coupling. Do not silently present god-node degree or "X is the most central node" as fact against a non-canonical graph; name the specific defect instead. `duplicate_node_candidates_soft` (label-prefix matches without the id-suffix pattern) does not by itself flip `canonical` to False — treat it as a lead worth a glance, not a proven defect. + ### Step 5 - Label communities Read `graphify-out/.graphify_analysis.json`. For each community key, look at its node labels and write a 2-5 word plain-language name (e.g. "Attention Mechanism", "Training Pipeline", "Data Loading"). diff --git a/tools/skillgen/expected/graphify__skill-windows.md b/tools/skillgen/expected/graphify__skill-windows.md index d631821ec..8ee54e72f 100644 --- a/tools/skillgen/expected/graphify__skill-windows.md +++ b/tools/skillgen/expected/graphify__skill-windows.md @@ -477,7 +477,7 @@ Replace INPUT_PATH with the actual path. ### Step 4.5 - Graph health check (read-only integrity gate) -A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, and self-loops — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. +A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, self-loops, schema errors, likely-reversed citation edges, and probable duplicate whole-file nodes (canonical-identity violations) — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. ```powershell @' @@ -496,11 +496,19 @@ flags = [f'{summary[k]} {label}' for k, label in ( ('undirected_same_endpoint_collapsed_edges', 'collapsed (undirected) edges'), ) if summary.get(k, 0)] print('GRAPH HEALTH WARNING: ' + '; '.join(flags) + ' - graph may be incomplete/corrupt.' if flags else 'Graph health: OK (no dangling/missing/collapsed edges).') +is_canonical = summary.get('canonical', True) +issues_text = '; '.join(summary.get('canonical_issues', [])) +canonical_line = 'CANONICAL: ' + str(is_canonical) +if not is_canonical: + canonical_line += ' - ' + issues_text +print(canonical_line) '@ | & (Get-Content graphify-out\.graphify_python) - ``` Substitute `IS_DIRECTED` and `INPUT_PATH` as in Step 4. If a `GRAPH HEALTH WARNING` prints, surface it in the final summary (do not abort — the graph is still usable, but the integrity issue must be visible, per the Honesty Rules). +**If `CANONICAL: False`:** the graph has a structural defect stronger than a collapse warning — a schema error, a dangling/missing edge endpoint, a references/cites edge that is very likely drawn backwards, or two nodes that are probably the same real file under two different ids (see `duplicate_node_candidates` in the report). Say so plainly in the final summary, and while it stays non-canonical: treat god-node/centrality rankings, community/clustering results, and dependency-direction claims as **informational only**, not settled fact — a node's inflated apparent importance may be an artifact of exactly this kind of split, not real architectural coupling. Do not silently present god-node degree or "X is the most central node" as fact against a non-canonical graph; name the specific defect instead. `duplicate_node_candidates_soft` (label-prefix matches without the id-suffix pattern) does not by itself flip `canonical` to False — treat it as a lead worth a glance, not a proven defect. + ### Step 5 - Label communities Read `graphify-out/.graphify_analysis.json`. For each community key, look at its node labels and write a 2-5 word plain-language name (e.g. "Attention Mechanism", "Training Pipeline", "Data Loading"). diff --git a/tools/skillgen/expected/graphify__skill.md b/tools/skillgen/expected/graphify__skill.md index abd2811d2..715aa76a7 100644 --- a/tools/skillgen/expected/graphify__skill.md +++ b/tools/skillgen/expected/graphify__skill.md @@ -455,7 +455,7 @@ Replace INPUT_PATH with the actual path. ### Step 4.5 - Graph health check (read-only integrity gate) -A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, and self-loops — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. +A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, self-loops, schema errors, likely-reversed citation edges, and probable duplicate whole-file nodes (canonical-identity violations) — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. ```bash $(cat graphify-out/.graphify_python) -c " @@ -474,11 +474,19 @@ flags = [f'{summary[k]} {label}' for k, label in ( ('undirected_same_endpoint_collapsed_edges', 'collapsed (undirected) edges'), ) if summary.get(k, 0)] print('GRAPH HEALTH WARNING: ' + '; '.join(flags) + ' - graph may be incomplete/corrupt.' if flags else 'Graph health: OK (no dangling/missing/collapsed edges).') +is_canonical = summary.get('canonical', True) +issues_text = '; '.join(summary.get('canonical_issues', [])) +canonical_line = 'CANONICAL: ' + str(is_canonical) +if not is_canonical: + canonical_line += ' - ' + issues_text +print(canonical_line) " ``` Substitute `IS_DIRECTED` and `INPUT_PATH` as in Step 4. If a `GRAPH HEALTH WARNING` prints, surface it in the final summary (do not abort — the graph is still usable, but the integrity issue must be visible, per the Honesty Rules). +**If `CANONICAL: False`:** the graph has a structural defect stronger than a collapse warning — a schema error, a dangling/missing edge endpoint, a references/cites edge that is very likely drawn backwards, or two nodes that are probably the same real file under two different ids (see `duplicate_node_candidates` in the report). Say so plainly in the final summary, and while it stays non-canonical: treat god-node/centrality rankings, community/clustering results, and dependency-direction claims as **informational only**, not settled fact — a node's inflated apparent importance may be an artifact of exactly this kind of split, not real architectural coupling. Do not silently present god-node degree or "X is the most central node" as fact against a non-canonical graph; name the specific defect instead. `duplicate_node_candidates_soft` (label-prefix matches without the id-suffix pattern) does not by itself flip `canonical` to False — treat it as a lead worth a glance, not a proven defect. + ### Step 5 - Label communities Read `graphify-out/.graphify_analysis.json`. For each community key, look at its node labels and write a 2-5 word plain-language name (e.g. "Attention Mechanism", "Training Pipeline", "Data Loading"). diff --git a/tools/skillgen/fragments/core/core.md b/tools/skillgen/fragments/core/core.md index c527a1256..1f3b3e055 100644 --- a/tools/skillgen/fragments/core/core.md +++ b/tools/skillgen/fragments/core/core.md @@ -390,7 +390,7 @@ Replace INPUT_PATH with the actual path. ### Step 4.5 - Graph health check (read-only integrity gate) -A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, and self-loops — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. +A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, self-loops, schema errors, likely-reversed citation edges, and probable duplicate whole-file nodes (canonical-identity violations) — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts. ```bash $(cat graphify-out/.graphify_python) -c " @@ -409,11 +409,19 @@ flags = [f'{summary[k]} {label}' for k, label in ( ('undirected_same_endpoint_collapsed_edges', 'collapsed (undirected) edges'), ) if summary.get(k, 0)] print('GRAPH HEALTH WARNING: ' + '; '.join(flags) + ' - graph may be incomplete/corrupt.' if flags else 'Graph health: OK (no dangling/missing/collapsed edges).') +is_canonical = summary.get('canonical', True) +issues_text = '; '.join(summary.get('canonical_issues', [])) +canonical_line = 'CANONICAL: ' + str(is_canonical) +if not is_canonical: + canonical_line += ' - ' + issues_text +print(canonical_line) " ``` Substitute `IS_DIRECTED` and `INPUT_PATH` as in Step 4. If a `GRAPH HEALTH WARNING` prints, surface it in the final summary (do not abort — the graph is still usable, but the integrity issue must be visible, per the Honesty Rules). +**If `CANONICAL: False`:** the graph has a structural defect stronger than a collapse warning — a schema error, a dangling/missing edge endpoint, a references/cites edge that is very likely drawn backwards, or two nodes that are probably the same real file under two different ids (see `duplicate_node_candidates` in the report). Say so plainly in the final summary, and while it stays non-canonical: treat god-node/centrality rankings, community/clustering results, and dependency-direction claims as **informational only**, not settled fact — a node's inflated apparent importance may be an artifact of exactly this kind of split, not real architectural coupling. Do not silently present god-node degree or "X is the most central node" as fact against a non-canonical graph; name the specific defect instead. `duplicate_node_candidates_soft` (label-prefix matches without the id-suffix pattern) does not by itself flip `canonical` to False — treat it as a lead worth a glance, not a proven defect. + ### Step 5 - Label communities Read `graphify-out/.graphify_analysis.json`. For each community key, look at its node labels and write a 2-5 word plain-language name (e.g. "Attention Mechanism", "Training Pipeline", "Data Loading").