diff --git a/graphify/skill-agents.md b/graphify/skill-agents.md index afb4ecc12..1732e13b3 100644 --- a/graphify/skill-agents.md +++ b/graphify/skill-agents.md @@ -664,6 +664,8 @@ fi Both are non-default subcommands. `--update` re-extracts only new or changed files; `--cluster-only` reruns clustering on the existing graph. See `references/update.md` for both flows. +> **Re-extracting an already-graphed doc requires the mandatory-ID gate** (`references/update.md` → "REQUIRED: mandatory-ID gate for re-extracted docs"): snapshot that file's existing node IDs, pass them to the subagent as must-include, and refuse the merge if the node count drops below 90% of baseline or any ID is missing. Semantic re-extraction is non-deterministic and drops whole sections without it. + --- ## For /graphify query diff --git a/graphify/skill-amp.md b/graphify/skill-amp.md index afb4ecc12..1732e13b3 100644 --- a/graphify/skill-amp.md +++ b/graphify/skill-amp.md @@ -664,6 +664,8 @@ fi Both are non-default subcommands. `--update` re-extracts only new or changed files; `--cluster-only` reruns clustering on the existing graph. See `references/update.md` for both flows. +> **Re-extracting an already-graphed doc requires the mandatory-ID gate** (`references/update.md` → "REQUIRED: mandatory-ID gate for re-extracted docs"): snapshot that file's existing node IDs, pass them to the subagent as must-include, and refuse the merge if the node count drops below 90% of baseline or any ID is missing. Semantic re-extraction is non-deterministic and drops whole sections without it. + --- ## For /graphify query diff --git a/graphify/skill-claw.md b/graphify/skill-claw.md index d98865cc8..524d933f2 100644 --- a/graphify/skill-claw.md +++ b/graphify/skill-claw.md @@ -667,6 +667,8 @@ fi Both are non-default subcommands. `--update` re-extracts only new or changed files; `--cluster-only` reruns clustering on the existing graph. See `references/update.md` for both flows. +> **Re-extracting an already-graphed doc requires the mandatory-ID gate** (`references/update.md` → "REQUIRED: mandatory-ID gate for re-extracted docs"): snapshot that file's existing node IDs, pass them to the subagent as must-include, and refuse the merge if the node count drops below 90% of baseline or any ID is missing. Semantic re-extraction is non-deterministic and drops whole sections without it. + --- ## For /graphify query diff --git a/graphify/skill-codex.md b/graphify/skill-codex.md index 0c821a278..ac3ee9781 100644 --- a/graphify/skill-codex.md +++ b/graphify/skill-codex.md @@ -664,6 +664,8 @@ fi Both are non-default subcommands. `--update` re-extracts only new or changed files; `--cluster-only` reruns clustering on the existing graph. See `references/update.md` for both flows. +> **Re-extracting an already-graphed doc requires the mandatory-ID gate** (`references/update.md` → "REQUIRED: mandatory-ID gate for re-extracted docs"): snapshot that file's existing node IDs, pass them to the subagent as must-include, and refuse the merge if the node count drops below 90% of baseline or any ID is missing. Semantic re-extraction is non-deterministic and drops whole sections without it. + --- ## For /graphify query diff --git a/graphify/skill-copilot.md b/graphify/skill-copilot.md index d98865cc8..524d933f2 100644 --- a/graphify/skill-copilot.md +++ b/graphify/skill-copilot.md @@ -667,6 +667,8 @@ fi Both are non-default subcommands. `--update` re-extracts only new or changed files; `--cluster-only` reruns clustering on the existing graph. See `references/update.md` for both flows. +> **Re-extracting an already-graphed doc requires the mandatory-ID gate** (`references/update.md` → "REQUIRED: mandatory-ID gate for re-extracted docs"): snapshot that file's existing node IDs, pass them to the subagent as must-include, and refuse the merge if the node count drops below 90% of baseline or any ID is missing. Semantic re-extraction is non-deterministic and drops whole sections without it. + --- ## For /graphify query diff --git a/graphify/skill-droid.md b/graphify/skill-droid.md index c3815d556..8a90a9d4f 100644 --- a/graphify/skill-droid.md +++ b/graphify/skill-droid.md @@ -664,6 +664,8 @@ fi Both are non-default subcommands. `--update` re-extracts only new or changed files; `--cluster-only` reruns clustering on the existing graph. See `references/update.md` for both flows. +> **Re-extracting an already-graphed doc requires the mandatory-ID gate** (`references/update.md` → "REQUIRED: mandatory-ID gate for re-extracted docs"): snapshot that file's existing node IDs, pass them to the subagent as must-include, and refuse the merge if the node count drops below 90% of baseline or any ID is missing. Semantic re-extraction is non-deterministic and drops whole sections without it. + --- ## For /graphify query diff --git a/graphify/skill-kilo.md b/graphify/skill-kilo.md index dbb4658ca..dfca9ebb9 100644 --- a/graphify/skill-kilo.md +++ b/graphify/skill-kilo.md @@ -667,6 +667,8 @@ fi Both are non-default subcommands. `--update` re-extracts only new or changed files; `--cluster-only` reruns clustering on the existing graph. See `references/update.md` for both flows. +> **Re-extracting an already-graphed doc requires the mandatory-ID gate** (`references/update.md` → "REQUIRED: mandatory-ID gate for re-extracted docs"): snapshot that file's existing node IDs, pass them to the subagent as must-include, and refuse the merge if the node count drops below 90% of baseline or any ID is missing. Semantic re-extraction is non-deterministic and drops whole sections without it. + --- ## For /graphify query diff --git a/graphify/skill-kiro.md b/graphify/skill-kiro.md index d98865cc8..524d933f2 100644 --- a/graphify/skill-kiro.md +++ b/graphify/skill-kiro.md @@ -667,6 +667,8 @@ fi Both are non-default subcommands. `--update` re-extracts only new or changed files; `--cluster-only` reruns clustering on the existing graph. See `references/update.md` for both flows. +> **Re-extracting an already-graphed doc requires the mandatory-ID gate** (`references/update.md` → "REQUIRED: mandatory-ID gate for re-extracted docs"): snapshot that file's existing node IDs, pass them to the subagent as must-include, and refuse the merge if the node count drops below 90% of baseline or any ID is missing. Semantic re-extraction is non-deterministic and drops whole sections without it. + --- ## For /graphify query diff --git a/graphify/skill-opencode.md b/graphify/skill-opencode.md index cf5dae440..9b9f5b726 100644 --- a/graphify/skill-opencode.md +++ b/graphify/skill-opencode.md @@ -659,6 +659,8 @@ fi Both are non-default subcommands. `--update` re-extracts only new or changed files; `--cluster-only` reruns clustering on the existing graph. See `references/update.md` for both flows. +> **Re-extracting an already-graphed doc requires the mandatory-ID gate** (`references/update.md` → "REQUIRED: mandatory-ID gate for re-extracted docs"): snapshot that file's existing node IDs, pass them to the subagent as must-include, and refuse the merge if the node count drops below 90% of baseline or any ID is missing. Semantic re-extraction is non-deterministic and drops whole sections without it. + --- ## For /graphify query diff --git a/graphify/skill-pi.md b/graphify/skill-pi.md index d98865cc8..524d933f2 100644 --- a/graphify/skill-pi.md +++ b/graphify/skill-pi.md @@ -667,6 +667,8 @@ fi Both are non-default subcommands. `--update` re-extracts only new or changed files; `--cluster-only` reruns clustering on the existing graph. See `references/update.md` for both flows. +> **Re-extracting an already-graphed doc requires the mandatory-ID gate** (`references/update.md` → "REQUIRED: mandatory-ID gate for re-extracted docs"): snapshot that file's existing node IDs, pass them to the subagent as must-include, and refuse the merge if the node count drops below 90% of baseline or any ID is missing. Semantic re-extraction is non-deterministic and drops whole sections without it. + --- ## For /graphify query diff --git a/graphify/skill-trae.md b/graphify/skill-trae.md index b0cbeb122..467c3b7d8 100644 --- a/graphify/skill-trae.md +++ b/graphify/skill-trae.md @@ -665,6 +665,8 @@ fi Both are non-default subcommands. `--update` re-extracts only new or changed files; `--cluster-only` reruns clustering on the existing graph. See `references/update.md` for both flows. +> **Re-extracting an already-graphed doc requires the mandatory-ID gate** (`references/update.md` → "REQUIRED: mandatory-ID gate for re-extracted docs"): snapshot that file's existing node IDs, pass them to the subagent as must-include, and refuse the merge if the node count drops below 90% of baseline or any ID is missing. Semantic re-extraction is non-deterministic and drops whole sections without it. + --- ## For /graphify query diff --git a/graphify/skill-vscode.md b/graphify/skill-vscode.md index 3e6bc6b7b..b8c55d517 100644 --- a/graphify/skill-vscode.md +++ b/graphify/skill-vscode.md @@ -663,6 +663,8 @@ fi Both are non-default subcommands. `--update` re-extracts only new or changed files; `--cluster-only` reruns clustering on the existing graph. See `references/update.md` for both flows. +> **Re-extracting an already-graphed doc requires the mandatory-ID gate** (`references/update.md` → "REQUIRED: mandatory-ID gate for re-extracted docs"): snapshot that file's existing node IDs, pass them to the subagent as must-include, and refuse the merge if the node count drops below 90% of baseline or any ID is missing. Semantic re-extraction is non-deterministic and drops whole sections without it. + --- ## For /graphify query diff --git a/graphify/skill-windows.md b/graphify/skill-windows.md index 574384576..abddef6e1 100644 --- a/graphify/skill-windows.md +++ b/graphify/skill-windows.md @@ -689,6 +689,8 @@ fi Both are non-default subcommands. `--update` re-extracts only new or changed files; `--cluster-only` reruns clustering on the existing graph. See `references/update.md` for both flows. +> **Re-extracting an already-graphed doc requires the mandatory-ID gate** (`references/update.md` → "REQUIRED: mandatory-ID gate for re-extracted docs"): snapshot that file's existing node IDs, pass them to the subagent as must-include, and refuse the merge if the node count drops below 90% of baseline or any ID is missing. Semantic re-extraction is non-deterministic and drops whole sections without it. + --- ## For /graphify query diff --git a/graphify/skill.md b/graphify/skill.md index d98865cc8..524d933f2 100644 --- a/graphify/skill.md +++ b/graphify/skill.md @@ -667,6 +667,8 @@ fi Both are non-default subcommands. `--update` re-extracts only new or changed files; `--cluster-only` reruns clustering on the existing graph. See `references/update.md` for both flows. +> **Re-extracting an already-graphed doc requires the mandatory-ID gate** (`references/update.md` → "REQUIRED: mandatory-ID gate for re-extracted docs"): snapshot that file's existing node IDs, pass them to the subagent as must-include, and refuse the merge if the node count drops below 90% of baseline or any ID is missing. Semantic re-extraction is non-deterministic and drops whole sections without it. + --- ## For /graphify query diff --git a/graphify/skills/agents/references/update.md b/graphify/skills/agents/references/update.md index 3632fd412..b5b16a731 100644 --- a/graphify/skills/agents/references/update.md +++ b/graphify/skills/agents/references/update.md @@ -65,6 +65,71 @@ If `code_only` is True: print `[graphify update] Code-only changes detected - sk If `code_only` is False (any changed file is a doc/paper/image/video): **first, if any changed file is in `new_files['video']`, run `references/transcribe.md` (Step 2.5) on those files, then rewrite `.graphify_detect.json` to move the resulting transcript paths into `files['document']` and drop `files['video']`** — otherwise raw `.mp4/.mp3` paths are fed to semantic subagents as unreadable media (#1392). Then run the full Steps 3A–3C pipeline as normal. +### REQUIRED: mandatory-ID gate for re-extracted docs + +Semantic re-extraction of an already-graphed doc is non-deterministic: the same file can +yield 75 nodes on one run and 49 on the next, dropping whole sections with no error. The +loss lands in `build_merge` and only surfaces later as a question the graph can no longer +answer. Apply this gate to **every changed doc/paper that already has nodes in +`graph.json`**. Do not skip it for a small edit — a one-line change re-extracts the whole +file. + +**1 — Snapshot the file's existing node IDs before extracting.** + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +TARGET = 'DOC_PATH' # substitute: the changed doc, as it appears in source_file +g = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) +ids = sorted(n['id'] for n in g['nodes'] if str(n.get('source_file','')).endswith(TARGET)) +Path('graphify-out/.graphify_must_ids.txt').write_text('\n'.join(ids), encoding='utf-8') +print(f'baseline: {len(ids)} existing node(s) for {TARGET}') +" +``` + +A count of 0 means the file is new — skip the gate and extract normally. + +**2 — Pass the IDs to the subagent as mandatory.** Append to the prompt from +`references/extraction-spec.md`: + +- the baseline count as an explicit target ("the prior build extracted N nodes from this + file; land at approximately N — under 90% of N means you under-extracted, go back and + cover the sections you skimmed"), +- the full ID list verbatim, labelled **MANDATORY — every one of these existed in the + prior graph and MUST appear in your output, reusing the ID verbatim**, +- an instruction to verify every mandatory ID is present *before* writing CHUNK_PATH. + +Reusing the IDs also suppresses gratuitous ID churn on re-extraction, which orphans saved +queries and inflates the merge diff for no semantic gain. + +**3 — Hard-gate the merge.** After the chunk lands and before `build_merge`, require both: +node count ≥ 90% of baseline, and zero mandatory IDs missing. + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +d = json.loads(Path('graphify-out/.graphify_chunk_01.json').read_text(encoding='utf-8')) +must = [l for l in Path('graphify-out/.graphify_must_ids.txt').read_text(encoding='utf-8').split() if l] +ids = {n['id'] for n in d['nodes']} +missing = [m for m in must if m not in ids] +ok = len(d['nodes']) >= 0.9 * len(must) and not missing +print(f'nodes={len(d[\"nodes\"])} baseline={len(must)} missing={len(missing)}') +if missing: print('MISSING:', ' '.join(missing[:20])) +print('GATE:', 'PASS' if ok else 'FAIL') +" +``` + +On **FAIL**, do not merge: re-dispatch the extraction with the same mandatory list and a +sharper completeness instruction naming the missing IDs. Two consecutive failures — stop +and report to the user rather than merging a regressed graph. + +**The `to_json` shrink guard (#479) stays authoritative.** It is the backstop, not a +nuisance. Never pass `force=True` to clear it until you have diffed the old and new node +sets and can name why each removed node is legitimately gone (file deleted, section +removed, ID renamed with a verified replacement). ID churn on a re-extracted file is a +legitimate shrink; a missing section is not. If no new files exist (only deletions), create an empty extraction so the merge step can prune: diff --git a/graphify/skills/amp/references/update.md b/graphify/skills/amp/references/update.md index 3632fd412..b5b16a731 100644 --- a/graphify/skills/amp/references/update.md +++ b/graphify/skills/amp/references/update.md @@ -65,6 +65,71 @@ If `code_only` is True: print `[graphify update] Code-only changes detected - sk If `code_only` is False (any changed file is a doc/paper/image/video): **first, if any changed file is in `new_files['video']`, run `references/transcribe.md` (Step 2.5) on those files, then rewrite `.graphify_detect.json` to move the resulting transcript paths into `files['document']` and drop `files['video']`** — otherwise raw `.mp4/.mp3` paths are fed to semantic subagents as unreadable media (#1392). Then run the full Steps 3A–3C pipeline as normal. +### REQUIRED: mandatory-ID gate for re-extracted docs + +Semantic re-extraction of an already-graphed doc is non-deterministic: the same file can +yield 75 nodes on one run and 49 on the next, dropping whole sections with no error. The +loss lands in `build_merge` and only surfaces later as a question the graph can no longer +answer. Apply this gate to **every changed doc/paper that already has nodes in +`graph.json`**. Do not skip it for a small edit — a one-line change re-extracts the whole +file. + +**1 — Snapshot the file's existing node IDs before extracting.** + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +TARGET = 'DOC_PATH' # substitute: the changed doc, as it appears in source_file +g = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) +ids = sorted(n['id'] for n in g['nodes'] if str(n.get('source_file','')).endswith(TARGET)) +Path('graphify-out/.graphify_must_ids.txt').write_text('\n'.join(ids), encoding='utf-8') +print(f'baseline: {len(ids)} existing node(s) for {TARGET}') +" +``` + +A count of 0 means the file is new — skip the gate and extract normally. + +**2 — Pass the IDs to the subagent as mandatory.** Append to the prompt from +`references/extraction-spec.md`: + +- the baseline count as an explicit target ("the prior build extracted N nodes from this + file; land at approximately N — under 90% of N means you under-extracted, go back and + cover the sections you skimmed"), +- the full ID list verbatim, labelled **MANDATORY — every one of these existed in the + prior graph and MUST appear in your output, reusing the ID verbatim**, +- an instruction to verify every mandatory ID is present *before* writing CHUNK_PATH. + +Reusing the IDs also suppresses gratuitous ID churn on re-extraction, which orphans saved +queries and inflates the merge diff for no semantic gain. + +**3 — Hard-gate the merge.** After the chunk lands and before `build_merge`, require both: +node count ≥ 90% of baseline, and zero mandatory IDs missing. + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +d = json.loads(Path('graphify-out/.graphify_chunk_01.json').read_text(encoding='utf-8')) +must = [l for l in Path('graphify-out/.graphify_must_ids.txt').read_text(encoding='utf-8').split() if l] +ids = {n['id'] for n in d['nodes']} +missing = [m for m in must if m not in ids] +ok = len(d['nodes']) >= 0.9 * len(must) and not missing +print(f'nodes={len(d[\"nodes\"])} baseline={len(must)} missing={len(missing)}') +if missing: print('MISSING:', ' '.join(missing[:20])) +print('GATE:', 'PASS' if ok else 'FAIL') +" +``` + +On **FAIL**, do not merge: re-dispatch the extraction with the same mandatory list and a +sharper completeness instruction naming the missing IDs. Two consecutive failures — stop +and report to the user rather than merging a regressed graph. + +**The `to_json` shrink guard (#479) stays authoritative.** It is the backstop, not a +nuisance. Never pass `force=True` to clear it until you have diffed the old and new node +sets and can name why each removed node is legitimately gone (file deleted, section +removed, ID renamed with a verified replacement). ID churn on a re-extracted file is a +legitimate shrink; a missing section is not. If no new files exist (only deletions), create an empty extraction so the merge step can prune: diff --git a/graphify/skills/claude/references/update.md b/graphify/skills/claude/references/update.md index 3632fd412..b5b16a731 100644 --- a/graphify/skills/claude/references/update.md +++ b/graphify/skills/claude/references/update.md @@ -65,6 +65,71 @@ If `code_only` is True: print `[graphify update] Code-only changes detected - sk If `code_only` is False (any changed file is a doc/paper/image/video): **first, if any changed file is in `new_files['video']`, run `references/transcribe.md` (Step 2.5) on those files, then rewrite `.graphify_detect.json` to move the resulting transcript paths into `files['document']` and drop `files['video']`** — otherwise raw `.mp4/.mp3` paths are fed to semantic subagents as unreadable media (#1392). Then run the full Steps 3A–3C pipeline as normal. +### REQUIRED: mandatory-ID gate for re-extracted docs + +Semantic re-extraction of an already-graphed doc is non-deterministic: the same file can +yield 75 nodes on one run and 49 on the next, dropping whole sections with no error. The +loss lands in `build_merge` and only surfaces later as a question the graph can no longer +answer. Apply this gate to **every changed doc/paper that already has nodes in +`graph.json`**. Do not skip it for a small edit — a one-line change re-extracts the whole +file. + +**1 — Snapshot the file's existing node IDs before extracting.** + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +TARGET = 'DOC_PATH' # substitute: the changed doc, as it appears in source_file +g = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) +ids = sorted(n['id'] for n in g['nodes'] if str(n.get('source_file','')).endswith(TARGET)) +Path('graphify-out/.graphify_must_ids.txt').write_text('\n'.join(ids), encoding='utf-8') +print(f'baseline: {len(ids)} existing node(s) for {TARGET}') +" +``` + +A count of 0 means the file is new — skip the gate and extract normally. + +**2 — Pass the IDs to the subagent as mandatory.** Append to the prompt from +`references/extraction-spec.md`: + +- the baseline count as an explicit target ("the prior build extracted N nodes from this + file; land at approximately N — under 90% of N means you under-extracted, go back and + cover the sections you skimmed"), +- the full ID list verbatim, labelled **MANDATORY — every one of these existed in the + prior graph and MUST appear in your output, reusing the ID verbatim**, +- an instruction to verify every mandatory ID is present *before* writing CHUNK_PATH. + +Reusing the IDs also suppresses gratuitous ID churn on re-extraction, which orphans saved +queries and inflates the merge diff for no semantic gain. + +**3 — Hard-gate the merge.** After the chunk lands and before `build_merge`, require both: +node count ≥ 90% of baseline, and zero mandatory IDs missing. + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +d = json.loads(Path('graphify-out/.graphify_chunk_01.json').read_text(encoding='utf-8')) +must = [l for l in Path('graphify-out/.graphify_must_ids.txt').read_text(encoding='utf-8').split() if l] +ids = {n['id'] for n in d['nodes']} +missing = [m for m in must if m not in ids] +ok = len(d['nodes']) >= 0.9 * len(must) and not missing +print(f'nodes={len(d[\"nodes\"])} baseline={len(must)} missing={len(missing)}') +if missing: print('MISSING:', ' '.join(missing[:20])) +print('GATE:', 'PASS' if ok else 'FAIL') +" +``` + +On **FAIL**, do not merge: re-dispatch the extraction with the same mandatory list and a +sharper completeness instruction naming the missing IDs. Two consecutive failures — stop +and report to the user rather than merging a regressed graph. + +**The `to_json` shrink guard (#479) stays authoritative.** It is the backstop, not a +nuisance. Never pass `force=True` to clear it until you have diffed the old and new node +sets and can name why each removed node is legitimately gone (file deleted, section +removed, ID renamed with a verified replacement). ID churn on a re-extracted file is a +legitimate shrink; a missing section is not. If no new files exist (only deletions), create an empty extraction so the merge step can prune: diff --git a/graphify/skills/claw/references/update.md b/graphify/skills/claw/references/update.md index 3632fd412..b5b16a731 100644 --- a/graphify/skills/claw/references/update.md +++ b/graphify/skills/claw/references/update.md @@ -65,6 +65,71 @@ If `code_only` is True: print `[graphify update] Code-only changes detected - sk If `code_only` is False (any changed file is a doc/paper/image/video): **first, if any changed file is in `new_files['video']`, run `references/transcribe.md` (Step 2.5) on those files, then rewrite `.graphify_detect.json` to move the resulting transcript paths into `files['document']` and drop `files['video']`** — otherwise raw `.mp4/.mp3` paths are fed to semantic subagents as unreadable media (#1392). Then run the full Steps 3A–3C pipeline as normal. +### REQUIRED: mandatory-ID gate for re-extracted docs + +Semantic re-extraction of an already-graphed doc is non-deterministic: the same file can +yield 75 nodes on one run and 49 on the next, dropping whole sections with no error. The +loss lands in `build_merge` and only surfaces later as a question the graph can no longer +answer. Apply this gate to **every changed doc/paper that already has nodes in +`graph.json`**. Do not skip it for a small edit — a one-line change re-extracts the whole +file. + +**1 — Snapshot the file's existing node IDs before extracting.** + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +TARGET = 'DOC_PATH' # substitute: the changed doc, as it appears in source_file +g = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) +ids = sorted(n['id'] for n in g['nodes'] if str(n.get('source_file','')).endswith(TARGET)) +Path('graphify-out/.graphify_must_ids.txt').write_text('\n'.join(ids), encoding='utf-8') +print(f'baseline: {len(ids)} existing node(s) for {TARGET}') +" +``` + +A count of 0 means the file is new — skip the gate and extract normally. + +**2 — Pass the IDs to the subagent as mandatory.** Append to the prompt from +`references/extraction-spec.md`: + +- the baseline count as an explicit target ("the prior build extracted N nodes from this + file; land at approximately N — under 90% of N means you under-extracted, go back and + cover the sections you skimmed"), +- the full ID list verbatim, labelled **MANDATORY — every one of these existed in the + prior graph and MUST appear in your output, reusing the ID verbatim**, +- an instruction to verify every mandatory ID is present *before* writing CHUNK_PATH. + +Reusing the IDs also suppresses gratuitous ID churn on re-extraction, which orphans saved +queries and inflates the merge diff for no semantic gain. + +**3 — Hard-gate the merge.** After the chunk lands and before `build_merge`, require both: +node count ≥ 90% of baseline, and zero mandatory IDs missing. + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +d = json.loads(Path('graphify-out/.graphify_chunk_01.json').read_text(encoding='utf-8')) +must = [l for l in Path('graphify-out/.graphify_must_ids.txt').read_text(encoding='utf-8').split() if l] +ids = {n['id'] for n in d['nodes']} +missing = [m for m in must if m not in ids] +ok = len(d['nodes']) >= 0.9 * len(must) and not missing +print(f'nodes={len(d[\"nodes\"])} baseline={len(must)} missing={len(missing)}') +if missing: print('MISSING:', ' '.join(missing[:20])) +print('GATE:', 'PASS' if ok else 'FAIL') +" +``` + +On **FAIL**, do not merge: re-dispatch the extraction with the same mandatory list and a +sharper completeness instruction naming the missing IDs. Two consecutive failures — stop +and report to the user rather than merging a regressed graph. + +**The `to_json` shrink guard (#479) stays authoritative.** It is the backstop, not a +nuisance. Never pass `force=True` to clear it until you have diffed the old and new node +sets and can name why each removed node is legitimately gone (file deleted, section +removed, ID renamed with a verified replacement). ID churn on a re-extracted file is a +legitimate shrink; a missing section is not. If no new files exist (only deletions), create an empty extraction so the merge step can prune: diff --git a/graphify/skills/codex/references/update.md b/graphify/skills/codex/references/update.md index 3632fd412..b5b16a731 100644 --- a/graphify/skills/codex/references/update.md +++ b/graphify/skills/codex/references/update.md @@ -65,6 +65,71 @@ If `code_only` is True: print `[graphify update] Code-only changes detected - sk If `code_only` is False (any changed file is a doc/paper/image/video): **first, if any changed file is in `new_files['video']`, run `references/transcribe.md` (Step 2.5) on those files, then rewrite `.graphify_detect.json` to move the resulting transcript paths into `files['document']` and drop `files['video']`** — otherwise raw `.mp4/.mp3` paths are fed to semantic subagents as unreadable media (#1392). Then run the full Steps 3A–3C pipeline as normal. +### REQUIRED: mandatory-ID gate for re-extracted docs + +Semantic re-extraction of an already-graphed doc is non-deterministic: the same file can +yield 75 nodes on one run and 49 on the next, dropping whole sections with no error. The +loss lands in `build_merge` and only surfaces later as a question the graph can no longer +answer. Apply this gate to **every changed doc/paper that already has nodes in +`graph.json`**. Do not skip it for a small edit — a one-line change re-extracts the whole +file. + +**1 — Snapshot the file's existing node IDs before extracting.** + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +TARGET = 'DOC_PATH' # substitute: the changed doc, as it appears in source_file +g = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) +ids = sorted(n['id'] for n in g['nodes'] if str(n.get('source_file','')).endswith(TARGET)) +Path('graphify-out/.graphify_must_ids.txt').write_text('\n'.join(ids), encoding='utf-8') +print(f'baseline: {len(ids)} existing node(s) for {TARGET}') +" +``` + +A count of 0 means the file is new — skip the gate and extract normally. + +**2 — Pass the IDs to the subagent as mandatory.** Append to the prompt from +`references/extraction-spec.md`: + +- the baseline count as an explicit target ("the prior build extracted N nodes from this + file; land at approximately N — under 90% of N means you under-extracted, go back and + cover the sections you skimmed"), +- the full ID list verbatim, labelled **MANDATORY — every one of these existed in the + prior graph and MUST appear in your output, reusing the ID verbatim**, +- an instruction to verify every mandatory ID is present *before* writing CHUNK_PATH. + +Reusing the IDs also suppresses gratuitous ID churn on re-extraction, which orphans saved +queries and inflates the merge diff for no semantic gain. + +**3 — Hard-gate the merge.** After the chunk lands and before `build_merge`, require both: +node count ≥ 90% of baseline, and zero mandatory IDs missing. + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +d = json.loads(Path('graphify-out/.graphify_chunk_01.json').read_text(encoding='utf-8')) +must = [l for l in Path('graphify-out/.graphify_must_ids.txt').read_text(encoding='utf-8').split() if l] +ids = {n['id'] for n in d['nodes']} +missing = [m for m in must if m not in ids] +ok = len(d['nodes']) >= 0.9 * len(must) and not missing +print(f'nodes={len(d[\"nodes\"])} baseline={len(must)} missing={len(missing)}') +if missing: print('MISSING:', ' '.join(missing[:20])) +print('GATE:', 'PASS' if ok else 'FAIL') +" +``` + +On **FAIL**, do not merge: re-dispatch the extraction with the same mandatory list and a +sharper completeness instruction naming the missing IDs. Two consecutive failures — stop +and report to the user rather than merging a regressed graph. + +**The `to_json` shrink guard (#479) stays authoritative.** It is the backstop, not a +nuisance. Never pass `force=True` to clear it until you have diffed the old and new node +sets and can name why each removed node is legitimately gone (file deleted, section +removed, ID renamed with a verified replacement). ID churn on a re-extracted file is a +legitimate shrink; a missing section is not. If no new files exist (only deletions), create an empty extraction so the merge step can prune: diff --git a/graphify/skills/copilot/references/update.md b/graphify/skills/copilot/references/update.md index 3632fd412..b5b16a731 100644 --- a/graphify/skills/copilot/references/update.md +++ b/graphify/skills/copilot/references/update.md @@ -65,6 +65,71 @@ If `code_only` is True: print `[graphify update] Code-only changes detected - sk If `code_only` is False (any changed file is a doc/paper/image/video): **first, if any changed file is in `new_files['video']`, run `references/transcribe.md` (Step 2.5) on those files, then rewrite `.graphify_detect.json` to move the resulting transcript paths into `files['document']` and drop `files['video']`** — otherwise raw `.mp4/.mp3` paths are fed to semantic subagents as unreadable media (#1392). Then run the full Steps 3A–3C pipeline as normal. +### REQUIRED: mandatory-ID gate for re-extracted docs + +Semantic re-extraction of an already-graphed doc is non-deterministic: the same file can +yield 75 nodes on one run and 49 on the next, dropping whole sections with no error. The +loss lands in `build_merge` and only surfaces later as a question the graph can no longer +answer. Apply this gate to **every changed doc/paper that already has nodes in +`graph.json`**. Do not skip it for a small edit — a one-line change re-extracts the whole +file. + +**1 — Snapshot the file's existing node IDs before extracting.** + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +TARGET = 'DOC_PATH' # substitute: the changed doc, as it appears in source_file +g = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) +ids = sorted(n['id'] for n in g['nodes'] if str(n.get('source_file','')).endswith(TARGET)) +Path('graphify-out/.graphify_must_ids.txt').write_text('\n'.join(ids), encoding='utf-8') +print(f'baseline: {len(ids)} existing node(s) for {TARGET}') +" +``` + +A count of 0 means the file is new — skip the gate and extract normally. + +**2 — Pass the IDs to the subagent as mandatory.** Append to the prompt from +`references/extraction-spec.md`: + +- the baseline count as an explicit target ("the prior build extracted N nodes from this + file; land at approximately N — under 90% of N means you under-extracted, go back and + cover the sections you skimmed"), +- the full ID list verbatim, labelled **MANDATORY — every one of these existed in the + prior graph and MUST appear in your output, reusing the ID verbatim**, +- an instruction to verify every mandatory ID is present *before* writing CHUNK_PATH. + +Reusing the IDs also suppresses gratuitous ID churn on re-extraction, which orphans saved +queries and inflates the merge diff for no semantic gain. + +**3 — Hard-gate the merge.** After the chunk lands and before `build_merge`, require both: +node count ≥ 90% of baseline, and zero mandatory IDs missing. + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +d = json.loads(Path('graphify-out/.graphify_chunk_01.json').read_text(encoding='utf-8')) +must = [l for l in Path('graphify-out/.graphify_must_ids.txt').read_text(encoding='utf-8').split() if l] +ids = {n['id'] for n in d['nodes']} +missing = [m for m in must if m not in ids] +ok = len(d['nodes']) >= 0.9 * len(must) and not missing +print(f'nodes={len(d[\"nodes\"])} baseline={len(must)} missing={len(missing)}') +if missing: print('MISSING:', ' '.join(missing[:20])) +print('GATE:', 'PASS' if ok else 'FAIL') +" +``` + +On **FAIL**, do not merge: re-dispatch the extraction with the same mandatory list and a +sharper completeness instruction naming the missing IDs. Two consecutive failures — stop +and report to the user rather than merging a regressed graph. + +**The `to_json` shrink guard (#479) stays authoritative.** It is the backstop, not a +nuisance. Never pass `force=True` to clear it until you have diffed the old and new node +sets and can name why each removed node is legitimately gone (file deleted, section +removed, ID renamed with a verified replacement). ID churn on a re-extracted file is a +legitimate shrink; a missing section is not. If no new files exist (only deletions), create an empty extraction so the merge step can prune: diff --git a/graphify/skills/droid/references/update.md b/graphify/skills/droid/references/update.md index 3632fd412..b5b16a731 100644 --- a/graphify/skills/droid/references/update.md +++ b/graphify/skills/droid/references/update.md @@ -65,6 +65,71 @@ If `code_only` is True: print `[graphify update] Code-only changes detected - sk If `code_only` is False (any changed file is a doc/paper/image/video): **first, if any changed file is in `new_files['video']`, run `references/transcribe.md` (Step 2.5) on those files, then rewrite `.graphify_detect.json` to move the resulting transcript paths into `files['document']` and drop `files['video']`** — otherwise raw `.mp4/.mp3` paths are fed to semantic subagents as unreadable media (#1392). Then run the full Steps 3A–3C pipeline as normal. +### REQUIRED: mandatory-ID gate for re-extracted docs + +Semantic re-extraction of an already-graphed doc is non-deterministic: the same file can +yield 75 nodes on one run and 49 on the next, dropping whole sections with no error. The +loss lands in `build_merge` and only surfaces later as a question the graph can no longer +answer. Apply this gate to **every changed doc/paper that already has nodes in +`graph.json`**. Do not skip it for a small edit — a one-line change re-extracts the whole +file. + +**1 — Snapshot the file's existing node IDs before extracting.** + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +TARGET = 'DOC_PATH' # substitute: the changed doc, as it appears in source_file +g = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) +ids = sorted(n['id'] for n in g['nodes'] if str(n.get('source_file','')).endswith(TARGET)) +Path('graphify-out/.graphify_must_ids.txt').write_text('\n'.join(ids), encoding='utf-8') +print(f'baseline: {len(ids)} existing node(s) for {TARGET}') +" +``` + +A count of 0 means the file is new — skip the gate and extract normally. + +**2 — Pass the IDs to the subagent as mandatory.** Append to the prompt from +`references/extraction-spec.md`: + +- the baseline count as an explicit target ("the prior build extracted N nodes from this + file; land at approximately N — under 90% of N means you under-extracted, go back and + cover the sections you skimmed"), +- the full ID list verbatim, labelled **MANDATORY — every one of these existed in the + prior graph and MUST appear in your output, reusing the ID verbatim**, +- an instruction to verify every mandatory ID is present *before* writing CHUNK_PATH. + +Reusing the IDs also suppresses gratuitous ID churn on re-extraction, which orphans saved +queries and inflates the merge diff for no semantic gain. + +**3 — Hard-gate the merge.** After the chunk lands and before `build_merge`, require both: +node count ≥ 90% of baseline, and zero mandatory IDs missing. + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +d = json.loads(Path('graphify-out/.graphify_chunk_01.json').read_text(encoding='utf-8')) +must = [l for l in Path('graphify-out/.graphify_must_ids.txt').read_text(encoding='utf-8').split() if l] +ids = {n['id'] for n in d['nodes']} +missing = [m for m in must if m not in ids] +ok = len(d['nodes']) >= 0.9 * len(must) and not missing +print(f'nodes={len(d[\"nodes\"])} baseline={len(must)} missing={len(missing)}') +if missing: print('MISSING:', ' '.join(missing[:20])) +print('GATE:', 'PASS' if ok else 'FAIL') +" +``` + +On **FAIL**, do not merge: re-dispatch the extraction with the same mandatory list and a +sharper completeness instruction naming the missing IDs. Two consecutive failures — stop +and report to the user rather than merging a regressed graph. + +**The `to_json` shrink guard (#479) stays authoritative.** It is the backstop, not a +nuisance. Never pass `force=True` to clear it until you have diffed the old and new node +sets and can name why each removed node is legitimately gone (file deleted, section +removed, ID renamed with a verified replacement). ID churn on a re-extracted file is a +legitimate shrink; a missing section is not. If no new files exist (only deletions), create an empty extraction so the merge step can prune: diff --git a/graphify/skills/kilo/references/update.md b/graphify/skills/kilo/references/update.md index 3632fd412..b5b16a731 100644 --- a/graphify/skills/kilo/references/update.md +++ b/graphify/skills/kilo/references/update.md @@ -65,6 +65,71 @@ If `code_only` is True: print `[graphify update] Code-only changes detected - sk If `code_only` is False (any changed file is a doc/paper/image/video): **first, if any changed file is in `new_files['video']`, run `references/transcribe.md` (Step 2.5) on those files, then rewrite `.graphify_detect.json` to move the resulting transcript paths into `files['document']` and drop `files['video']`** — otherwise raw `.mp4/.mp3` paths are fed to semantic subagents as unreadable media (#1392). Then run the full Steps 3A–3C pipeline as normal. +### REQUIRED: mandatory-ID gate for re-extracted docs + +Semantic re-extraction of an already-graphed doc is non-deterministic: the same file can +yield 75 nodes on one run and 49 on the next, dropping whole sections with no error. The +loss lands in `build_merge` and only surfaces later as a question the graph can no longer +answer. Apply this gate to **every changed doc/paper that already has nodes in +`graph.json`**. Do not skip it for a small edit — a one-line change re-extracts the whole +file. + +**1 — Snapshot the file's existing node IDs before extracting.** + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +TARGET = 'DOC_PATH' # substitute: the changed doc, as it appears in source_file +g = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) +ids = sorted(n['id'] for n in g['nodes'] if str(n.get('source_file','')).endswith(TARGET)) +Path('graphify-out/.graphify_must_ids.txt').write_text('\n'.join(ids), encoding='utf-8') +print(f'baseline: {len(ids)} existing node(s) for {TARGET}') +" +``` + +A count of 0 means the file is new — skip the gate and extract normally. + +**2 — Pass the IDs to the subagent as mandatory.** Append to the prompt from +`references/extraction-spec.md`: + +- the baseline count as an explicit target ("the prior build extracted N nodes from this + file; land at approximately N — under 90% of N means you under-extracted, go back and + cover the sections you skimmed"), +- the full ID list verbatim, labelled **MANDATORY — every one of these existed in the + prior graph and MUST appear in your output, reusing the ID verbatim**, +- an instruction to verify every mandatory ID is present *before* writing CHUNK_PATH. + +Reusing the IDs also suppresses gratuitous ID churn on re-extraction, which orphans saved +queries and inflates the merge diff for no semantic gain. + +**3 — Hard-gate the merge.** After the chunk lands and before `build_merge`, require both: +node count ≥ 90% of baseline, and zero mandatory IDs missing. + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +d = json.loads(Path('graphify-out/.graphify_chunk_01.json').read_text(encoding='utf-8')) +must = [l for l in Path('graphify-out/.graphify_must_ids.txt').read_text(encoding='utf-8').split() if l] +ids = {n['id'] for n in d['nodes']} +missing = [m for m in must if m not in ids] +ok = len(d['nodes']) >= 0.9 * len(must) and not missing +print(f'nodes={len(d[\"nodes\"])} baseline={len(must)} missing={len(missing)}') +if missing: print('MISSING:', ' '.join(missing[:20])) +print('GATE:', 'PASS' if ok else 'FAIL') +" +``` + +On **FAIL**, do not merge: re-dispatch the extraction with the same mandatory list and a +sharper completeness instruction naming the missing IDs. Two consecutive failures — stop +and report to the user rather than merging a regressed graph. + +**The `to_json` shrink guard (#479) stays authoritative.** It is the backstop, not a +nuisance. Never pass `force=True` to clear it until you have diffed the old and new node +sets and can name why each removed node is legitimately gone (file deleted, section +removed, ID renamed with a verified replacement). ID churn on a re-extracted file is a +legitimate shrink; a missing section is not. If no new files exist (only deletions), create an empty extraction so the merge step can prune: diff --git a/graphify/skills/kiro/references/update.md b/graphify/skills/kiro/references/update.md index 3632fd412..b5b16a731 100644 --- a/graphify/skills/kiro/references/update.md +++ b/graphify/skills/kiro/references/update.md @@ -65,6 +65,71 @@ If `code_only` is True: print `[graphify update] Code-only changes detected - sk If `code_only` is False (any changed file is a doc/paper/image/video): **first, if any changed file is in `new_files['video']`, run `references/transcribe.md` (Step 2.5) on those files, then rewrite `.graphify_detect.json` to move the resulting transcript paths into `files['document']` and drop `files['video']`** — otherwise raw `.mp4/.mp3` paths are fed to semantic subagents as unreadable media (#1392). Then run the full Steps 3A–3C pipeline as normal. +### REQUIRED: mandatory-ID gate for re-extracted docs + +Semantic re-extraction of an already-graphed doc is non-deterministic: the same file can +yield 75 nodes on one run and 49 on the next, dropping whole sections with no error. The +loss lands in `build_merge` and only surfaces later as a question the graph can no longer +answer. Apply this gate to **every changed doc/paper that already has nodes in +`graph.json`**. Do not skip it for a small edit — a one-line change re-extracts the whole +file. + +**1 — Snapshot the file's existing node IDs before extracting.** + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +TARGET = 'DOC_PATH' # substitute: the changed doc, as it appears in source_file +g = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) +ids = sorted(n['id'] for n in g['nodes'] if str(n.get('source_file','')).endswith(TARGET)) +Path('graphify-out/.graphify_must_ids.txt').write_text('\n'.join(ids), encoding='utf-8') +print(f'baseline: {len(ids)} existing node(s) for {TARGET}') +" +``` + +A count of 0 means the file is new — skip the gate and extract normally. + +**2 — Pass the IDs to the subagent as mandatory.** Append to the prompt from +`references/extraction-spec.md`: + +- the baseline count as an explicit target ("the prior build extracted N nodes from this + file; land at approximately N — under 90% of N means you under-extracted, go back and + cover the sections you skimmed"), +- the full ID list verbatim, labelled **MANDATORY — every one of these existed in the + prior graph and MUST appear in your output, reusing the ID verbatim**, +- an instruction to verify every mandatory ID is present *before* writing CHUNK_PATH. + +Reusing the IDs also suppresses gratuitous ID churn on re-extraction, which orphans saved +queries and inflates the merge diff for no semantic gain. + +**3 — Hard-gate the merge.** After the chunk lands and before `build_merge`, require both: +node count ≥ 90% of baseline, and zero mandatory IDs missing. + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +d = json.loads(Path('graphify-out/.graphify_chunk_01.json').read_text(encoding='utf-8')) +must = [l for l in Path('graphify-out/.graphify_must_ids.txt').read_text(encoding='utf-8').split() if l] +ids = {n['id'] for n in d['nodes']} +missing = [m for m in must if m not in ids] +ok = len(d['nodes']) >= 0.9 * len(must) and not missing +print(f'nodes={len(d[\"nodes\"])} baseline={len(must)} missing={len(missing)}') +if missing: print('MISSING:', ' '.join(missing[:20])) +print('GATE:', 'PASS' if ok else 'FAIL') +" +``` + +On **FAIL**, do not merge: re-dispatch the extraction with the same mandatory list and a +sharper completeness instruction naming the missing IDs. Two consecutive failures — stop +and report to the user rather than merging a regressed graph. + +**The `to_json` shrink guard (#479) stays authoritative.** It is the backstop, not a +nuisance. Never pass `force=True` to clear it until you have diffed the old and new node +sets and can name why each removed node is legitimately gone (file deleted, section +removed, ID renamed with a verified replacement). ID churn on a re-extracted file is a +legitimate shrink; a missing section is not. If no new files exist (only deletions), create an empty extraction so the merge step can prune: diff --git a/graphify/skills/opencode/references/update.md b/graphify/skills/opencode/references/update.md index 3632fd412..b5b16a731 100644 --- a/graphify/skills/opencode/references/update.md +++ b/graphify/skills/opencode/references/update.md @@ -65,6 +65,71 @@ If `code_only` is True: print `[graphify update] Code-only changes detected - sk If `code_only` is False (any changed file is a doc/paper/image/video): **first, if any changed file is in `new_files['video']`, run `references/transcribe.md` (Step 2.5) on those files, then rewrite `.graphify_detect.json` to move the resulting transcript paths into `files['document']` and drop `files['video']`** — otherwise raw `.mp4/.mp3` paths are fed to semantic subagents as unreadable media (#1392). Then run the full Steps 3A–3C pipeline as normal. +### REQUIRED: mandatory-ID gate for re-extracted docs + +Semantic re-extraction of an already-graphed doc is non-deterministic: the same file can +yield 75 nodes on one run and 49 on the next, dropping whole sections with no error. The +loss lands in `build_merge` and only surfaces later as a question the graph can no longer +answer. Apply this gate to **every changed doc/paper that already has nodes in +`graph.json`**. Do not skip it for a small edit — a one-line change re-extracts the whole +file. + +**1 — Snapshot the file's existing node IDs before extracting.** + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +TARGET = 'DOC_PATH' # substitute: the changed doc, as it appears in source_file +g = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) +ids = sorted(n['id'] for n in g['nodes'] if str(n.get('source_file','')).endswith(TARGET)) +Path('graphify-out/.graphify_must_ids.txt').write_text('\n'.join(ids), encoding='utf-8') +print(f'baseline: {len(ids)} existing node(s) for {TARGET}') +" +``` + +A count of 0 means the file is new — skip the gate and extract normally. + +**2 — Pass the IDs to the subagent as mandatory.** Append to the prompt from +`references/extraction-spec.md`: + +- the baseline count as an explicit target ("the prior build extracted N nodes from this + file; land at approximately N — under 90% of N means you under-extracted, go back and + cover the sections you skimmed"), +- the full ID list verbatim, labelled **MANDATORY — every one of these existed in the + prior graph and MUST appear in your output, reusing the ID verbatim**, +- an instruction to verify every mandatory ID is present *before* writing CHUNK_PATH. + +Reusing the IDs also suppresses gratuitous ID churn on re-extraction, which orphans saved +queries and inflates the merge diff for no semantic gain. + +**3 — Hard-gate the merge.** After the chunk lands and before `build_merge`, require both: +node count ≥ 90% of baseline, and zero mandatory IDs missing. + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +d = json.loads(Path('graphify-out/.graphify_chunk_01.json').read_text(encoding='utf-8')) +must = [l for l in Path('graphify-out/.graphify_must_ids.txt').read_text(encoding='utf-8').split() if l] +ids = {n['id'] for n in d['nodes']} +missing = [m for m in must if m not in ids] +ok = len(d['nodes']) >= 0.9 * len(must) and not missing +print(f'nodes={len(d[\"nodes\"])} baseline={len(must)} missing={len(missing)}') +if missing: print('MISSING:', ' '.join(missing[:20])) +print('GATE:', 'PASS' if ok else 'FAIL') +" +``` + +On **FAIL**, do not merge: re-dispatch the extraction with the same mandatory list and a +sharper completeness instruction naming the missing IDs. Two consecutive failures — stop +and report to the user rather than merging a regressed graph. + +**The `to_json` shrink guard (#479) stays authoritative.** It is the backstop, not a +nuisance. Never pass `force=True` to clear it until you have diffed the old and new node +sets and can name why each removed node is legitimately gone (file deleted, section +removed, ID renamed with a verified replacement). ID churn on a re-extracted file is a +legitimate shrink; a missing section is not. If no new files exist (only deletions), create an empty extraction so the merge step can prune: diff --git a/graphify/skills/pi/references/update.md b/graphify/skills/pi/references/update.md index 3632fd412..b5b16a731 100644 --- a/graphify/skills/pi/references/update.md +++ b/graphify/skills/pi/references/update.md @@ -65,6 +65,71 @@ If `code_only` is True: print `[graphify update] Code-only changes detected - sk If `code_only` is False (any changed file is a doc/paper/image/video): **first, if any changed file is in `new_files['video']`, run `references/transcribe.md` (Step 2.5) on those files, then rewrite `.graphify_detect.json` to move the resulting transcript paths into `files['document']` and drop `files['video']`** — otherwise raw `.mp4/.mp3` paths are fed to semantic subagents as unreadable media (#1392). Then run the full Steps 3A–3C pipeline as normal. +### REQUIRED: mandatory-ID gate for re-extracted docs + +Semantic re-extraction of an already-graphed doc is non-deterministic: the same file can +yield 75 nodes on one run and 49 on the next, dropping whole sections with no error. The +loss lands in `build_merge` and only surfaces later as a question the graph can no longer +answer. Apply this gate to **every changed doc/paper that already has nodes in +`graph.json`**. Do not skip it for a small edit — a one-line change re-extracts the whole +file. + +**1 — Snapshot the file's existing node IDs before extracting.** + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +TARGET = 'DOC_PATH' # substitute: the changed doc, as it appears in source_file +g = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) +ids = sorted(n['id'] for n in g['nodes'] if str(n.get('source_file','')).endswith(TARGET)) +Path('graphify-out/.graphify_must_ids.txt').write_text('\n'.join(ids), encoding='utf-8') +print(f'baseline: {len(ids)} existing node(s) for {TARGET}') +" +``` + +A count of 0 means the file is new — skip the gate and extract normally. + +**2 — Pass the IDs to the subagent as mandatory.** Append to the prompt from +`references/extraction-spec.md`: + +- the baseline count as an explicit target ("the prior build extracted N nodes from this + file; land at approximately N — under 90% of N means you under-extracted, go back and + cover the sections you skimmed"), +- the full ID list verbatim, labelled **MANDATORY — every one of these existed in the + prior graph and MUST appear in your output, reusing the ID verbatim**, +- an instruction to verify every mandatory ID is present *before* writing CHUNK_PATH. + +Reusing the IDs also suppresses gratuitous ID churn on re-extraction, which orphans saved +queries and inflates the merge diff for no semantic gain. + +**3 — Hard-gate the merge.** After the chunk lands and before `build_merge`, require both: +node count ≥ 90% of baseline, and zero mandatory IDs missing. + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +d = json.loads(Path('graphify-out/.graphify_chunk_01.json').read_text(encoding='utf-8')) +must = [l for l in Path('graphify-out/.graphify_must_ids.txt').read_text(encoding='utf-8').split() if l] +ids = {n['id'] for n in d['nodes']} +missing = [m for m in must if m not in ids] +ok = len(d['nodes']) >= 0.9 * len(must) and not missing +print(f'nodes={len(d[\"nodes\"])} baseline={len(must)} missing={len(missing)}') +if missing: print('MISSING:', ' '.join(missing[:20])) +print('GATE:', 'PASS' if ok else 'FAIL') +" +``` + +On **FAIL**, do not merge: re-dispatch the extraction with the same mandatory list and a +sharper completeness instruction naming the missing IDs. Two consecutive failures — stop +and report to the user rather than merging a regressed graph. + +**The `to_json` shrink guard (#479) stays authoritative.** It is the backstop, not a +nuisance. Never pass `force=True` to clear it until you have diffed the old and new node +sets and can name why each removed node is legitimately gone (file deleted, section +removed, ID renamed with a verified replacement). ID churn on a re-extracted file is a +legitimate shrink; a missing section is not. If no new files exist (only deletions), create an empty extraction so the merge step can prune: diff --git a/graphify/skills/trae/references/update.md b/graphify/skills/trae/references/update.md index 3632fd412..b5b16a731 100644 --- a/graphify/skills/trae/references/update.md +++ b/graphify/skills/trae/references/update.md @@ -65,6 +65,71 @@ If `code_only` is True: print `[graphify update] Code-only changes detected - sk If `code_only` is False (any changed file is a doc/paper/image/video): **first, if any changed file is in `new_files['video']`, run `references/transcribe.md` (Step 2.5) on those files, then rewrite `.graphify_detect.json` to move the resulting transcript paths into `files['document']` and drop `files['video']`** — otherwise raw `.mp4/.mp3` paths are fed to semantic subagents as unreadable media (#1392). Then run the full Steps 3A–3C pipeline as normal. +### REQUIRED: mandatory-ID gate for re-extracted docs + +Semantic re-extraction of an already-graphed doc is non-deterministic: the same file can +yield 75 nodes on one run and 49 on the next, dropping whole sections with no error. The +loss lands in `build_merge` and only surfaces later as a question the graph can no longer +answer. Apply this gate to **every changed doc/paper that already has nodes in +`graph.json`**. Do not skip it for a small edit — a one-line change re-extracts the whole +file. + +**1 — Snapshot the file's existing node IDs before extracting.** + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +TARGET = 'DOC_PATH' # substitute: the changed doc, as it appears in source_file +g = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) +ids = sorted(n['id'] for n in g['nodes'] if str(n.get('source_file','')).endswith(TARGET)) +Path('graphify-out/.graphify_must_ids.txt').write_text('\n'.join(ids), encoding='utf-8') +print(f'baseline: {len(ids)} existing node(s) for {TARGET}') +" +``` + +A count of 0 means the file is new — skip the gate and extract normally. + +**2 — Pass the IDs to the subagent as mandatory.** Append to the prompt from +`references/extraction-spec.md`: + +- the baseline count as an explicit target ("the prior build extracted N nodes from this + file; land at approximately N — under 90% of N means you under-extracted, go back and + cover the sections you skimmed"), +- the full ID list verbatim, labelled **MANDATORY — every one of these existed in the + prior graph and MUST appear in your output, reusing the ID verbatim**, +- an instruction to verify every mandatory ID is present *before* writing CHUNK_PATH. + +Reusing the IDs also suppresses gratuitous ID churn on re-extraction, which orphans saved +queries and inflates the merge diff for no semantic gain. + +**3 — Hard-gate the merge.** After the chunk lands and before `build_merge`, require both: +node count ≥ 90% of baseline, and zero mandatory IDs missing. + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +d = json.loads(Path('graphify-out/.graphify_chunk_01.json').read_text(encoding='utf-8')) +must = [l for l in Path('graphify-out/.graphify_must_ids.txt').read_text(encoding='utf-8').split() if l] +ids = {n['id'] for n in d['nodes']} +missing = [m for m in must if m not in ids] +ok = len(d['nodes']) >= 0.9 * len(must) and not missing +print(f'nodes={len(d[\"nodes\"])} baseline={len(must)} missing={len(missing)}') +if missing: print('MISSING:', ' '.join(missing[:20])) +print('GATE:', 'PASS' if ok else 'FAIL') +" +``` + +On **FAIL**, do not merge: re-dispatch the extraction with the same mandatory list and a +sharper completeness instruction naming the missing IDs. Two consecutive failures — stop +and report to the user rather than merging a regressed graph. + +**The `to_json` shrink guard (#479) stays authoritative.** It is the backstop, not a +nuisance. Never pass `force=True` to clear it until you have diffed the old and new node +sets and can name why each removed node is legitimately gone (file deleted, section +removed, ID renamed with a verified replacement). ID churn on a re-extracted file is a +legitimate shrink; a missing section is not. If no new files exist (only deletions), create an empty extraction so the merge step can prune: diff --git a/graphify/skills/vscode/references/update.md b/graphify/skills/vscode/references/update.md index 3632fd412..b5b16a731 100644 --- a/graphify/skills/vscode/references/update.md +++ b/graphify/skills/vscode/references/update.md @@ -65,6 +65,71 @@ If `code_only` is True: print `[graphify update] Code-only changes detected - sk If `code_only` is False (any changed file is a doc/paper/image/video): **first, if any changed file is in `new_files['video']`, run `references/transcribe.md` (Step 2.5) on those files, then rewrite `.graphify_detect.json` to move the resulting transcript paths into `files['document']` and drop `files['video']`** — otherwise raw `.mp4/.mp3` paths are fed to semantic subagents as unreadable media (#1392). Then run the full Steps 3A–3C pipeline as normal. +### REQUIRED: mandatory-ID gate for re-extracted docs + +Semantic re-extraction of an already-graphed doc is non-deterministic: the same file can +yield 75 nodes on one run and 49 on the next, dropping whole sections with no error. The +loss lands in `build_merge` and only surfaces later as a question the graph can no longer +answer. Apply this gate to **every changed doc/paper that already has nodes in +`graph.json`**. Do not skip it for a small edit — a one-line change re-extracts the whole +file. + +**1 — Snapshot the file's existing node IDs before extracting.** + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +TARGET = 'DOC_PATH' # substitute: the changed doc, as it appears in source_file +g = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) +ids = sorted(n['id'] for n in g['nodes'] if str(n.get('source_file','')).endswith(TARGET)) +Path('graphify-out/.graphify_must_ids.txt').write_text('\n'.join(ids), encoding='utf-8') +print(f'baseline: {len(ids)} existing node(s) for {TARGET}') +" +``` + +A count of 0 means the file is new — skip the gate and extract normally. + +**2 — Pass the IDs to the subagent as mandatory.** Append to the prompt from +`references/extraction-spec.md`: + +- the baseline count as an explicit target ("the prior build extracted N nodes from this + file; land at approximately N — under 90% of N means you under-extracted, go back and + cover the sections you skimmed"), +- the full ID list verbatim, labelled **MANDATORY — every one of these existed in the + prior graph and MUST appear in your output, reusing the ID verbatim**, +- an instruction to verify every mandatory ID is present *before* writing CHUNK_PATH. + +Reusing the IDs also suppresses gratuitous ID churn on re-extraction, which orphans saved +queries and inflates the merge diff for no semantic gain. + +**3 — Hard-gate the merge.** After the chunk lands and before `build_merge`, require both: +node count ≥ 90% of baseline, and zero mandatory IDs missing. + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +d = json.loads(Path('graphify-out/.graphify_chunk_01.json').read_text(encoding='utf-8')) +must = [l for l in Path('graphify-out/.graphify_must_ids.txt').read_text(encoding='utf-8').split() if l] +ids = {n['id'] for n in d['nodes']} +missing = [m for m in must if m not in ids] +ok = len(d['nodes']) >= 0.9 * len(must) and not missing +print(f'nodes={len(d[\"nodes\"])} baseline={len(must)} missing={len(missing)}') +if missing: print('MISSING:', ' '.join(missing[:20])) +print('GATE:', 'PASS' if ok else 'FAIL') +" +``` + +On **FAIL**, do not merge: re-dispatch the extraction with the same mandatory list and a +sharper completeness instruction naming the missing IDs. Two consecutive failures — stop +and report to the user rather than merging a regressed graph. + +**The `to_json` shrink guard (#479) stays authoritative.** It is the backstop, not a +nuisance. Never pass `force=True` to clear it until you have diffed the old and new node +sets and can name why each removed node is legitimately gone (file deleted, section +removed, ID renamed with a verified replacement). ID churn on a re-extracted file is a +legitimate shrink; a missing section is not. If no new files exist (only deletions), create an empty extraction so the merge step can prune: diff --git a/graphify/skills/windows/references/update.md b/graphify/skills/windows/references/update.md index 3632fd412..b5b16a731 100644 --- a/graphify/skills/windows/references/update.md +++ b/graphify/skills/windows/references/update.md @@ -65,6 +65,71 @@ If `code_only` is True: print `[graphify update] Code-only changes detected - sk If `code_only` is False (any changed file is a doc/paper/image/video): **first, if any changed file is in `new_files['video']`, run `references/transcribe.md` (Step 2.5) on those files, then rewrite `.graphify_detect.json` to move the resulting transcript paths into `files['document']` and drop `files['video']`** — otherwise raw `.mp4/.mp3` paths are fed to semantic subagents as unreadable media (#1392). Then run the full Steps 3A–3C pipeline as normal. +### REQUIRED: mandatory-ID gate for re-extracted docs + +Semantic re-extraction of an already-graphed doc is non-deterministic: the same file can +yield 75 nodes on one run and 49 on the next, dropping whole sections with no error. The +loss lands in `build_merge` and only surfaces later as a question the graph can no longer +answer. Apply this gate to **every changed doc/paper that already has nodes in +`graph.json`**. Do not skip it for a small edit — a one-line change re-extracts the whole +file. + +**1 — Snapshot the file's existing node IDs before extracting.** + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +TARGET = 'DOC_PATH' # substitute: the changed doc, as it appears in source_file +g = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) +ids = sorted(n['id'] for n in g['nodes'] if str(n.get('source_file','')).endswith(TARGET)) +Path('graphify-out/.graphify_must_ids.txt').write_text('\n'.join(ids), encoding='utf-8') +print(f'baseline: {len(ids)} existing node(s) for {TARGET}') +" +``` + +A count of 0 means the file is new — skip the gate and extract normally. + +**2 — Pass the IDs to the subagent as mandatory.** Append to the prompt from +`references/extraction-spec.md`: + +- the baseline count as an explicit target ("the prior build extracted N nodes from this + file; land at approximately N — under 90% of N means you under-extracted, go back and + cover the sections you skimmed"), +- the full ID list verbatim, labelled **MANDATORY — every one of these existed in the + prior graph and MUST appear in your output, reusing the ID verbatim**, +- an instruction to verify every mandatory ID is present *before* writing CHUNK_PATH. + +Reusing the IDs also suppresses gratuitous ID churn on re-extraction, which orphans saved +queries and inflates the merge diff for no semantic gain. + +**3 — Hard-gate the merge.** After the chunk lands and before `build_merge`, require both: +node count ≥ 90% of baseline, and zero mandatory IDs missing. + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +d = json.loads(Path('graphify-out/.graphify_chunk_01.json').read_text(encoding='utf-8')) +must = [l for l in Path('graphify-out/.graphify_must_ids.txt').read_text(encoding='utf-8').split() if l] +ids = {n['id'] for n in d['nodes']} +missing = [m for m in must if m not in ids] +ok = len(d['nodes']) >= 0.9 * len(must) and not missing +print(f'nodes={len(d[\"nodes\"])} baseline={len(must)} missing={len(missing)}') +if missing: print('MISSING:', ' '.join(missing[:20])) +print('GATE:', 'PASS' if ok else 'FAIL') +" +``` + +On **FAIL**, do not merge: re-dispatch the extraction with the same mandatory list and a +sharper completeness instruction naming the missing IDs. Two consecutive failures — stop +and report to the user rather than merging a regressed graph. + +**The `to_json` shrink guard (#479) stays authoritative.** It is the backstop, not a +nuisance. Never pass `force=True` to clear it until you have diffed the old and new node +sets and can name why each removed node is legitimately gone (file deleted, section +removed, ID renamed with a verified replacement). ID churn on a re-extracted file is a +legitimate shrink; a missing section is not. If no new files exist (only deletions), create an empty extraction so the merge step can prune: diff --git a/tools/skillgen/expected/graphify__skill-agents.md b/tools/skillgen/expected/graphify__skill-agents.md index afb4ecc12..1732e13b3 100644 --- a/tools/skillgen/expected/graphify__skill-agents.md +++ b/tools/skillgen/expected/graphify__skill-agents.md @@ -664,6 +664,8 @@ fi Both are non-default subcommands. `--update` re-extracts only new or changed files; `--cluster-only` reruns clustering on the existing graph. See `references/update.md` for both flows. +> **Re-extracting an already-graphed doc requires the mandatory-ID gate** (`references/update.md` → "REQUIRED: mandatory-ID gate for re-extracted docs"): snapshot that file's existing node IDs, pass them to the subagent as must-include, and refuse the merge if the node count drops below 90% of baseline or any ID is missing. Semantic re-extraction is non-deterministic and drops whole sections without it. + --- ## For /graphify query diff --git a/tools/skillgen/expected/graphify__skill-amp.md b/tools/skillgen/expected/graphify__skill-amp.md index afb4ecc12..1732e13b3 100644 --- a/tools/skillgen/expected/graphify__skill-amp.md +++ b/tools/skillgen/expected/graphify__skill-amp.md @@ -664,6 +664,8 @@ fi Both are non-default subcommands. `--update` re-extracts only new or changed files; `--cluster-only` reruns clustering on the existing graph. See `references/update.md` for both flows. +> **Re-extracting an already-graphed doc requires the mandatory-ID gate** (`references/update.md` → "REQUIRED: mandatory-ID gate for re-extracted docs"): snapshot that file's existing node IDs, pass them to the subagent as must-include, and refuse the merge if the node count drops below 90% of baseline or any ID is missing. Semantic re-extraction is non-deterministic and drops whole sections without it. + --- ## For /graphify query diff --git a/tools/skillgen/expected/graphify__skill-claw.md b/tools/skillgen/expected/graphify__skill-claw.md index d98865cc8..524d933f2 100644 --- a/tools/skillgen/expected/graphify__skill-claw.md +++ b/tools/skillgen/expected/graphify__skill-claw.md @@ -667,6 +667,8 @@ fi Both are non-default subcommands. `--update` re-extracts only new or changed files; `--cluster-only` reruns clustering on the existing graph. See `references/update.md` for both flows. +> **Re-extracting an already-graphed doc requires the mandatory-ID gate** (`references/update.md` → "REQUIRED: mandatory-ID gate for re-extracted docs"): snapshot that file's existing node IDs, pass them to the subagent as must-include, and refuse the merge if the node count drops below 90% of baseline or any ID is missing. Semantic re-extraction is non-deterministic and drops whole sections without it. + --- ## For /graphify query diff --git a/tools/skillgen/expected/graphify__skill-codex.md b/tools/skillgen/expected/graphify__skill-codex.md index 0c821a278..ac3ee9781 100644 --- a/tools/skillgen/expected/graphify__skill-codex.md +++ b/tools/skillgen/expected/graphify__skill-codex.md @@ -664,6 +664,8 @@ fi Both are non-default subcommands. `--update` re-extracts only new or changed files; `--cluster-only` reruns clustering on the existing graph. See `references/update.md` for both flows. +> **Re-extracting an already-graphed doc requires the mandatory-ID gate** (`references/update.md` → "REQUIRED: mandatory-ID gate for re-extracted docs"): snapshot that file's existing node IDs, pass them to the subagent as must-include, and refuse the merge if the node count drops below 90% of baseline or any ID is missing. Semantic re-extraction is non-deterministic and drops whole sections without it. + --- ## For /graphify query diff --git a/tools/skillgen/expected/graphify__skill-copilot.md b/tools/skillgen/expected/graphify__skill-copilot.md index d98865cc8..524d933f2 100644 --- a/tools/skillgen/expected/graphify__skill-copilot.md +++ b/tools/skillgen/expected/graphify__skill-copilot.md @@ -667,6 +667,8 @@ fi Both are non-default subcommands. `--update` re-extracts only new or changed files; `--cluster-only` reruns clustering on the existing graph. See `references/update.md` for both flows. +> **Re-extracting an already-graphed doc requires the mandatory-ID gate** (`references/update.md` → "REQUIRED: mandatory-ID gate for re-extracted docs"): snapshot that file's existing node IDs, pass them to the subagent as must-include, and refuse the merge if the node count drops below 90% of baseline or any ID is missing. Semantic re-extraction is non-deterministic and drops whole sections without it. + --- ## For /graphify query diff --git a/tools/skillgen/expected/graphify__skill-droid.md b/tools/skillgen/expected/graphify__skill-droid.md index c3815d556..8a90a9d4f 100644 --- a/tools/skillgen/expected/graphify__skill-droid.md +++ b/tools/skillgen/expected/graphify__skill-droid.md @@ -664,6 +664,8 @@ fi Both are non-default subcommands. `--update` re-extracts only new or changed files; `--cluster-only` reruns clustering on the existing graph. See `references/update.md` for both flows. +> **Re-extracting an already-graphed doc requires the mandatory-ID gate** (`references/update.md` → "REQUIRED: mandatory-ID gate for re-extracted docs"): snapshot that file's existing node IDs, pass them to the subagent as must-include, and refuse the merge if the node count drops below 90% of baseline or any ID is missing. Semantic re-extraction is non-deterministic and drops whole sections without it. + --- ## For /graphify query diff --git a/tools/skillgen/expected/graphify__skill-kilo.md b/tools/skillgen/expected/graphify__skill-kilo.md index dbb4658ca..dfca9ebb9 100644 --- a/tools/skillgen/expected/graphify__skill-kilo.md +++ b/tools/skillgen/expected/graphify__skill-kilo.md @@ -667,6 +667,8 @@ fi Both are non-default subcommands. `--update` re-extracts only new or changed files; `--cluster-only` reruns clustering on the existing graph. See `references/update.md` for both flows. +> **Re-extracting an already-graphed doc requires the mandatory-ID gate** (`references/update.md` → "REQUIRED: mandatory-ID gate for re-extracted docs"): snapshot that file's existing node IDs, pass them to the subagent as must-include, and refuse the merge if the node count drops below 90% of baseline or any ID is missing. Semantic re-extraction is non-deterministic and drops whole sections without it. + --- ## For /graphify query diff --git a/tools/skillgen/expected/graphify__skill-kiro.md b/tools/skillgen/expected/graphify__skill-kiro.md index d98865cc8..524d933f2 100644 --- a/tools/skillgen/expected/graphify__skill-kiro.md +++ b/tools/skillgen/expected/graphify__skill-kiro.md @@ -667,6 +667,8 @@ fi Both are non-default subcommands. `--update` re-extracts only new or changed files; `--cluster-only` reruns clustering on the existing graph. See `references/update.md` for both flows. +> **Re-extracting an already-graphed doc requires the mandatory-ID gate** (`references/update.md` → "REQUIRED: mandatory-ID gate for re-extracted docs"): snapshot that file's existing node IDs, pass them to the subagent as must-include, and refuse the merge if the node count drops below 90% of baseline or any ID is missing. Semantic re-extraction is non-deterministic and drops whole sections without it. + --- ## For /graphify query diff --git a/tools/skillgen/expected/graphify__skill-opencode.md b/tools/skillgen/expected/graphify__skill-opencode.md index cf5dae440..9b9f5b726 100644 --- a/tools/skillgen/expected/graphify__skill-opencode.md +++ b/tools/skillgen/expected/graphify__skill-opencode.md @@ -659,6 +659,8 @@ fi Both are non-default subcommands. `--update` re-extracts only new or changed files; `--cluster-only` reruns clustering on the existing graph. See `references/update.md` for both flows. +> **Re-extracting an already-graphed doc requires the mandatory-ID gate** (`references/update.md` → "REQUIRED: mandatory-ID gate for re-extracted docs"): snapshot that file's existing node IDs, pass them to the subagent as must-include, and refuse the merge if the node count drops below 90% of baseline or any ID is missing. Semantic re-extraction is non-deterministic and drops whole sections without it. + --- ## For /graphify query diff --git a/tools/skillgen/expected/graphify__skill-pi.md b/tools/skillgen/expected/graphify__skill-pi.md index d98865cc8..524d933f2 100644 --- a/tools/skillgen/expected/graphify__skill-pi.md +++ b/tools/skillgen/expected/graphify__skill-pi.md @@ -667,6 +667,8 @@ fi Both are non-default subcommands. `--update` re-extracts only new or changed files; `--cluster-only` reruns clustering on the existing graph. See `references/update.md` for both flows. +> **Re-extracting an already-graphed doc requires the mandatory-ID gate** (`references/update.md` → "REQUIRED: mandatory-ID gate for re-extracted docs"): snapshot that file's existing node IDs, pass them to the subagent as must-include, and refuse the merge if the node count drops below 90% of baseline or any ID is missing. Semantic re-extraction is non-deterministic and drops whole sections without it. + --- ## For /graphify query diff --git a/tools/skillgen/expected/graphify__skill-trae.md b/tools/skillgen/expected/graphify__skill-trae.md index b0cbeb122..467c3b7d8 100644 --- a/tools/skillgen/expected/graphify__skill-trae.md +++ b/tools/skillgen/expected/graphify__skill-trae.md @@ -665,6 +665,8 @@ fi Both are non-default subcommands. `--update` re-extracts only new or changed files; `--cluster-only` reruns clustering on the existing graph. See `references/update.md` for both flows. +> **Re-extracting an already-graphed doc requires the mandatory-ID gate** (`references/update.md` → "REQUIRED: mandatory-ID gate for re-extracted docs"): snapshot that file's existing node IDs, pass them to the subagent as must-include, and refuse the merge if the node count drops below 90% of baseline or any ID is missing. Semantic re-extraction is non-deterministic and drops whole sections without it. + --- ## For /graphify query diff --git a/tools/skillgen/expected/graphify__skill-vscode.md b/tools/skillgen/expected/graphify__skill-vscode.md index 3e6bc6b7b..b8c55d517 100644 --- a/tools/skillgen/expected/graphify__skill-vscode.md +++ b/tools/skillgen/expected/graphify__skill-vscode.md @@ -663,6 +663,8 @@ fi Both are non-default subcommands. `--update` re-extracts only new or changed files; `--cluster-only` reruns clustering on the existing graph. See `references/update.md` for both flows. +> **Re-extracting an already-graphed doc requires the mandatory-ID gate** (`references/update.md` → "REQUIRED: mandatory-ID gate for re-extracted docs"): snapshot that file's existing node IDs, pass them to the subagent as must-include, and refuse the merge if the node count drops below 90% of baseline or any ID is missing. Semantic re-extraction is non-deterministic and drops whole sections without it. + --- ## For /graphify query diff --git a/tools/skillgen/expected/graphify__skill-windows.md b/tools/skillgen/expected/graphify__skill-windows.md index 574384576..abddef6e1 100644 --- a/tools/skillgen/expected/graphify__skill-windows.md +++ b/tools/skillgen/expected/graphify__skill-windows.md @@ -689,6 +689,8 @@ fi Both are non-default subcommands. `--update` re-extracts only new or changed files; `--cluster-only` reruns clustering on the existing graph. See `references/update.md` for both flows. +> **Re-extracting an already-graphed doc requires the mandatory-ID gate** (`references/update.md` → "REQUIRED: mandatory-ID gate for re-extracted docs"): snapshot that file's existing node IDs, pass them to the subagent as must-include, and refuse the merge if the node count drops below 90% of baseline or any ID is missing. Semantic re-extraction is non-deterministic and drops whole sections without it. + --- ## For /graphify query diff --git a/tools/skillgen/expected/graphify__skill.md b/tools/skillgen/expected/graphify__skill.md index d98865cc8..524d933f2 100644 --- a/tools/skillgen/expected/graphify__skill.md +++ b/tools/skillgen/expected/graphify__skill.md @@ -667,6 +667,8 @@ fi Both are non-default subcommands. `--update` re-extracts only new or changed files; `--cluster-only` reruns clustering on the existing graph. See `references/update.md` for both flows. +> **Re-extracting an already-graphed doc requires the mandatory-ID gate** (`references/update.md` → "REQUIRED: mandatory-ID gate for re-extracted docs"): snapshot that file's existing node IDs, pass them to the subagent as must-include, and refuse the merge if the node count drops below 90% of baseline or any ID is missing. Semantic re-extraction is non-deterministic and drops whole sections without it. + --- ## For /graphify query diff --git a/tools/skillgen/expected/graphify__skills__agents__references__update.md b/tools/skillgen/expected/graphify__skills__agents__references__update.md index 3632fd412..b5b16a731 100644 --- a/tools/skillgen/expected/graphify__skills__agents__references__update.md +++ b/tools/skillgen/expected/graphify__skills__agents__references__update.md @@ -65,6 +65,71 @@ If `code_only` is True: print `[graphify update] Code-only changes detected - sk If `code_only` is False (any changed file is a doc/paper/image/video): **first, if any changed file is in `new_files['video']`, run `references/transcribe.md` (Step 2.5) on those files, then rewrite `.graphify_detect.json` to move the resulting transcript paths into `files['document']` and drop `files['video']`** — otherwise raw `.mp4/.mp3` paths are fed to semantic subagents as unreadable media (#1392). Then run the full Steps 3A–3C pipeline as normal. +### REQUIRED: mandatory-ID gate for re-extracted docs + +Semantic re-extraction of an already-graphed doc is non-deterministic: the same file can +yield 75 nodes on one run and 49 on the next, dropping whole sections with no error. The +loss lands in `build_merge` and only surfaces later as a question the graph can no longer +answer. Apply this gate to **every changed doc/paper that already has nodes in +`graph.json`**. Do not skip it for a small edit — a one-line change re-extracts the whole +file. + +**1 — Snapshot the file's existing node IDs before extracting.** + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +TARGET = 'DOC_PATH' # substitute: the changed doc, as it appears in source_file +g = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) +ids = sorted(n['id'] for n in g['nodes'] if str(n.get('source_file','')).endswith(TARGET)) +Path('graphify-out/.graphify_must_ids.txt').write_text('\n'.join(ids), encoding='utf-8') +print(f'baseline: {len(ids)} existing node(s) for {TARGET}') +" +``` + +A count of 0 means the file is new — skip the gate and extract normally. + +**2 — Pass the IDs to the subagent as mandatory.** Append to the prompt from +`references/extraction-spec.md`: + +- the baseline count as an explicit target ("the prior build extracted N nodes from this + file; land at approximately N — under 90% of N means you under-extracted, go back and + cover the sections you skimmed"), +- the full ID list verbatim, labelled **MANDATORY — every one of these existed in the + prior graph and MUST appear in your output, reusing the ID verbatim**, +- an instruction to verify every mandatory ID is present *before* writing CHUNK_PATH. + +Reusing the IDs also suppresses gratuitous ID churn on re-extraction, which orphans saved +queries and inflates the merge diff for no semantic gain. + +**3 — Hard-gate the merge.** After the chunk lands and before `build_merge`, require both: +node count ≥ 90% of baseline, and zero mandatory IDs missing. + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +d = json.loads(Path('graphify-out/.graphify_chunk_01.json').read_text(encoding='utf-8')) +must = [l for l in Path('graphify-out/.graphify_must_ids.txt').read_text(encoding='utf-8').split() if l] +ids = {n['id'] for n in d['nodes']} +missing = [m for m in must if m not in ids] +ok = len(d['nodes']) >= 0.9 * len(must) and not missing +print(f'nodes={len(d[\"nodes\"])} baseline={len(must)} missing={len(missing)}') +if missing: print('MISSING:', ' '.join(missing[:20])) +print('GATE:', 'PASS' if ok else 'FAIL') +" +``` + +On **FAIL**, do not merge: re-dispatch the extraction with the same mandatory list and a +sharper completeness instruction naming the missing IDs. Two consecutive failures — stop +and report to the user rather than merging a regressed graph. + +**The `to_json` shrink guard (#479) stays authoritative.** It is the backstop, not a +nuisance. Never pass `force=True` to clear it until you have diffed the old and new node +sets and can name why each removed node is legitimately gone (file deleted, section +removed, ID renamed with a verified replacement). ID churn on a re-extracted file is a +legitimate shrink; a missing section is not. If no new files exist (only deletions), create an empty extraction so the merge step can prune: diff --git a/tools/skillgen/expected/graphify__skills__amp__references__update.md b/tools/skillgen/expected/graphify__skills__amp__references__update.md index 3632fd412..b5b16a731 100644 --- a/tools/skillgen/expected/graphify__skills__amp__references__update.md +++ b/tools/skillgen/expected/graphify__skills__amp__references__update.md @@ -65,6 +65,71 @@ If `code_only` is True: print `[graphify update] Code-only changes detected - sk If `code_only` is False (any changed file is a doc/paper/image/video): **first, if any changed file is in `new_files['video']`, run `references/transcribe.md` (Step 2.5) on those files, then rewrite `.graphify_detect.json` to move the resulting transcript paths into `files['document']` and drop `files['video']`** — otherwise raw `.mp4/.mp3` paths are fed to semantic subagents as unreadable media (#1392). Then run the full Steps 3A–3C pipeline as normal. +### REQUIRED: mandatory-ID gate for re-extracted docs + +Semantic re-extraction of an already-graphed doc is non-deterministic: the same file can +yield 75 nodes on one run and 49 on the next, dropping whole sections with no error. The +loss lands in `build_merge` and only surfaces later as a question the graph can no longer +answer. Apply this gate to **every changed doc/paper that already has nodes in +`graph.json`**. Do not skip it for a small edit — a one-line change re-extracts the whole +file. + +**1 — Snapshot the file's existing node IDs before extracting.** + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +TARGET = 'DOC_PATH' # substitute: the changed doc, as it appears in source_file +g = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) +ids = sorted(n['id'] for n in g['nodes'] if str(n.get('source_file','')).endswith(TARGET)) +Path('graphify-out/.graphify_must_ids.txt').write_text('\n'.join(ids), encoding='utf-8') +print(f'baseline: {len(ids)} existing node(s) for {TARGET}') +" +``` + +A count of 0 means the file is new — skip the gate and extract normally. + +**2 — Pass the IDs to the subagent as mandatory.** Append to the prompt from +`references/extraction-spec.md`: + +- the baseline count as an explicit target ("the prior build extracted N nodes from this + file; land at approximately N — under 90% of N means you under-extracted, go back and + cover the sections you skimmed"), +- the full ID list verbatim, labelled **MANDATORY — every one of these existed in the + prior graph and MUST appear in your output, reusing the ID verbatim**, +- an instruction to verify every mandatory ID is present *before* writing CHUNK_PATH. + +Reusing the IDs also suppresses gratuitous ID churn on re-extraction, which orphans saved +queries and inflates the merge diff for no semantic gain. + +**3 — Hard-gate the merge.** After the chunk lands and before `build_merge`, require both: +node count ≥ 90% of baseline, and zero mandatory IDs missing. + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +d = json.loads(Path('graphify-out/.graphify_chunk_01.json').read_text(encoding='utf-8')) +must = [l for l in Path('graphify-out/.graphify_must_ids.txt').read_text(encoding='utf-8').split() if l] +ids = {n['id'] for n in d['nodes']} +missing = [m for m in must if m not in ids] +ok = len(d['nodes']) >= 0.9 * len(must) and not missing +print(f'nodes={len(d[\"nodes\"])} baseline={len(must)} missing={len(missing)}') +if missing: print('MISSING:', ' '.join(missing[:20])) +print('GATE:', 'PASS' if ok else 'FAIL') +" +``` + +On **FAIL**, do not merge: re-dispatch the extraction with the same mandatory list and a +sharper completeness instruction naming the missing IDs. Two consecutive failures — stop +and report to the user rather than merging a regressed graph. + +**The `to_json` shrink guard (#479) stays authoritative.** It is the backstop, not a +nuisance. Never pass `force=True` to clear it until you have diffed the old and new node +sets and can name why each removed node is legitimately gone (file deleted, section +removed, ID renamed with a verified replacement). ID churn on a re-extracted file is a +legitimate shrink; a missing section is not. If no new files exist (only deletions), create an empty extraction so the merge step can prune: diff --git a/tools/skillgen/expected/graphify__skills__claude__references__update.md b/tools/skillgen/expected/graphify__skills__claude__references__update.md index 3632fd412..b5b16a731 100644 --- a/tools/skillgen/expected/graphify__skills__claude__references__update.md +++ b/tools/skillgen/expected/graphify__skills__claude__references__update.md @@ -65,6 +65,71 @@ If `code_only` is True: print `[graphify update] Code-only changes detected - sk If `code_only` is False (any changed file is a doc/paper/image/video): **first, if any changed file is in `new_files['video']`, run `references/transcribe.md` (Step 2.5) on those files, then rewrite `.graphify_detect.json` to move the resulting transcript paths into `files['document']` and drop `files['video']`** — otherwise raw `.mp4/.mp3` paths are fed to semantic subagents as unreadable media (#1392). Then run the full Steps 3A–3C pipeline as normal. +### REQUIRED: mandatory-ID gate for re-extracted docs + +Semantic re-extraction of an already-graphed doc is non-deterministic: the same file can +yield 75 nodes on one run and 49 on the next, dropping whole sections with no error. The +loss lands in `build_merge` and only surfaces later as a question the graph can no longer +answer. Apply this gate to **every changed doc/paper that already has nodes in +`graph.json`**. Do not skip it for a small edit — a one-line change re-extracts the whole +file. + +**1 — Snapshot the file's existing node IDs before extracting.** + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +TARGET = 'DOC_PATH' # substitute: the changed doc, as it appears in source_file +g = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) +ids = sorted(n['id'] for n in g['nodes'] if str(n.get('source_file','')).endswith(TARGET)) +Path('graphify-out/.graphify_must_ids.txt').write_text('\n'.join(ids), encoding='utf-8') +print(f'baseline: {len(ids)} existing node(s) for {TARGET}') +" +``` + +A count of 0 means the file is new — skip the gate and extract normally. + +**2 — Pass the IDs to the subagent as mandatory.** Append to the prompt from +`references/extraction-spec.md`: + +- the baseline count as an explicit target ("the prior build extracted N nodes from this + file; land at approximately N — under 90% of N means you under-extracted, go back and + cover the sections you skimmed"), +- the full ID list verbatim, labelled **MANDATORY — every one of these existed in the + prior graph and MUST appear in your output, reusing the ID verbatim**, +- an instruction to verify every mandatory ID is present *before* writing CHUNK_PATH. + +Reusing the IDs also suppresses gratuitous ID churn on re-extraction, which orphans saved +queries and inflates the merge diff for no semantic gain. + +**3 — Hard-gate the merge.** After the chunk lands and before `build_merge`, require both: +node count ≥ 90% of baseline, and zero mandatory IDs missing. + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +d = json.loads(Path('graphify-out/.graphify_chunk_01.json').read_text(encoding='utf-8')) +must = [l for l in Path('graphify-out/.graphify_must_ids.txt').read_text(encoding='utf-8').split() if l] +ids = {n['id'] for n in d['nodes']} +missing = [m for m in must if m not in ids] +ok = len(d['nodes']) >= 0.9 * len(must) and not missing +print(f'nodes={len(d[\"nodes\"])} baseline={len(must)} missing={len(missing)}') +if missing: print('MISSING:', ' '.join(missing[:20])) +print('GATE:', 'PASS' if ok else 'FAIL') +" +``` + +On **FAIL**, do not merge: re-dispatch the extraction with the same mandatory list and a +sharper completeness instruction naming the missing IDs. Two consecutive failures — stop +and report to the user rather than merging a regressed graph. + +**The `to_json` shrink guard (#479) stays authoritative.** It is the backstop, not a +nuisance. Never pass `force=True` to clear it until you have diffed the old and new node +sets and can name why each removed node is legitimately gone (file deleted, section +removed, ID renamed with a verified replacement). ID churn on a re-extracted file is a +legitimate shrink; a missing section is not. If no new files exist (only deletions), create an empty extraction so the merge step can prune: diff --git a/tools/skillgen/expected/graphify__skills__claw__references__update.md b/tools/skillgen/expected/graphify__skills__claw__references__update.md index 3632fd412..b5b16a731 100644 --- a/tools/skillgen/expected/graphify__skills__claw__references__update.md +++ b/tools/skillgen/expected/graphify__skills__claw__references__update.md @@ -65,6 +65,71 @@ If `code_only` is True: print `[graphify update] Code-only changes detected - sk If `code_only` is False (any changed file is a doc/paper/image/video): **first, if any changed file is in `new_files['video']`, run `references/transcribe.md` (Step 2.5) on those files, then rewrite `.graphify_detect.json` to move the resulting transcript paths into `files['document']` and drop `files['video']`** — otherwise raw `.mp4/.mp3` paths are fed to semantic subagents as unreadable media (#1392). Then run the full Steps 3A–3C pipeline as normal. +### REQUIRED: mandatory-ID gate for re-extracted docs + +Semantic re-extraction of an already-graphed doc is non-deterministic: the same file can +yield 75 nodes on one run and 49 on the next, dropping whole sections with no error. The +loss lands in `build_merge` and only surfaces later as a question the graph can no longer +answer. Apply this gate to **every changed doc/paper that already has nodes in +`graph.json`**. Do not skip it for a small edit — a one-line change re-extracts the whole +file. + +**1 — Snapshot the file's existing node IDs before extracting.** + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +TARGET = 'DOC_PATH' # substitute: the changed doc, as it appears in source_file +g = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) +ids = sorted(n['id'] for n in g['nodes'] if str(n.get('source_file','')).endswith(TARGET)) +Path('graphify-out/.graphify_must_ids.txt').write_text('\n'.join(ids), encoding='utf-8') +print(f'baseline: {len(ids)} existing node(s) for {TARGET}') +" +``` + +A count of 0 means the file is new — skip the gate and extract normally. + +**2 — Pass the IDs to the subagent as mandatory.** Append to the prompt from +`references/extraction-spec.md`: + +- the baseline count as an explicit target ("the prior build extracted N nodes from this + file; land at approximately N — under 90% of N means you under-extracted, go back and + cover the sections you skimmed"), +- the full ID list verbatim, labelled **MANDATORY — every one of these existed in the + prior graph and MUST appear in your output, reusing the ID verbatim**, +- an instruction to verify every mandatory ID is present *before* writing CHUNK_PATH. + +Reusing the IDs also suppresses gratuitous ID churn on re-extraction, which orphans saved +queries and inflates the merge diff for no semantic gain. + +**3 — Hard-gate the merge.** After the chunk lands and before `build_merge`, require both: +node count ≥ 90% of baseline, and zero mandatory IDs missing. + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +d = json.loads(Path('graphify-out/.graphify_chunk_01.json').read_text(encoding='utf-8')) +must = [l for l in Path('graphify-out/.graphify_must_ids.txt').read_text(encoding='utf-8').split() if l] +ids = {n['id'] for n in d['nodes']} +missing = [m for m in must if m not in ids] +ok = len(d['nodes']) >= 0.9 * len(must) and not missing +print(f'nodes={len(d[\"nodes\"])} baseline={len(must)} missing={len(missing)}') +if missing: print('MISSING:', ' '.join(missing[:20])) +print('GATE:', 'PASS' if ok else 'FAIL') +" +``` + +On **FAIL**, do not merge: re-dispatch the extraction with the same mandatory list and a +sharper completeness instruction naming the missing IDs. Two consecutive failures — stop +and report to the user rather than merging a regressed graph. + +**The `to_json` shrink guard (#479) stays authoritative.** It is the backstop, not a +nuisance. Never pass `force=True` to clear it until you have diffed the old and new node +sets and can name why each removed node is legitimately gone (file deleted, section +removed, ID renamed with a verified replacement). ID churn on a re-extracted file is a +legitimate shrink; a missing section is not. If no new files exist (only deletions), create an empty extraction so the merge step can prune: diff --git a/tools/skillgen/expected/graphify__skills__codex__references__update.md b/tools/skillgen/expected/graphify__skills__codex__references__update.md index 3632fd412..b5b16a731 100644 --- a/tools/skillgen/expected/graphify__skills__codex__references__update.md +++ b/tools/skillgen/expected/graphify__skills__codex__references__update.md @@ -65,6 +65,71 @@ If `code_only` is True: print `[graphify update] Code-only changes detected - sk If `code_only` is False (any changed file is a doc/paper/image/video): **first, if any changed file is in `new_files['video']`, run `references/transcribe.md` (Step 2.5) on those files, then rewrite `.graphify_detect.json` to move the resulting transcript paths into `files['document']` and drop `files['video']`** — otherwise raw `.mp4/.mp3` paths are fed to semantic subagents as unreadable media (#1392). Then run the full Steps 3A–3C pipeline as normal. +### REQUIRED: mandatory-ID gate for re-extracted docs + +Semantic re-extraction of an already-graphed doc is non-deterministic: the same file can +yield 75 nodes on one run and 49 on the next, dropping whole sections with no error. The +loss lands in `build_merge` and only surfaces later as a question the graph can no longer +answer. Apply this gate to **every changed doc/paper that already has nodes in +`graph.json`**. Do not skip it for a small edit — a one-line change re-extracts the whole +file. + +**1 — Snapshot the file's existing node IDs before extracting.** + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +TARGET = 'DOC_PATH' # substitute: the changed doc, as it appears in source_file +g = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) +ids = sorted(n['id'] for n in g['nodes'] if str(n.get('source_file','')).endswith(TARGET)) +Path('graphify-out/.graphify_must_ids.txt').write_text('\n'.join(ids), encoding='utf-8') +print(f'baseline: {len(ids)} existing node(s) for {TARGET}') +" +``` + +A count of 0 means the file is new — skip the gate and extract normally. + +**2 — Pass the IDs to the subagent as mandatory.** Append to the prompt from +`references/extraction-spec.md`: + +- the baseline count as an explicit target ("the prior build extracted N nodes from this + file; land at approximately N — under 90% of N means you under-extracted, go back and + cover the sections you skimmed"), +- the full ID list verbatim, labelled **MANDATORY — every one of these existed in the + prior graph and MUST appear in your output, reusing the ID verbatim**, +- an instruction to verify every mandatory ID is present *before* writing CHUNK_PATH. + +Reusing the IDs also suppresses gratuitous ID churn on re-extraction, which orphans saved +queries and inflates the merge diff for no semantic gain. + +**3 — Hard-gate the merge.** After the chunk lands and before `build_merge`, require both: +node count ≥ 90% of baseline, and zero mandatory IDs missing. + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +d = json.loads(Path('graphify-out/.graphify_chunk_01.json').read_text(encoding='utf-8')) +must = [l for l in Path('graphify-out/.graphify_must_ids.txt').read_text(encoding='utf-8').split() if l] +ids = {n['id'] for n in d['nodes']} +missing = [m for m in must if m not in ids] +ok = len(d['nodes']) >= 0.9 * len(must) and not missing +print(f'nodes={len(d[\"nodes\"])} baseline={len(must)} missing={len(missing)}') +if missing: print('MISSING:', ' '.join(missing[:20])) +print('GATE:', 'PASS' if ok else 'FAIL') +" +``` + +On **FAIL**, do not merge: re-dispatch the extraction with the same mandatory list and a +sharper completeness instruction naming the missing IDs. Two consecutive failures — stop +and report to the user rather than merging a regressed graph. + +**The `to_json` shrink guard (#479) stays authoritative.** It is the backstop, not a +nuisance. Never pass `force=True` to clear it until you have diffed the old and new node +sets and can name why each removed node is legitimately gone (file deleted, section +removed, ID renamed with a verified replacement). ID churn on a re-extracted file is a +legitimate shrink; a missing section is not. If no new files exist (only deletions), create an empty extraction so the merge step can prune: diff --git a/tools/skillgen/expected/graphify__skills__copilot__references__update.md b/tools/skillgen/expected/graphify__skills__copilot__references__update.md index 3632fd412..b5b16a731 100644 --- a/tools/skillgen/expected/graphify__skills__copilot__references__update.md +++ b/tools/skillgen/expected/graphify__skills__copilot__references__update.md @@ -65,6 +65,71 @@ If `code_only` is True: print `[graphify update] Code-only changes detected - sk If `code_only` is False (any changed file is a doc/paper/image/video): **first, if any changed file is in `new_files['video']`, run `references/transcribe.md` (Step 2.5) on those files, then rewrite `.graphify_detect.json` to move the resulting transcript paths into `files['document']` and drop `files['video']`** — otherwise raw `.mp4/.mp3` paths are fed to semantic subagents as unreadable media (#1392). Then run the full Steps 3A–3C pipeline as normal. +### REQUIRED: mandatory-ID gate for re-extracted docs + +Semantic re-extraction of an already-graphed doc is non-deterministic: the same file can +yield 75 nodes on one run and 49 on the next, dropping whole sections with no error. The +loss lands in `build_merge` and only surfaces later as a question the graph can no longer +answer. Apply this gate to **every changed doc/paper that already has nodes in +`graph.json`**. Do not skip it for a small edit — a one-line change re-extracts the whole +file. + +**1 — Snapshot the file's existing node IDs before extracting.** + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +TARGET = 'DOC_PATH' # substitute: the changed doc, as it appears in source_file +g = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) +ids = sorted(n['id'] for n in g['nodes'] if str(n.get('source_file','')).endswith(TARGET)) +Path('graphify-out/.graphify_must_ids.txt').write_text('\n'.join(ids), encoding='utf-8') +print(f'baseline: {len(ids)} existing node(s) for {TARGET}') +" +``` + +A count of 0 means the file is new — skip the gate and extract normally. + +**2 — Pass the IDs to the subagent as mandatory.** Append to the prompt from +`references/extraction-spec.md`: + +- the baseline count as an explicit target ("the prior build extracted N nodes from this + file; land at approximately N — under 90% of N means you under-extracted, go back and + cover the sections you skimmed"), +- the full ID list verbatim, labelled **MANDATORY — every one of these existed in the + prior graph and MUST appear in your output, reusing the ID verbatim**, +- an instruction to verify every mandatory ID is present *before* writing CHUNK_PATH. + +Reusing the IDs also suppresses gratuitous ID churn on re-extraction, which orphans saved +queries and inflates the merge diff for no semantic gain. + +**3 — Hard-gate the merge.** After the chunk lands and before `build_merge`, require both: +node count ≥ 90% of baseline, and zero mandatory IDs missing. + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +d = json.loads(Path('graphify-out/.graphify_chunk_01.json').read_text(encoding='utf-8')) +must = [l for l in Path('graphify-out/.graphify_must_ids.txt').read_text(encoding='utf-8').split() if l] +ids = {n['id'] for n in d['nodes']} +missing = [m for m in must if m not in ids] +ok = len(d['nodes']) >= 0.9 * len(must) and not missing +print(f'nodes={len(d[\"nodes\"])} baseline={len(must)} missing={len(missing)}') +if missing: print('MISSING:', ' '.join(missing[:20])) +print('GATE:', 'PASS' if ok else 'FAIL') +" +``` + +On **FAIL**, do not merge: re-dispatch the extraction with the same mandatory list and a +sharper completeness instruction naming the missing IDs. Two consecutive failures — stop +and report to the user rather than merging a regressed graph. + +**The `to_json` shrink guard (#479) stays authoritative.** It is the backstop, not a +nuisance. Never pass `force=True` to clear it until you have diffed the old and new node +sets and can name why each removed node is legitimately gone (file deleted, section +removed, ID renamed with a verified replacement). ID churn on a re-extracted file is a +legitimate shrink; a missing section is not. If no new files exist (only deletions), create an empty extraction so the merge step can prune: diff --git a/tools/skillgen/expected/graphify__skills__droid__references__update.md b/tools/skillgen/expected/graphify__skills__droid__references__update.md index 3632fd412..b5b16a731 100644 --- a/tools/skillgen/expected/graphify__skills__droid__references__update.md +++ b/tools/skillgen/expected/graphify__skills__droid__references__update.md @@ -65,6 +65,71 @@ If `code_only` is True: print `[graphify update] Code-only changes detected - sk If `code_only` is False (any changed file is a doc/paper/image/video): **first, if any changed file is in `new_files['video']`, run `references/transcribe.md` (Step 2.5) on those files, then rewrite `.graphify_detect.json` to move the resulting transcript paths into `files['document']` and drop `files['video']`** — otherwise raw `.mp4/.mp3` paths are fed to semantic subagents as unreadable media (#1392). Then run the full Steps 3A–3C pipeline as normal. +### REQUIRED: mandatory-ID gate for re-extracted docs + +Semantic re-extraction of an already-graphed doc is non-deterministic: the same file can +yield 75 nodes on one run and 49 on the next, dropping whole sections with no error. The +loss lands in `build_merge` and only surfaces later as a question the graph can no longer +answer. Apply this gate to **every changed doc/paper that already has nodes in +`graph.json`**. Do not skip it for a small edit — a one-line change re-extracts the whole +file. + +**1 — Snapshot the file's existing node IDs before extracting.** + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +TARGET = 'DOC_PATH' # substitute: the changed doc, as it appears in source_file +g = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) +ids = sorted(n['id'] for n in g['nodes'] if str(n.get('source_file','')).endswith(TARGET)) +Path('graphify-out/.graphify_must_ids.txt').write_text('\n'.join(ids), encoding='utf-8') +print(f'baseline: {len(ids)} existing node(s) for {TARGET}') +" +``` + +A count of 0 means the file is new — skip the gate and extract normally. + +**2 — Pass the IDs to the subagent as mandatory.** Append to the prompt from +`references/extraction-spec.md`: + +- the baseline count as an explicit target ("the prior build extracted N nodes from this + file; land at approximately N — under 90% of N means you under-extracted, go back and + cover the sections you skimmed"), +- the full ID list verbatim, labelled **MANDATORY — every one of these existed in the + prior graph and MUST appear in your output, reusing the ID verbatim**, +- an instruction to verify every mandatory ID is present *before* writing CHUNK_PATH. + +Reusing the IDs also suppresses gratuitous ID churn on re-extraction, which orphans saved +queries and inflates the merge diff for no semantic gain. + +**3 — Hard-gate the merge.** After the chunk lands and before `build_merge`, require both: +node count ≥ 90% of baseline, and zero mandatory IDs missing. + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +d = json.loads(Path('graphify-out/.graphify_chunk_01.json').read_text(encoding='utf-8')) +must = [l for l in Path('graphify-out/.graphify_must_ids.txt').read_text(encoding='utf-8').split() if l] +ids = {n['id'] for n in d['nodes']} +missing = [m for m in must if m not in ids] +ok = len(d['nodes']) >= 0.9 * len(must) and not missing +print(f'nodes={len(d[\"nodes\"])} baseline={len(must)} missing={len(missing)}') +if missing: print('MISSING:', ' '.join(missing[:20])) +print('GATE:', 'PASS' if ok else 'FAIL') +" +``` + +On **FAIL**, do not merge: re-dispatch the extraction with the same mandatory list and a +sharper completeness instruction naming the missing IDs. Two consecutive failures — stop +and report to the user rather than merging a regressed graph. + +**The `to_json` shrink guard (#479) stays authoritative.** It is the backstop, not a +nuisance. Never pass `force=True` to clear it until you have diffed the old and new node +sets and can name why each removed node is legitimately gone (file deleted, section +removed, ID renamed with a verified replacement). ID churn on a re-extracted file is a +legitimate shrink; a missing section is not. If no new files exist (only deletions), create an empty extraction so the merge step can prune: diff --git a/tools/skillgen/expected/graphify__skills__kilo__references__update.md b/tools/skillgen/expected/graphify__skills__kilo__references__update.md index 3632fd412..b5b16a731 100644 --- a/tools/skillgen/expected/graphify__skills__kilo__references__update.md +++ b/tools/skillgen/expected/graphify__skills__kilo__references__update.md @@ -65,6 +65,71 @@ If `code_only` is True: print `[graphify update] Code-only changes detected - sk If `code_only` is False (any changed file is a doc/paper/image/video): **first, if any changed file is in `new_files['video']`, run `references/transcribe.md` (Step 2.5) on those files, then rewrite `.graphify_detect.json` to move the resulting transcript paths into `files['document']` and drop `files['video']`** — otherwise raw `.mp4/.mp3` paths are fed to semantic subagents as unreadable media (#1392). Then run the full Steps 3A–3C pipeline as normal. +### REQUIRED: mandatory-ID gate for re-extracted docs + +Semantic re-extraction of an already-graphed doc is non-deterministic: the same file can +yield 75 nodes on one run and 49 on the next, dropping whole sections with no error. The +loss lands in `build_merge` and only surfaces later as a question the graph can no longer +answer. Apply this gate to **every changed doc/paper that already has nodes in +`graph.json`**. Do not skip it for a small edit — a one-line change re-extracts the whole +file. + +**1 — Snapshot the file's existing node IDs before extracting.** + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +TARGET = 'DOC_PATH' # substitute: the changed doc, as it appears in source_file +g = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) +ids = sorted(n['id'] for n in g['nodes'] if str(n.get('source_file','')).endswith(TARGET)) +Path('graphify-out/.graphify_must_ids.txt').write_text('\n'.join(ids), encoding='utf-8') +print(f'baseline: {len(ids)} existing node(s) for {TARGET}') +" +``` + +A count of 0 means the file is new — skip the gate and extract normally. + +**2 — Pass the IDs to the subagent as mandatory.** Append to the prompt from +`references/extraction-spec.md`: + +- the baseline count as an explicit target ("the prior build extracted N nodes from this + file; land at approximately N — under 90% of N means you under-extracted, go back and + cover the sections you skimmed"), +- the full ID list verbatim, labelled **MANDATORY — every one of these existed in the + prior graph and MUST appear in your output, reusing the ID verbatim**, +- an instruction to verify every mandatory ID is present *before* writing CHUNK_PATH. + +Reusing the IDs also suppresses gratuitous ID churn on re-extraction, which orphans saved +queries and inflates the merge diff for no semantic gain. + +**3 — Hard-gate the merge.** After the chunk lands and before `build_merge`, require both: +node count ≥ 90% of baseline, and zero mandatory IDs missing. + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +d = json.loads(Path('graphify-out/.graphify_chunk_01.json').read_text(encoding='utf-8')) +must = [l for l in Path('graphify-out/.graphify_must_ids.txt').read_text(encoding='utf-8').split() if l] +ids = {n['id'] for n in d['nodes']} +missing = [m for m in must if m not in ids] +ok = len(d['nodes']) >= 0.9 * len(must) and not missing +print(f'nodes={len(d[\"nodes\"])} baseline={len(must)} missing={len(missing)}') +if missing: print('MISSING:', ' '.join(missing[:20])) +print('GATE:', 'PASS' if ok else 'FAIL') +" +``` + +On **FAIL**, do not merge: re-dispatch the extraction with the same mandatory list and a +sharper completeness instruction naming the missing IDs. Two consecutive failures — stop +and report to the user rather than merging a regressed graph. + +**The `to_json` shrink guard (#479) stays authoritative.** It is the backstop, not a +nuisance. Never pass `force=True` to clear it until you have diffed the old and new node +sets and can name why each removed node is legitimately gone (file deleted, section +removed, ID renamed with a verified replacement). ID churn on a re-extracted file is a +legitimate shrink; a missing section is not. If no new files exist (only deletions), create an empty extraction so the merge step can prune: diff --git a/tools/skillgen/expected/graphify__skills__kiro__references__update.md b/tools/skillgen/expected/graphify__skills__kiro__references__update.md index 3632fd412..b5b16a731 100644 --- a/tools/skillgen/expected/graphify__skills__kiro__references__update.md +++ b/tools/skillgen/expected/graphify__skills__kiro__references__update.md @@ -65,6 +65,71 @@ If `code_only` is True: print `[graphify update] Code-only changes detected - sk If `code_only` is False (any changed file is a doc/paper/image/video): **first, if any changed file is in `new_files['video']`, run `references/transcribe.md` (Step 2.5) on those files, then rewrite `.graphify_detect.json` to move the resulting transcript paths into `files['document']` and drop `files['video']`** — otherwise raw `.mp4/.mp3` paths are fed to semantic subagents as unreadable media (#1392). Then run the full Steps 3A–3C pipeline as normal. +### REQUIRED: mandatory-ID gate for re-extracted docs + +Semantic re-extraction of an already-graphed doc is non-deterministic: the same file can +yield 75 nodes on one run and 49 on the next, dropping whole sections with no error. The +loss lands in `build_merge` and only surfaces later as a question the graph can no longer +answer. Apply this gate to **every changed doc/paper that already has nodes in +`graph.json`**. Do not skip it for a small edit — a one-line change re-extracts the whole +file. + +**1 — Snapshot the file's existing node IDs before extracting.** + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +TARGET = 'DOC_PATH' # substitute: the changed doc, as it appears in source_file +g = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) +ids = sorted(n['id'] for n in g['nodes'] if str(n.get('source_file','')).endswith(TARGET)) +Path('graphify-out/.graphify_must_ids.txt').write_text('\n'.join(ids), encoding='utf-8') +print(f'baseline: {len(ids)} existing node(s) for {TARGET}') +" +``` + +A count of 0 means the file is new — skip the gate and extract normally. + +**2 — Pass the IDs to the subagent as mandatory.** Append to the prompt from +`references/extraction-spec.md`: + +- the baseline count as an explicit target ("the prior build extracted N nodes from this + file; land at approximately N — under 90% of N means you under-extracted, go back and + cover the sections you skimmed"), +- the full ID list verbatim, labelled **MANDATORY — every one of these existed in the + prior graph and MUST appear in your output, reusing the ID verbatim**, +- an instruction to verify every mandatory ID is present *before* writing CHUNK_PATH. + +Reusing the IDs also suppresses gratuitous ID churn on re-extraction, which orphans saved +queries and inflates the merge diff for no semantic gain. + +**3 — Hard-gate the merge.** After the chunk lands and before `build_merge`, require both: +node count ≥ 90% of baseline, and zero mandatory IDs missing. + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +d = json.loads(Path('graphify-out/.graphify_chunk_01.json').read_text(encoding='utf-8')) +must = [l for l in Path('graphify-out/.graphify_must_ids.txt').read_text(encoding='utf-8').split() if l] +ids = {n['id'] for n in d['nodes']} +missing = [m for m in must if m not in ids] +ok = len(d['nodes']) >= 0.9 * len(must) and not missing +print(f'nodes={len(d[\"nodes\"])} baseline={len(must)} missing={len(missing)}') +if missing: print('MISSING:', ' '.join(missing[:20])) +print('GATE:', 'PASS' if ok else 'FAIL') +" +``` + +On **FAIL**, do not merge: re-dispatch the extraction with the same mandatory list and a +sharper completeness instruction naming the missing IDs. Two consecutive failures — stop +and report to the user rather than merging a regressed graph. + +**The `to_json` shrink guard (#479) stays authoritative.** It is the backstop, not a +nuisance. Never pass `force=True` to clear it until you have diffed the old and new node +sets and can name why each removed node is legitimately gone (file deleted, section +removed, ID renamed with a verified replacement). ID churn on a re-extracted file is a +legitimate shrink; a missing section is not. If no new files exist (only deletions), create an empty extraction so the merge step can prune: diff --git a/tools/skillgen/expected/graphify__skills__opencode__references__update.md b/tools/skillgen/expected/graphify__skills__opencode__references__update.md index 3632fd412..b5b16a731 100644 --- a/tools/skillgen/expected/graphify__skills__opencode__references__update.md +++ b/tools/skillgen/expected/graphify__skills__opencode__references__update.md @@ -65,6 +65,71 @@ If `code_only` is True: print `[graphify update] Code-only changes detected - sk If `code_only` is False (any changed file is a doc/paper/image/video): **first, if any changed file is in `new_files['video']`, run `references/transcribe.md` (Step 2.5) on those files, then rewrite `.graphify_detect.json` to move the resulting transcript paths into `files['document']` and drop `files['video']`** — otherwise raw `.mp4/.mp3` paths are fed to semantic subagents as unreadable media (#1392). Then run the full Steps 3A–3C pipeline as normal. +### REQUIRED: mandatory-ID gate for re-extracted docs + +Semantic re-extraction of an already-graphed doc is non-deterministic: the same file can +yield 75 nodes on one run and 49 on the next, dropping whole sections with no error. The +loss lands in `build_merge` and only surfaces later as a question the graph can no longer +answer. Apply this gate to **every changed doc/paper that already has nodes in +`graph.json`**. Do not skip it for a small edit — a one-line change re-extracts the whole +file. + +**1 — Snapshot the file's existing node IDs before extracting.** + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +TARGET = 'DOC_PATH' # substitute: the changed doc, as it appears in source_file +g = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) +ids = sorted(n['id'] for n in g['nodes'] if str(n.get('source_file','')).endswith(TARGET)) +Path('graphify-out/.graphify_must_ids.txt').write_text('\n'.join(ids), encoding='utf-8') +print(f'baseline: {len(ids)} existing node(s) for {TARGET}') +" +``` + +A count of 0 means the file is new — skip the gate and extract normally. + +**2 — Pass the IDs to the subagent as mandatory.** Append to the prompt from +`references/extraction-spec.md`: + +- the baseline count as an explicit target ("the prior build extracted N nodes from this + file; land at approximately N — under 90% of N means you under-extracted, go back and + cover the sections you skimmed"), +- the full ID list verbatim, labelled **MANDATORY — every one of these existed in the + prior graph and MUST appear in your output, reusing the ID verbatim**, +- an instruction to verify every mandatory ID is present *before* writing CHUNK_PATH. + +Reusing the IDs also suppresses gratuitous ID churn on re-extraction, which orphans saved +queries and inflates the merge diff for no semantic gain. + +**3 — Hard-gate the merge.** After the chunk lands and before `build_merge`, require both: +node count ≥ 90% of baseline, and zero mandatory IDs missing. + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +d = json.loads(Path('graphify-out/.graphify_chunk_01.json').read_text(encoding='utf-8')) +must = [l for l in Path('graphify-out/.graphify_must_ids.txt').read_text(encoding='utf-8').split() if l] +ids = {n['id'] for n in d['nodes']} +missing = [m for m in must if m not in ids] +ok = len(d['nodes']) >= 0.9 * len(must) and not missing +print(f'nodes={len(d[\"nodes\"])} baseline={len(must)} missing={len(missing)}') +if missing: print('MISSING:', ' '.join(missing[:20])) +print('GATE:', 'PASS' if ok else 'FAIL') +" +``` + +On **FAIL**, do not merge: re-dispatch the extraction with the same mandatory list and a +sharper completeness instruction naming the missing IDs. Two consecutive failures — stop +and report to the user rather than merging a regressed graph. + +**The `to_json` shrink guard (#479) stays authoritative.** It is the backstop, not a +nuisance. Never pass `force=True` to clear it until you have diffed the old and new node +sets and can name why each removed node is legitimately gone (file deleted, section +removed, ID renamed with a verified replacement). ID churn on a re-extracted file is a +legitimate shrink; a missing section is not. If no new files exist (only deletions), create an empty extraction so the merge step can prune: diff --git a/tools/skillgen/expected/graphify__skills__pi__references__update.md b/tools/skillgen/expected/graphify__skills__pi__references__update.md index 3632fd412..b5b16a731 100644 --- a/tools/skillgen/expected/graphify__skills__pi__references__update.md +++ b/tools/skillgen/expected/graphify__skills__pi__references__update.md @@ -65,6 +65,71 @@ If `code_only` is True: print `[graphify update] Code-only changes detected - sk If `code_only` is False (any changed file is a doc/paper/image/video): **first, if any changed file is in `new_files['video']`, run `references/transcribe.md` (Step 2.5) on those files, then rewrite `.graphify_detect.json` to move the resulting transcript paths into `files['document']` and drop `files['video']`** — otherwise raw `.mp4/.mp3` paths are fed to semantic subagents as unreadable media (#1392). Then run the full Steps 3A–3C pipeline as normal. +### REQUIRED: mandatory-ID gate for re-extracted docs + +Semantic re-extraction of an already-graphed doc is non-deterministic: the same file can +yield 75 nodes on one run and 49 on the next, dropping whole sections with no error. The +loss lands in `build_merge` and only surfaces later as a question the graph can no longer +answer. Apply this gate to **every changed doc/paper that already has nodes in +`graph.json`**. Do not skip it for a small edit — a one-line change re-extracts the whole +file. + +**1 — Snapshot the file's existing node IDs before extracting.** + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +TARGET = 'DOC_PATH' # substitute: the changed doc, as it appears in source_file +g = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) +ids = sorted(n['id'] for n in g['nodes'] if str(n.get('source_file','')).endswith(TARGET)) +Path('graphify-out/.graphify_must_ids.txt').write_text('\n'.join(ids), encoding='utf-8') +print(f'baseline: {len(ids)} existing node(s) for {TARGET}') +" +``` + +A count of 0 means the file is new — skip the gate and extract normally. + +**2 — Pass the IDs to the subagent as mandatory.** Append to the prompt from +`references/extraction-spec.md`: + +- the baseline count as an explicit target ("the prior build extracted N nodes from this + file; land at approximately N — under 90% of N means you under-extracted, go back and + cover the sections you skimmed"), +- the full ID list verbatim, labelled **MANDATORY — every one of these existed in the + prior graph and MUST appear in your output, reusing the ID verbatim**, +- an instruction to verify every mandatory ID is present *before* writing CHUNK_PATH. + +Reusing the IDs also suppresses gratuitous ID churn on re-extraction, which orphans saved +queries and inflates the merge diff for no semantic gain. + +**3 — Hard-gate the merge.** After the chunk lands and before `build_merge`, require both: +node count ≥ 90% of baseline, and zero mandatory IDs missing. + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +d = json.loads(Path('graphify-out/.graphify_chunk_01.json').read_text(encoding='utf-8')) +must = [l for l in Path('graphify-out/.graphify_must_ids.txt').read_text(encoding='utf-8').split() if l] +ids = {n['id'] for n in d['nodes']} +missing = [m for m in must if m not in ids] +ok = len(d['nodes']) >= 0.9 * len(must) and not missing +print(f'nodes={len(d[\"nodes\"])} baseline={len(must)} missing={len(missing)}') +if missing: print('MISSING:', ' '.join(missing[:20])) +print('GATE:', 'PASS' if ok else 'FAIL') +" +``` + +On **FAIL**, do not merge: re-dispatch the extraction with the same mandatory list and a +sharper completeness instruction naming the missing IDs. Two consecutive failures — stop +and report to the user rather than merging a regressed graph. + +**The `to_json` shrink guard (#479) stays authoritative.** It is the backstop, not a +nuisance. Never pass `force=True` to clear it until you have diffed the old and new node +sets and can name why each removed node is legitimately gone (file deleted, section +removed, ID renamed with a verified replacement). ID churn on a re-extracted file is a +legitimate shrink; a missing section is not. If no new files exist (only deletions), create an empty extraction so the merge step can prune: diff --git a/tools/skillgen/expected/graphify__skills__trae__references__update.md b/tools/skillgen/expected/graphify__skills__trae__references__update.md index 3632fd412..b5b16a731 100644 --- a/tools/skillgen/expected/graphify__skills__trae__references__update.md +++ b/tools/skillgen/expected/graphify__skills__trae__references__update.md @@ -65,6 +65,71 @@ If `code_only` is True: print `[graphify update] Code-only changes detected - sk If `code_only` is False (any changed file is a doc/paper/image/video): **first, if any changed file is in `new_files['video']`, run `references/transcribe.md` (Step 2.5) on those files, then rewrite `.graphify_detect.json` to move the resulting transcript paths into `files['document']` and drop `files['video']`** — otherwise raw `.mp4/.mp3` paths are fed to semantic subagents as unreadable media (#1392). Then run the full Steps 3A–3C pipeline as normal. +### REQUIRED: mandatory-ID gate for re-extracted docs + +Semantic re-extraction of an already-graphed doc is non-deterministic: the same file can +yield 75 nodes on one run and 49 on the next, dropping whole sections with no error. The +loss lands in `build_merge` and only surfaces later as a question the graph can no longer +answer. Apply this gate to **every changed doc/paper that already has nodes in +`graph.json`**. Do not skip it for a small edit — a one-line change re-extracts the whole +file. + +**1 — Snapshot the file's existing node IDs before extracting.** + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +TARGET = 'DOC_PATH' # substitute: the changed doc, as it appears in source_file +g = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) +ids = sorted(n['id'] for n in g['nodes'] if str(n.get('source_file','')).endswith(TARGET)) +Path('graphify-out/.graphify_must_ids.txt').write_text('\n'.join(ids), encoding='utf-8') +print(f'baseline: {len(ids)} existing node(s) for {TARGET}') +" +``` + +A count of 0 means the file is new — skip the gate and extract normally. + +**2 — Pass the IDs to the subagent as mandatory.** Append to the prompt from +`references/extraction-spec.md`: + +- the baseline count as an explicit target ("the prior build extracted N nodes from this + file; land at approximately N — under 90% of N means you under-extracted, go back and + cover the sections you skimmed"), +- the full ID list verbatim, labelled **MANDATORY — every one of these existed in the + prior graph and MUST appear in your output, reusing the ID verbatim**, +- an instruction to verify every mandatory ID is present *before* writing CHUNK_PATH. + +Reusing the IDs also suppresses gratuitous ID churn on re-extraction, which orphans saved +queries and inflates the merge diff for no semantic gain. + +**3 — Hard-gate the merge.** After the chunk lands and before `build_merge`, require both: +node count ≥ 90% of baseline, and zero mandatory IDs missing. + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +d = json.loads(Path('graphify-out/.graphify_chunk_01.json').read_text(encoding='utf-8')) +must = [l for l in Path('graphify-out/.graphify_must_ids.txt').read_text(encoding='utf-8').split() if l] +ids = {n['id'] for n in d['nodes']} +missing = [m for m in must if m not in ids] +ok = len(d['nodes']) >= 0.9 * len(must) and not missing +print(f'nodes={len(d[\"nodes\"])} baseline={len(must)} missing={len(missing)}') +if missing: print('MISSING:', ' '.join(missing[:20])) +print('GATE:', 'PASS' if ok else 'FAIL') +" +``` + +On **FAIL**, do not merge: re-dispatch the extraction with the same mandatory list and a +sharper completeness instruction naming the missing IDs. Two consecutive failures — stop +and report to the user rather than merging a regressed graph. + +**The `to_json` shrink guard (#479) stays authoritative.** It is the backstop, not a +nuisance. Never pass `force=True` to clear it until you have diffed the old and new node +sets and can name why each removed node is legitimately gone (file deleted, section +removed, ID renamed with a verified replacement). ID churn on a re-extracted file is a +legitimate shrink; a missing section is not. If no new files exist (only deletions), create an empty extraction so the merge step can prune: diff --git a/tools/skillgen/expected/graphify__skills__vscode__references__update.md b/tools/skillgen/expected/graphify__skills__vscode__references__update.md index 3632fd412..b5b16a731 100644 --- a/tools/skillgen/expected/graphify__skills__vscode__references__update.md +++ b/tools/skillgen/expected/graphify__skills__vscode__references__update.md @@ -65,6 +65,71 @@ If `code_only` is True: print `[graphify update] Code-only changes detected - sk If `code_only` is False (any changed file is a doc/paper/image/video): **first, if any changed file is in `new_files['video']`, run `references/transcribe.md` (Step 2.5) on those files, then rewrite `.graphify_detect.json` to move the resulting transcript paths into `files['document']` and drop `files['video']`** — otherwise raw `.mp4/.mp3` paths are fed to semantic subagents as unreadable media (#1392). Then run the full Steps 3A–3C pipeline as normal. +### REQUIRED: mandatory-ID gate for re-extracted docs + +Semantic re-extraction of an already-graphed doc is non-deterministic: the same file can +yield 75 nodes on one run and 49 on the next, dropping whole sections with no error. The +loss lands in `build_merge` and only surfaces later as a question the graph can no longer +answer. Apply this gate to **every changed doc/paper that already has nodes in +`graph.json`**. Do not skip it for a small edit — a one-line change re-extracts the whole +file. + +**1 — Snapshot the file's existing node IDs before extracting.** + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +TARGET = 'DOC_PATH' # substitute: the changed doc, as it appears in source_file +g = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) +ids = sorted(n['id'] for n in g['nodes'] if str(n.get('source_file','')).endswith(TARGET)) +Path('graphify-out/.graphify_must_ids.txt').write_text('\n'.join(ids), encoding='utf-8') +print(f'baseline: {len(ids)} existing node(s) for {TARGET}') +" +``` + +A count of 0 means the file is new — skip the gate and extract normally. + +**2 — Pass the IDs to the subagent as mandatory.** Append to the prompt from +`references/extraction-spec.md`: + +- the baseline count as an explicit target ("the prior build extracted N nodes from this + file; land at approximately N — under 90% of N means you under-extracted, go back and + cover the sections you skimmed"), +- the full ID list verbatim, labelled **MANDATORY — every one of these existed in the + prior graph and MUST appear in your output, reusing the ID verbatim**, +- an instruction to verify every mandatory ID is present *before* writing CHUNK_PATH. + +Reusing the IDs also suppresses gratuitous ID churn on re-extraction, which orphans saved +queries and inflates the merge diff for no semantic gain. + +**3 — Hard-gate the merge.** After the chunk lands and before `build_merge`, require both: +node count ≥ 90% of baseline, and zero mandatory IDs missing. + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +d = json.loads(Path('graphify-out/.graphify_chunk_01.json').read_text(encoding='utf-8')) +must = [l for l in Path('graphify-out/.graphify_must_ids.txt').read_text(encoding='utf-8').split() if l] +ids = {n['id'] for n in d['nodes']} +missing = [m for m in must if m not in ids] +ok = len(d['nodes']) >= 0.9 * len(must) and not missing +print(f'nodes={len(d[\"nodes\"])} baseline={len(must)} missing={len(missing)}') +if missing: print('MISSING:', ' '.join(missing[:20])) +print('GATE:', 'PASS' if ok else 'FAIL') +" +``` + +On **FAIL**, do not merge: re-dispatch the extraction with the same mandatory list and a +sharper completeness instruction naming the missing IDs. Two consecutive failures — stop +and report to the user rather than merging a regressed graph. + +**The `to_json` shrink guard (#479) stays authoritative.** It is the backstop, not a +nuisance. Never pass `force=True` to clear it until you have diffed the old and new node +sets and can name why each removed node is legitimately gone (file deleted, section +removed, ID renamed with a verified replacement). ID churn on a re-extracted file is a +legitimate shrink; a missing section is not. If no new files exist (only deletions), create an empty extraction so the merge step can prune: diff --git a/tools/skillgen/expected/graphify__skills__windows__references__update.md b/tools/skillgen/expected/graphify__skills__windows__references__update.md index 3632fd412..b5b16a731 100644 --- a/tools/skillgen/expected/graphify__skills__windows__references__update.md +++ b/tools/skillgen/expected/graphify__skills__windows__references__update.md @@ -65,6 +65,71 @@ If `code_only` is True: print `[graphify update] Code-only changes detected - sk If `code_only` is False (any changed file is a doc/paper/image/video): **first, if any changed file is in `new_files['video']`, run `references/transcribe.md` (Step 2.5) on those files, then rewrite `.graphify_detect.json` to move the resulting transcript paths into `files['document']` and drop `files['video']`** — otherwise raw `.mp4/.mp3` paths are fed to semantic subagents as unreadable media (#1392). Then run the full Steps 3A–3C pipeline as normal. +### REQUIRED: mandatory-ID gate for re-extracted docs + +Semantic re-extraction of an already-graphed doc is non-deterministic: the same file can +yield 75 nodes on one run and 49 on the next, dropping whole sections with no error. The +loss lands in `build_merge` and only surfaces later as a question the graph can no longer +answer. Apply this gate to **every changed doc/paper that already has nodes in +`graph.json`**. Do not skip it for a small edit — a one-line change re-extracts the whole +file. + +**1 — Snapshot the file's existing node IDs before extracting.** + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +TARGET = 'DOC_PATH' # substitute: the changed doc, as it appears in source_file +g = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) +ids = sorted(n['id'] for n in g['nodes'] if str(n.get('source_file','')).endswith(TARGET)) +Path('graphify-out/.graphify_must_ids.txt').write_text('\n'.join(ids), encoding='utf-8') +print(f'baseline: {len(ids)} existing node(s) for {TARGET}') +" +``` + +A count of 0 means the file is new — skip the gate and extract normally. + +**2 — Pass the IDs to the subagent as mandatory.** Append to the prompt from +`references/extraction-spec.md`: + +- the baseline count as an explicit target ("the prior build extracted N nodes from this + file; land at approximately N — under 90% of N means you under-extracted, go back and + cover the sections you skimmed"), +- the full ID list verbatim, labelled **MANDATORY — every one of these existed in the + prior graph and MUST appear in your output, reusing the ID verbatim**, +- an instruction to verify every mandatory ID is present *before* writing CHUNK_PATH. + +Reusing the IDs also suppresses gratuitous ID churn on re-extraction, which orphans saved +queries and inflates the merge diff for no semantic gain. + +**3 — Hard-gate the merge.** After the chunk lands and before `build_merge`, require both: +node count ≥ 90% of baseline, and zero mandatory IDs missing. + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +d = json.loads(Path('graphify-out/.graphify_chunk_01.json').read_text(encoding='utf-8')) +must = [l for l in Path('graphify-out/.graphify_must_ids.txt').read_text(encoding='utf-8').split() if l] +ids = {n['id'] for n in d['nodes']} +missing = [m for m in must if m not in ids] +ok = len(d['nodes']) >= 0.9 * len(must) and not missing +print(f'nodes={len(d[\"nodes\"])} baseline={len(must)} missing={len(missing)}') +if missing: print('MISSING:', ' '.join(missing[:20])) +print('GATE:', 'PASS' if ok else 'FAIL') +" +``` + +On **FAIL**, do not merge: re-dispatch the extraction with the same mandatory list and a +sharper completeness instruction naming the missing IDs. Two consecutive failures — stop +and report to the user rather than merging a regressed graph. + +**The `to_json` shrink guard (#479) stays authoritative.** It is the backstop, not a +nuisance. Never pass `force=True` to clear it until you have diffed the old and new node +sets and can name why each removed node is legitimately gone (file deleted, section +removed, ID renamed with a verified replacement). ID churn on a re-extracted file is a +legitimate shrink; a missing section is not. If no new files exist (only deletions), create an empty extraction so the merge step can prune: diff --git a/tools/skillgen/fragments/core/core.md b/tools/skillgen/fragments/core/core.md index e28910728..27e33978e 100644 --- a/tools/skillgen/fragments/core/core.md +++ b/tools/skillgen/fragments/core/core.md @@ -602,6 +602,8 @@ fi Both are non-default subcommands. `--update` re-extracts only new or changed files; `--cluster-only` reruns clustering on the existing graph. See `references/update.md` for both flows. +> **Re-extracting an already-graphed doc requires the mandatory-ID gate** (`references/update.md` → "REQUIRED: mandatory-ID gate for re-extracted docs"): snapshot that file's existing node IDs, pass them to the subagent as must-include, and refuse the merge if the node count drops below 90% of baseline or any ID is missing. Semantic re-extraction is non-deterministic and drops whole sections without it. + --- ## For /graphify query diff --git a/tools/skillgen/fragments/references/shared/update.md b/tools/skillgen/fragments/references/shared/update.md index 3632fd412..b5b16a731 100644 --- a/tools/skillgen/fragments/references/shared/update.md +++ b/tools/skillgen/fragments/references/shared/update.md @@ -65,6 +65,71 @@ If `code_only` is True: print `[graphify update] Code-only changes detected - sk If `code_only` is False (any changed file is a doc/paper/image/video): **first, if any changed file is in `new_files['video']`, run `references/transcribe.md` (Step 2.5) on those files, then rewrite `.graphify_detect.json` to move the resulting transcript paths into `files['document']` and drop `files['video']`** — otherwise raw `.mp4/.mp3` paths are fed to semantic subagents as unreadable media (#1392). Then run the full Steps 3A–3C pipeline as normal. +### REQUIRED: mandatory-ID gate for re-extracted docs + +Semantic re-extraction of an already-graphed doc is non-deterministic: the same file can +yield 75 nodes on one run and 49 on the next, dropping whole sections with no error. The +loss lands in `build_merge` and only surfaces later as a question the graph can no longer +answer. Apply this gate to **every changed doc/paper that already has nodes in +`graph.json`**. Do not skip it for a small edit — a one-line change re-extracts the whole +file. + +**1 — Snapshot the file's existing node IDs before extracting.** + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +TARGET = 'DOC_PATH' # substitute: the changed doc, as it appears in source_file +g = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8')) +ids = sorted(n['id'] for n in g['nodes'] if str(n.get('source_file','')).endswith(TARGET)) +Path('graphify-out/.graphify_must_ids.txt').write_text('\n'.join(ids), encoding='utf-8') +print(f'baseline: {len(ids)} existing node(s) for {TARGET}') +" +``` + +A count of 0 means the file is new — skip the gate and extract normally. + +**2 — Pass the IDs to the subagent as mandatory.** Append to the prompt from +`references/extraction-spec.md`: + +- the baseline count as an explicit target ("the prior build extracted N nodes from this + file; land at approximately N — under 90% of N means you under-extracted, go back and + cover the sections you skimmed"), +- the full ID list verbatim, labelled **MANDATORY — every one of these existed in the + prior graph and MUST appear in your output, reusing the ID verbatim**, +- an instruction to verify every mandatory ID is present *before* writing CHUNK_PATH. + +Reusing the IDs also suppresses gratuitous ID churn on re-extraction, which orphans saved +queries and inflates the merge diff for no semantic gain. + +**3 — Hard-gate the merge.** After the chunk lands and before `build_merge`, require both: +node count ≥ 90% of baseline, and zero mandatory IDs missing. + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +d = json.loads(Path('graphify-out/.graphify_chunk_01.json').read_text(encoding='utf-8')) +must = [l for l in Path('graphify-out/.graphify_must_ids.txt').read_text(encoding='utf-8').split() if l] +ids = {n['id'] for n in d['nodes']} +missing = [m for m in must if m not in ids] +ok = len(d['nodes']) >= 0.9 * len(must) and not missing +print(f'nodes={len(d[\"nodes\"])} baseline={len(must)} missing={len(missing)}') +if missing: print('MISSING:', ' '.join(missing[:20])) +print('GATE:', 'PASS' if ok else 'FAIL') +" +``` + +On **FAIL**, do not merge: re-dispatch the extraction with the same mandatory list and a +sharper completeness instruction naming the missing IDs. Two consecutive failures — stop +and report to the user rather than merging a regressed graph. + +**The `to_json` shrink guard (#479) stays authoritative.** It is the backstop, not a +nuisance. Never pass `force=True` to clear it until you have diffed the old and new node +sets and can name why each removed node is legitimately gone (file deleted, section +removed, ID renamed with a verified replacement). ID churn on a re-extracted file is a +legitimate shrink; a missing section is not. If no new files exist (only deletions), create an empty extraction so the merge step can prune: