Request hyperedges in the native-backend extraction prompt (#1418 follow-up)

`graphify extract --backend <gemini|claude|claude-cli|openai|kimi|...>` produced
zero hyperedges for any corpus: llm._EXTRACTION_SYSTEM only showed
"hyperedges":[] in its output schema and never described what a hyperedge is, so
every model returned the empty array. Meanwhile the agent/skill path, whose
references/extraction-spec.md fully documents hyperedges ("3 or more nodes
participate together..."), produced them — the two prompts had drifted.

Bring the native prompt in line with the skill spec: add the hyperedge
instruction and a populated schema example. The parse/merge side already handled
hyperedges, so this is prompt-only. Verified with a real claude-cli run — a doc
that previously yielded 0 hyperedges now yields one, correctly relativized (#1418).

Adds two guard tests: the native prompt must request hyperedges with a populated
example, and it must share the skill spec's hyperedge wording so they can't drift
apart again.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
safishamsi
2026-06-22 22:20:40 +01:00
co-authored by Claude Opus 4.8
parent b8dc31f760
commit aad3b47098
3 changed files with 33 additions and 1 deletions
+1
View File
@@ -4,6 +4,7 @@ Full release notes with details on each version: [GitHub Releases](https://githu
## Unreleased
- Fix: native-backend semantic extraction now produces hyperedges. The `graphify extract --backend <gemini|claude|claude-cli|openai|kimi|…>` prompt (`llm._EXTRACTION_SYSTEM`) only ever showed `"hyperedges":[]` in its output schema and never explained what a hyperedge is, so every native backend silently emitted zero — while the agent/skill path (whose `extraction-spec.md` fully documents hyperedges) produced them. The two prompts had drifted. The native prompt now carries the same "3 or more nodes participate together" instruction and a populated schema example, so both extraction paths yield the same hyperedge behaviour for a given corpus. Verified end-to-end: a doc that previously produced 0 hyperedges now produces one (correctly relativized, per #1418). Guard tests assert the two prompts can't drift apart again.
- Fix: `GRAPHIFY_OUT` is now honoured end-to-end. The override (a custom output-dir name or absolute path, for worktrees/shared setups — #686) was only respected by some readers; `graphify extract` and several commands hardcoded `graphify-out/`, so a `GRAPHIFY_OUT=custom-out graphify extract` still wrote to `graphify-out/`, and downstream `query`/`serve`/`update` looked in the wrong place. The output-dir name is now resolved through `graphify.paths` everywhere it matters: the `extract` write dir, `cluster-only`/`label`, `query`/`affected`/`benchmark` defaults, `save-result --memory-dir`, `uninstall --purge`, `cache-check`, the `manifest.json`/`transcripts`/`memory`/`converted` paths in `detect`/`transcribe`, the `build_merge`/`serve`/`benchmark`/`prs` graph-path defaults, and the `detect` scan-exclude (so a renamed output dir is never re-ingested as source). Default behaviour is unchanged — without the env var everything still uses `graphify-out/` (#1423).
- Fix: the `GRAPH_REPORT.md` header now shows the actual scan root instead of a literal `.`. The split-skill runbook passed `'.'` as the `root` argument to `report.generate` in Steps 4 and 5, so a `/graphify /some/path` run produced a report titled `# Graph Report - .`. It now passes `'INPUT_PATH'` (matching the monoliths, which were already correct). Display-only — no path written to `graph.json`/`manifest.json` was affected (#1419).
- Fix: the skill runbooks now write a portable `manifest.json`. Step 9 (full build) and the `--update` reference called `save_manifest(...)` without `root=`, so manifest keys were stored as absolute paths; cloning or moving the repo then broke `graphify --update` — every cached file missed and the whole corpus re-extracted. All four runbook call sites (the lean-core `skill.md`, the Aider/Devin monoliths, and the shared `--update` reference) now pass `root='INPUT_PATH'`, relativizing keys to the scan root to match the native `graphify update` path. The monolith change is registered as a new sanctioned change-class in the round-trip guard (#1417).
+3 -1
View File
@@ -386,8 +386,10 @@ Edge direction rule — source is always the ACTOR, target is the ACTED-UPON:
- imports/references: source = the file/entity that imports or references; target = the thing imported or referenced.
- implements/inherits: source = the subclass/implementor; target = the base class/interface.
Hyperedges: if 3 or more nodes clearly participate together in a shared concept, flow, or pattern that is not captured by pairwise edges alone, add a hyperedge to the top-level `hyperedges` array (e.g. all classes implementing one protocol, all functions in one auth flow even if they don't all call each other, all concepts from a paper section forming one coherent idea). Use sparingly — only when the group relationship adds information beyond the pairwise edges. Maximum 3 hyperedges per chunk.
Output exactly this schema:
{"nodes":[{"id":"stem_entity","label":"Human Readable Name","file_type":"code|document|paper|image|rationale|concept","source_file":"relative/path","source_location":null,"source_url":null,"captured_at":null,"author":null,"contributor":null}],"edges":[{"source":"node_id","target":"node_id","relation":"calls|implements|references|cites|conceptually_related_to|shares_data_with|semantically_similar_to","confidence":"EXTRACTED|INFERRED|AMBIGUOUS","confidence_score":1.0,"source_file":"relative/path","source_location":null,"weight":1.0}],"hyperedges":[],"input_tokens":0,"output_tokens":0}
{"nodes":[{"id":"stem_entity","label":"Human Readable Name","file_type":"code|document|paper|image|rationale|concept","source_file":"relative/path","source_location":null,"source_url":null,"captured_at":null,"author":null,"contributor":null}],"edges":[{"source":"node_id","target":"node_id","relation":"calls|implements|references|cites|conceptually_related_to|shares_data_with|semantically_similar_to","confidence":"EXTRACTED|INFERRED|AMBIGUOUS","confidence_score":1.0,"source_file":"relative/path","source_location":null,"weight":1.0}],"hyperedges":[{"id":"snake_case_id","label":"Human Readable Label","nodes":["node_id1","node_id2","node_id3"],"relation":"participate_in|implement|form","confidence":"EXTRACTED|INFERRED","confidence_score":0.75,"source_file":"relative/path"}],"input_tokens":0,"output_tokens":0}
"""
_DEEP_EXTRACTION_SUFFIX = """\
+29
View File
@@ -845,3 +845,32 @@ def test_openai_compat_env_var_temperature_applied(tmp_path, monkeypatch):
llm.extract_files_direct([tmp_path / "f.py"], backend="openai", root=tmp_path)
assert captured.get("temperature") == 0.3
def test_native_extraction_prompt_requests_hyperedges():
"""The native-backend prompt must request hyperedges, like the skill's
extraction-spec does — otherwise `graphify extract --backend X` silently
produces zero hyperedges while the agent path produces them. Guards against
the two prompts drifting apart again.
"""
for deep in (False, True):
prompt = llm._extraction_system(deep=deep)
assert "hyperedge" in prompt.lower(), f"deep={deep}: prompt does not mention hyperedges"
assert "3 or more nodes" in prompt, f"deep={deep}: prompt lacks the hyperedge guidance"
# The schema example must show a populated hyperedge, not an empty array.
assert '"hyperedges":[]' not in prompt, f"deep={deep}: schema still shows empty hyperedges"
assert '"nodes":["node_id1"' in prompt, f"deep={deep}: schema lacks a populated hyperedge example"
def test_native_extraction_prompt_matches_skill_spec_on_hyperedges():
"""Both extraction paths share the same hyperedge contract (the '3 or more
nodes … participate together' rule), so a corpus yields the same hyperedge
behaviour whether built via the skill or `graphify extract --backend`.
"""
spec = (
Path(__file__).resolve().parents[1]
/ "tools" / "skillgen" / "fragments" / "references" / "shared" / "extraction-spec.md"
).read_text(encoding="utf-8")
shared = "3 or more nodes clearly participate together"
assert shared in spec, "skill extraction-spec changed its hyperedge wording"
assert shared in llm._EXTRACTION_SYSTEM, "native prompt drifted from the skill hyperedge wording"