From b59f1e2705c0ccc611a5e02f06564e05a4b82d05 Mon Sep 17 00:00:00 2001 From: HerenderKumar Date: Mon, 20 Jul 2026 00:03:02 +0530 Subject: [PATCH] fix(skill): per-type incremental kinds for the --update runbook (#2033) The --update runbook called detect_incremental() with the default kind="semantic", so on an AST-built corpus (code files never receive a semantic_hash) every file reported as changed and a 12-file real delta became a 629-file full-corpus re-extraction. Passing plain kind="ast" instead would skip the semantic catch-up for docs an AST-only rebuild downgraded (#2014), so neither existing mode fits a pipeline where code is structural-only and docs are semantic. detect_incremental() gains kind="auto": code files compare against ast_hash, everything else against semantic_hash. The runbook (shared update reference plus the aider/devin monoliths) now detects with it. The runbook's save_manifest() call had the matching stamping defect: the kind="both" default forged a semantic_hash for every corpus file, including docs never semantically extracted, silently cancelling their future catch-up. The merge step now stamps kind="ast" over the corpus, then kind="semantic" over just the changed docs/papers/images/videos the semantic pass actually covered; on a code-only update that set is empty, leaving exactly the kind="ast"-only stamp the issue asks for. skillgen: the monolith round-trip sanctions the new detection lines via _is_incremental_kind_fix_line; artifacts and expected/ regenerated. --- graphify/detect.py | 12 +++- graphify/skill-aider.md | 5 +- graphify/skill-devin.md | 5 +- graphify/skills/agents/references/update.md | 21 ++++++- graphify/skills/amp/references/update.md | 21 ++++++- graphify/skills/claude/references/update.md | 21 ++++++- graphify/skills/claw/references/update.md | 21 ++++++- graphify/skills/codex/references/update.md | 21 ++++++- graphify/skills/copilot/references/update.md | 21 ++++++- graphify/skills/droid/references/update.md | 21 ++++++- graphify/skills/kilo/references/update.md | 21 ++++++- graphify/skills/kiro/references/update.md | 21 ++++++- graphify/skills/opencode/references/update.md | 21 ++++++- graphify/skills/pi/references/update.md | 21 ++++++- graphify/skills/trae/references/update.md | 21 ++++++- graphify/skills/vscode/references/update.md | 21 ++++++- graphify/skills/windows/references/update.md | 21 ++++++- tests/test_detect.py | 63 +++++++++++++++++++ .../expected/graphify__skill-aider.md | 5 +- .../expected/graphify__skill-devin.md | 5 +- ...ify__skills__agents__references__update.md | 21 ++++++- ...aphify__skills__amp__references__update.md | 21 ++++++- ...ify__skills__claude__references__update.md | 21 ++++++- ...phify__skills__claw__references__update.md | 21 ++++++- ...hify__skills__codex__references__update.md | 21 ++++++- ...fy__skills__copilot__references__update.md | 21 ++++++- ...hify__skills__droid__references__update.md | 21 ++++++- ...phify__skills__kilo__references__update.md | 21 ++++++- ...phify__skills__kiro__references__update.md | 21 ++++++- ...y__skills__opencode__references__update.md | 21 ++++++- ...raphify__skills__pi__references__update.md | 21 ++++++- ...phify__skills__trae__references__update.md | 21 ++++++- ...ify__skills__vscode__references__update.md | 21 ++++++- ...fy__skills__windows__references__update.md | 21 ++++++- tools/skillgen/fragments/core/aider.md | 5 +- tools/skillgen/fragments/core/devin.md | 5 +- .../fragments/references/shared/update.md | 21 ++++++- tools/skillgen/gen.py | 24 +++++++ 38 files changed, 673 insertions(+), 65 deletions(-) diff --git a/graphify/detect.py b/graphify/detect.py index c2638a0ef..dd842474d 100644 --- a/graphify/detect.py +++ b/graphify/detect.py @@ -1706,6 +1706,13 @@ def detect_incremental( semantically. kind="ast": a file is "changed" when its ast_hash is missing or its content has changed. Use this for `graphify update`. + kind="auto": per-file-type — code files compare against ast_hash, + everything else (docs/papers/images/videos) against semantic_hash. + Use this for the skill's `--update` runbook (#2033): code never + receives semantic extraction, so its missing semantic_hash is not a + pending obligation and must not re-queue an AST-built corpus, while + docs downgraded by an AST-only rebuild still get their semantic + catch-up. Fast path: mtime unchanged + hash matches → unchanged (free, no disk IO beyond stat). Slow path: mtime bumped → compare MD5 against the relevant @@ -1765,7 +1772,10 @@ def detect_incremental( # Normalise legacy {mtime, hash} to new schema if "hash" in stored and "ast_hash" not in stored: stored = {"mtime": stored.get("mtime", 0), "ast_hash": stored["hash"], "semantic_hash": ""} - hash_key = "semantic_hash" if kind == "semantic" else "ast_hash" + if kind == "auto": + hash_key = "ast_hash" if ftype == "code" else "semantic_hash" + else: + hash_key = "semantic_hash" if kind == "semantic" else "ast_hash" stored_hash = stored.get(hash_key, "") # Missing semantic_hash means update ran but extract hasn't — always re-extract if not stored_hash: diff --git a/graphify/skill-aider.md b/graphify/skill-aider.md index 8db8f73f2..4be4eccd4 100644 --- a/graphify/skill-aider.md +++ b/graphify/skill-aider.md @@ -748,7 +748,10 @@ import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path -result = detect_incremental(Path('INPUT_PATH')) +# kind='auto': ast_hash for code, semantic_hash for docs/papers/images. The +# semantic default would report every code file of an AST-built corpus as +# changed and re-extract the whole corpus (#2033). +result = detect_incremental(Path('INPUT_PATH'), kind='auto') new_total = result.get('new_total', 0) print(json.dumps(result, indent=2)) Path('.graphify_incremental.json').write_text(json.dumps(result)) diff --git a/graphify/skill-devin.md b/graphify/skill-devin.md index c444a852d..2c9d57f8d 100644 --- a/graphify/skill-devin.md +++ b/graphify/skill-devin.md @@ -887,7 +887,10 @@ import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path -result = detect_incremental(Path('INPUT_PATH')) +# kind='auto': ast_hash for code, semantic_hash for docs/papers/images. The +# semantic default would report every code file of an AST-built corpus as +# changed and re-extract the whole corpus (#2033). +result = detect_incremental(Path('INPUT_PATH'), kind='auto') new_total = result.get('new_total', 0) print(json.dumps(result, indent=2)) Path('graphify-out/.graphify_incremental.json').write_text(json.dumps(result)) diff --git a/graphify/skills/agents/references/update.md b/graphify/skills/agents/references/update.md index fa2612180..f7db138a6 100644 --- a/graphify/skills/agents/references/update.md +++ b/graphify/skills/agents/references/update.md @@ -12,7 +12,13 @@ import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path -result = detect_incremental(Path('INPUT_PATH')) +# kind='auto' compares code files against ast_hash and docs/papers/images +# against semantic_hash. The default (kind='semantic') reports every code file +# of an AST-built corpus as changed — code never gets a semantic_hash — turning +# a 12-file delta into a full-corpus re-extraction (#2033); plain kind='ast' +# would instead skip the semantic catch-up for docs an AST-only rebuild +# downgraded (#2014). Per-type is the only mode that matches this pipeline. +result = detect_incremental(Path('INPUT_PATH'), kind='auto') new_total = result.get('new_total', 0) print(json.dumps(result, indent=2, ensure_ascii=False)) Path('graphify-out/.graphify_incremental.json').write_text(json.dumps(result, ensure_ascii=False), encoding=\"utf-8\") @@ -142,7 +148,18 @@ print(f'[graphify update] Merged extraction written ({len(merged_out[\"nodes\"]) # root= matches the build_merge call above so the manifest keys stay relative to # the scan root — portable across clones/machines, so --update keeps matching # cached files instead of missing every one after a move (#1417). -save_manifest(incremental['files'], root='INPUT_PATH') +# Two stamps, not one kind='both' pass (#2033): 'both' would forge a +# semantic_hash for every file in the corpus, including docs this run never +# semantically extracted, silently cancelling their future catch-up. +# 1) kind='ast' over the full corpus: refreshes ast_hash; an untouched file +# keeps its semantic_hash, a changed one has it cleared. +# 2) kind='semantic' over just the files the semantic pass covered this run — +# the changed docs/papers/images (and transcribed videos). On a code-only +# update that set is empty and the second stamp is skipped. +save_manifest(incremental['files'], root='INPUT_PATH', kind='ast') +sem_files = {t: incremental.get('new_files', {}).get(t, []) for t in ('document', 'paper', 'image', 'video')} +if any(sem_files.values()): + save_manifest(sem_files, root='INPUT_PATH', kind='semantic') print('[graphify update] Manifest saved.') " ``` diff --git a/graphify/skills/amp/references/update.md b/graphify/skills/amp/references/update.md index fa2612180..f7db138a6 100644 --- a/graphify/skills/amp/references/update.md +++ b/graphify/skills/amp/references/update.md @@ -12,7 +12,13 @@ import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path -result = detect_incremental(Path('INPUT_PATH')) +# kind='auto' compares code files against ast_hash and docs/papers/images +# against semantic_hash. The default (kind='semantic') reports every code file +# of an AST-built corpus as changed — code never gets a semantic_hash — turning +# a 12-file delta into a full-corpus re-extraction (#2033); plain kind='ast' +# would instead skip the semantic catch-up for docs an AST-only rebuild +# downgraded (#2014). Per-type is the only mode that matches this pipeline. +result = detect_incremental(Path('INPUT_PATH'), kind='auto') new_total = result.get('new_total', 0) print(json.dumps(result, indent=2, ensure_ascii=False)) Path('graphify-out/.graphify_incremental.json').write_text(json.dumps(result, ensure_ascii=False), encoding=\"utf-8\") @@ -142,7 +148,18 @@ print(f'[graphify update] Merged extraction written ({len(merged_out[\"nodes\"]) # root= matches the build_merge call above so the manifest keys stay relative to # the scan root — portable across clones/machines, so --update keeps matching # cached files instead of missing every one after a move (#1417). -save_manifest(incremental['files'], root='INPUT_PATH') +# Two stamps, not one kind='both' pass (#2033): 'both' would forge a +# semantic_hash for every file in the corpus, including docs this run never +# semantically extracted, silently cancelling their future catch-up. +# 1) kind='ast' over the full corpus: refreshes ast_hash; an untouched file +# keeps its semantic_hash, a changed one has it cleared. +# 2) kind='semantic' over just the files the semantic pass covered this run — +# the changed docs/papers/images (and transcribed videos). On a code-only +# update that set is empty and the second stamp is skipped. +save_manifest(incremental['files'], root='INPUT_PATH', kind='ast') +sem_files = {t: incremental.get('new_files', {}).get(t, []) for t in ('document', 'paper', 'image', 'video')} +if any(sem_files.values()): + save_manifest(sem_files, root='INPUT_PATH', kind='semantic') print('[graphify update] Manifest saved.') " ``` diff --git a/graphify/skills/claude/references/update.md b/graphify/skills/claude/references/update.md index fa2612180..f7db138a6 100644 --- a/graphify/skills/claude/references/update.md +++ b/graphify/skills/claude/references/update.md @@ -12,7 +12,13 @@ import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path -result = detect_incremental(Path('INPUT_PATH')) +# kind='auto' compares code files against ast_hash and docs/papers/images +# against semantic_hash. The default (kind='semantic') reports every code file +# of an AST-built corpus as changed — code never gets a semantic_hash — turning +# a 12-file delta into a full-corpus re-extraction (#2033); plain kind='ast' +# would instead skip the semantic catch-up for docs an AST-only rebuild +# downgraded (#2014). Per-type is the only mode that matches this pipeline. +result = detect_incremental(Path('INPUT_PATH'), kind='auto') new_total = result.get('new_total', 0) print(json.dumps(result, indent=2, ensure_ascii=False)) Path('graphify-out/.graphify_incremental.json').write_text(json.dumps(result, ensure_ascii=False), encoding=\"utf-8\") @@ -142,7 +148,18 @@ print(f'[graphify update] Merged extraction written ({len(merged_out[\"nodes\"]) # root= matches the build_merge call above so the manifest keys stay relative to # the scan root — portable across clones/machines, so --update keeps matching # cached files instead of missing every one after a move (#1417). -save_manifest(incremental['files'], root='INPUT_PATH') +# Two stamps, not one kind='both' pass (#2033): 'both' would forge a +# semantic_hash for every file in the corpus, including docs this run never +# semantically extracted, silently cancelling their future catch-up. +# 1) kind='ast' over the full corpus: refreshes ast_hash; an untouched file +# keeps its semantic_hash, a changed one has it cleared. +# 2) kind='semantic' over just the files the semantic pass covered this run — +# the changed docs/papers/images (and transcribed videos). On a code-only +# update that set is empty and the second stamp is skipped. +save_manifest(incremental['files'], root='INPUT_PATH', kind='ast') +sem_files = {t: incremental.get('new_files', {}).get(t, []) for t in ('document', 'paper', 'image', 'video')} +if any(sem_files.values()): + save_manifest(sem_files, root='INPUT_PATH', kind='semantic') print('[graphify update] Manifest saved.') " ``` diff --git a/graphify/skills/claw/references/update.md b/graphify/skills/claw/references/update.md index fa2612180..f7db138a6 100644 --- a/graphify/skills/claw/references/update.md +++ b/graphify/skills/claw/references/update.md @@ -12,7 +12,13 @@ import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path -result = detect_incremental(Path('INPUT_PATH')) +# kind='auto' compares code files against ast_hash and docs/papers/images +# against semantic_hash. The default (kind='semantic') reports every code file +# of an AST-built corpus as changed — code never gets a semantic_hash — turning +# a 12-file delta into a full-corpus re-extraction (#2033); plain kind='ast' +# would instead skip the semantic catch-up for docs an AST-only rebuild +# downgraded (#2014). Per-type is the only mode that matches this pipeline. +result = detect_incremental(Path('INPUT_PATH'), kind='auto') new_total = result.get('new_total', 0) print(json.dumps(result, indent=2, ensure_ascii=False)) Path('graphify-out/.graphify_incremental.json').write_text(json.dumps(result, ensure_ascii=False), encoding=\"utf-8\") @@ -142,7 +148,18 @@ print(f'[graphify update] Merged extraction written ({len(merged_out[\"nodes\"]) # root= matches the build_merge call above so the manifest keys stay relative to # the scan root — portable across clones/machines, so --update keeps matching # cached files instead of missing every one after a move (#1417). -save_manifest(incremental['files'], root='INPUT_PATH') +# Two stamps, not one kind='both' pass (#2033): 'both' would forge a +# semantic_hash for every file in the corpus, including docs this run never +# semantically extracted, silently cancelling their future catch-up. +# 1) kind='ast' over the full corpus: refreshes ast_hash; an untouched file +# keeps its semantic_hash, a changed one has it cleared. +# 2) kind='semantic' over just the files the semantic pass covered this run — +# the changed docs/papers/images (and transcribed videos). On a code-only +# update that set is empty and the second stamp is skipped. +save_manifest(incremental['files'], root='INPUT_PATH', kind='ast') +sem_files = {t: incremental.get('new_files', {}).get(t, []) for t in ('document', 'paper', 'image', 'video')} +if any(sem_files.values()): + save_manifest(sem_files, root='INPUT_PATH', kind='semantic') print('[graphify update] Manifest saved.') " ``` diff --git a/graphify/skills/codex/references/update.md b/graphify/skills/codex/references/update.md index fa2612180..f7db138a6 100644 --- a/graphify/skills/codex/references/update.md +++ b/graphify/skills/codex/references/update.md @@ -12,7 +12,13 @@ import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path -result = detect_incremental(Path('INPUT_PATH')) +# kind='auto' compares code files against ast_hash and docs/papers/images +# against semantic_hash. The default (kind='semantic') reports every code file +# of an AST-built corpus as changed — code never gets a semantic_hash — turning +# a 12-file delta into a full-corpus re-extraction (#2033); plain kind='ast' +# would instead skip the semantic catch-up for docs an AST-only rebuild +# downgraded (#2014). Per-type is the only mode that matches this pipeline. +result = detect_incremental(Path('INPUT_PATH'), kind='auto') new_total = result.get('new_total', 0) print(json.dumps(result, indent=2, ensure_ascii=False)) Path('graphify-out/.graphify_incremental.json').write_text(json.dumps(result, ensure_ascii=False), encoding=\"utf-8\") @@ -142,7 +148,18 @@ print(f'[graphify update] Merged extraction written ({len(merged_out[\"nodes\"]) # root= matches the build_merge call above so the manifest keys stay relative to # the scan root — portable across clones/machines, so --update keeps matching # cached files instead of missing every one after a move (#1417). -save_manifest(incremental['files'], root='INPUT_PATH') +# Two stamps, not one kind='both' pass (#2033): 'both' would forge a +# semantic_hash for every file in the corpus, including docs this run never +# semantically extracted, silently cancelling their future catch-up. +# 1) kind='ast' over the full corpus: refreshes ast_hash; an untouched file +# keeps its semantic_hash, a changed one has it cleared. +# 2) kind='semantic' over just the files the semantic pass covered this run — +# the changed docs/papers/images (and transcribed videos). On a code-only +# update that set is empty and the second stamp is skipped. +save_manifest(incremental['files'], root='INPUT_PATH', kind='ast') +sem_files = {t: incremental.get('new_files', {}).get(t, []) for t in ('document', 'paper', 'image', 'video')} +if any(sem_files.values()): + save_manifest(sem_files, root='INPUT_PATH', kind='semantic') print('[graphify update] Manifest saved.') " ``` diff --git a/graphify/skills/copilot/references/update.md b/graphify/skills/copilot/references/update.md index fa2612180..f7db138a6 100644 --- a/graphify/skills/copilot/references/update.md +++ b/graphify/skills/copilot/references/update.md @@ -12,7 +12,13 @@ import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path -result = detect_incremental(Path('INPUT_PATH')) +# kind='auto' compares code files against ast_hash and docs/papers/images +# against semantic_hash. The default (kind='semantic') reports every code file +# of an AST-built corpus as changed — code never gets a semantic_hash — turning +# a 12-file delta into a full-corpus re-extraction (#2033); plain kind='ast' +# would instead skip the semantic catch-up for docs an AST-only rebuild +# downgraded (#2014). Per-type is the only mode that matches this pipeline. +result = detect_incremental(Path('INPUT_PATH'), kind='auto') new_total = result.get('new_total', 0) print(json.dumps(result, indent=2, ensure_ascii=False)) Path('graphify-out/.graphify_incremental.json').write_text(json.dumps(result, ensure_ascii=False), encoding=\"utf-8\") @@ -142,7 +148,18 @@ print(f'[graphify update] Merged extraction written ({len(merged_out[\"nodes\"]) # root= matches the build_merge call above so the manifest keys stay relative to # the scan root — portable across clones/machines, so --update keeps matching # cached files instead of missing every one after a move (#1417). -save_manifest(incremental['files'], root='INPUT_PATH') +# Two stamps, not one kind='both' pass (#2033): 'both' would forge a +# semantic_hash for every file in the corpus, including docs this run never +# semantically extracted, silently cancelling their future catch-up. +# 1) kind='ast' over the full corpus: refreshes ast_hash; an untouched file +# keeps its semantic_hash, a changed one has it cleared. +# 2) kind='semantic' over just the files the semantic pass covered this run — +# the changed docs/papers/images (and transcribed videos). On a code-only +# update that set is empty and the second stamp is skipped. +save_manifest(incremental['files'], root='INPUT_PATH', kind='ast') +sem_files = {t: incremental.get('new_files', {}).get(t, []) for t in ('document', 'paper', 'image', 'video')} +if any(sem_files.values()): + save_manifest(sem_files, root='INPUT_PATH', kind='semantic') print('[graphify update] Manifest saved.') " ``` diff --git a/graphify/skills/droid/references/update.md b/graphify/skills/droid/references/update.md index fa2612180..f7db138a6 100644 --- a/graphify/skills/droid/references/update.md +++ b/graphify/skills/droid/references/update.md @@ -12,7 +12,13 @@ import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path -result = detect_incremental(Path('INPUT_PATH')) +# kind='auto' compares code files against ast_hash and docs/papers/images +# against semantic_hash. The default (kind='semantic') reports every code file +# of an AST-built corpus as changed — code never gets a semantic_hash — turning +# a 12-file delta into a full-corpus re-extraction (#2033); plain kind='ast' +# would instead skip the semantic catch-up for docs an AST-only rebuild +# downgraded (#2014). Per-type is the only mode that matches this pipeline. +result = detect_incremental(Path('INPUT_PATH'), kind='auto') new_total = result.get('new_total', 0) print(json.dumps(result, indent=2, ensure_ascii=False)) Path('graphify-out/.graphify_incremental.json').write_text(json.dumps(result, ensure_ascii=False), encoding=\"utf-8\") @@ -142,7 +148,18 @@ print(f'[graphify update] Merged extraction written ({len(merged_out[\"nodes\"]) # root= matches the build_merge call above so the manifest keys stay relative to # the scan root — portable across clones/machines, so --update keeps matching # cached files instead of missing every one after a move (#1417). -save_manifest(incremental['files'], root='INPUT_PATH') +# Two stamps, not one kind='both' pass (#2033): 'both' would forge a +# semantic_hash for every file in the corpus, including docs this run never +# semantically extracted, silently cancelling their future catch-up. +# 1) kind='ast' over the full corpus: refreshes ast_hash; an untouched file +# keeps its semantic_hash, a changed one has it cleared. +# 2) kind='semantic' over just the files the semantic pass covered this run — +# the changed docs/papers/images (and transcribed videos). On a code-only +# update that set is empty and the second stamp is skipped. +save_manifest(incremental['files'], root='INPUT_PATH', kind='ast') +sem_files = {t: incremental.get('new_files', {}).get(t, []) for t in ('document', 'paper', 'image', 'video')} +if any(sem_files.values()): + save_manifest(sem_files, root='INPUT_PATH', kind='semantic') print('[graphify update] Manifest saved.') " ``` diff --git a/graphify/skills/kilo/references/update.md b/graphify/skills/kilo/references/update.md index fa2612180..f7db138a6 100644 --- a/graphify/skills/kilo/references/update.md +++ b/graphify/skills/kilo/references/update.md @@ -12,7 +12,13 @@ import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path -result = detect_incremental(Path('INPUT_PATH')) +# kind='auto' compares code files against ast_hash and docs/papers/images +# against semantic_hash. The default (kind='semantic') reports every code file +# of an AST-built corpus as changed — code never gets a semantic_hash — turning +# a 12-file delta into a full-corpus re-extraction (#2033); plain kind='ast' +# would instead skip the semantic catch-up for docs an AST-only rebuild +# downgraded (#2014). Per-type is the only mode that matches this pipeline. +result = detect_incremental(Path('INPUT_PATH'), kind='auto') new_total = result.get('new_total', 0) print(json.dumps(result, indent=2, ensure_ascii=False)) Path('graphify-out/.graphify_incremental.json').write_text(json.dumps(result, ensure_ascii=False), encoding=\"utf-8\") @@ -142,7 +148,18 @@ print(f'[graphify update] Merged extraction written ({len(merged_out[\"nodes\"]) # root= matches the build_merge call above so the manifest keys stay relative to # the scan root — portable across clones/machines, so --update keeps matching # cached files instead of missing every one after a move (#1417). -save_manifest(incremental['files'], root='INPUT_PATH') +# Two stamps, not one kind='both' pass (#2033): 'both' would forge a +# semantic_hash for every file in the corpus, including docs this run never +# semantically extracted, silently cancelling their future catch-up. +# 1) kind='ast' over the full corpus: refreshes ast_hash; an untouched file +# keeps its semantic_hash, a changed one has it cleared. +# 2) kind='semantic' over just the files the semantic pass covered this run — +# the changed docs/papers/images (and transcribed videos). On a code-only +# update that set is empty and the second stamp is skipped. +save_manifest(incremental['files'], root='INPUT_PATH', kind='ast') +sem_files = {t: incremental.get('new_files', {}).get(t, []) for t in ('document', 'paper', 'image', 'video')} +if any(sem_files.values()): + save_manifest(sem_files, root='INPUT_PATH', kind='semantic') print('[graphify update] Manifest saved.') " ``` diff --git a/graphify/skills/kiro/references/update.md b/graphify/skills/kiro/references/update.md index fa2612180..f7db138a6 100644 --- a/graphify/skills/kiro/references/update.md +++ b/graphify/skills/kiro/references/update.md @@ -12,7 +12,13 @@ import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path -result = detect_incremental(Path('INPUT_PATH')) +# kind='auto' compares code files against ast_hash and docs/papers/images +# against semantic_hash. The default (kind='semantic') reports every code file +# of an AST-built corpus as changed — code never gets a semantic_hash — turning +# a 12-file delta into a full-corpus re-extraction (#2033); plain kind='ast' +# would instead skip the semantic catch-up for docs an AST-only rebuild +# downgraded (#2014). Per-type is the only mode that matches this pipeline. +result = detect_incremental(Path('INPUT_PATH'), kind='auto') new_total = result.get('new_total', 0) print(json.dumps(result, indent=2, ensure_ascii=False)) Path('graphify-out/.graphify_incremental.json').write_text(json.dumps(result, ensure_ascii=False), encoding=\"utf-8\") @@ -142,7 +148,18 @@ print(f'[graphify update] Merged extraction written ({len(merged_out[\"nodes\"]) # root= matches the build_merge call above so the manifest keys stay relative to # the scan root — portable across clones/machines, so --update keeps matching # cached files instead of missing every one after a move (#1417). -save_manifest(incremental['files'], root='INPUT_PATH') +# Two stamps, not one kind='both' pass (#2033): 'both' would forge a +# semantic_hash for every file in the corpus, including docs this run never +# semantically extracted, silently cancelling their future catch-up. +# 1) kind='ast' over the full corpus: refreshes ast_hash; an untouched file +# keeps its semantic_hash, a changed one has it cleared. +# 2) kind='semantic' over just the files the semantic pass covered this run — +# the changed docs/papers/images (and transcribed videos). On a code-only +# update that set is empty and the second stamp is skipped. +save_manifest(incremental['files'], root='INPUT_PATH', kind='ast') +sem_files = {t: incremental.get('new_files', {}).get(t, []) for t in ('document', 'paper', 'image', 'video')} +if any(sem_files.values()): + save_manifest(sem_files, root='INPUT_PATH', kind='semantic') print('[graphify update] Manifest saved.') " ``` diff --git a/graphify/skills/opencode/references/update.md b/graphify/skills/opencode/references/update.md index fa2612180..f7db138a6 100644 --- a/graphify/skills/opencode/references/update.md +++ b/graphify/skills/opencode/references/update.md @@ -12,7 +12,13 @@ import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path -result = detect_incremental(Path('INPUT_PATH')) +# kind='auto' compares code files against ast_hash and docs/papers/images +# against semantic_hash. The default (kind='semantic') reports every code file +# of an AST-built corpus as changed — code never gets a semantic_hash — turning +# a 12-file delta into a full-corpus re-extraction (#2033); plain kind='ast' +# would instead skip the semantic catch-up for docs an AST-only rebuild +# downgraded (#2014). Per-type is the only mode that matches this pipeline. +result = detect_incremental(Path('INPUT_PATH'), kind='auto') new_total = result.get('new_total', 0) print(json.dumps(result, indent=2, ensure_ascii=False)) Path('graphify-out/.graphify_incremental.json').write_text(json.dumps(result, ensure_ascii=False), encoding=\"utf-8\") @@ -142,7 +148,18 @@ print(f'[graphify update] Merged extraction written ({len(merged_out[\"nodes\"]) # root= matches the build_merge call above so the manifest keys stay relative to # the scan root — portable across clones/machines, so --update keeps matching # cached files instead of missing every one after a move (#1417). -save_manifest(incremental['files'], root='INPUT_PATH') +# Two stamps, not one kind='both' pass (#2033): 'both' would forge a +# semantic_hash for every file in the corpus, including docs this run never +# semantically extracted, silently cancelling their future catch-up. +# 1) kind='ast' over the full corpus: refreshes ast_hash; an untouched file +# keeps its semantic_hash, a changed one has it cleared. +# 2) kind='semantic' over just the files the semantic pass covered this run — +# the changed docs/papers/images (and transcribed videos). On a code-only +# update that set is empty and the second stamp is skipped. +save_manifest(incremental['files'], root='INPUT_PATH', kind='ast') +sem_files = {t: incremental.get('new_files', {}).get(t, []) for t in ('document', 'paper', 'image', 'video')} +if any(sem_files.values()): + save_manifest(sem_files, root='INPUT_PATH', kind='semantic') print('[graphify update] Manifest saved.') " ``` diff --git a/graphify/skills/pi/references/update.md b/graphify/skills/pi/references/update.md index fa2612180..f7db138a6 100644 --- a/graphify/skills/pi/references/update.md +++ b/graphify/skills/pi/references/update.md @@ -12,7 +12,13 @@ import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path -result = detect_incremental(Path('INPUT_PATH')) +# kind='auto' compares code files against ast_hash and docs/papers/images +# against semantic_hash. The default (kind='semantic') reports every code file +# of an AST-built corpus as changed — code never gets a semantic_hash — turning +# a 12-file delta into a full-corpus re-extraction (#2033); plain kind='ast' +# would instead skip the semantic catch-up for docs an AST-only rebuild +# downgraded (#2014). Per-type is the only mode that matches this pipeline. +result = detect_incremental(Path('INPUT_PATH'), kind='auto') new_total = result.get('new_total', 0) print(json.dumps(result, indent=2, ensure_ascii=False)) Path('graphify-out/.graphify_incremental.json').write_text(json.dumps(result, ensure_ascii=False), encoding=\"utf-8\") @@ -142,7 +148,18 @@ print(f'[graphify update] Merged extraction written ({len(merged_out[\"nodes\"]) # root= matches the build_merge call above so the manifest keys stay relative to # the scan root — portable across clones/machines, so --update keeps matching # cached files instead of missing every one after a move (#1417). -save_manifest(incremental['files'], root='INPUT_PATH') +# Two stamps, not one kind='both' pass (#2033): 'both' would forge a +# semantic_hash for every file in the corpus, including docs this run never +# semantically extracted, silently cancelling their future catch-up. +# 1) kind='ast' over the full corpus: refreshes ast_hash; an untouched file +# keeps its semantic_hash, a changed one has it cleared. +# 2) kind='semantic' over just the files the semantic pass covered this run — +# the changed docs/papers/images (and transcribed videos). On a code-only +# update that set is empty and the second stamp is skipped. +save_manifest(incremental['files'], root='INPUT_PATH', kind='ast') +sem_files = {t: incremental.get('new_files', {}).get(t, []) for t in ('document', 'paper', 'image', 'video')} +if any(sem_files.values()): + save_manifest(sem_files, root='INPUT_PATH', kind='semantic') print('[graphify update] Manifest saved.') " ``` diff --git a/graphify/skills/trae/references/update.md b/graphify/skills/trae/references/update.md index fa2612180..f7db138a6 100644 --- a/graphify/skills/trae/references/update.md +++ b/graphify/skills/trae/references/update.md @@ -12,7 +12,13 @@ import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path -result = detect_incremental(Path('INPUT_PATH')) +# kind='auto' compares code files against ast_hash and docs/papers/images +# against semantic_hash. The default (kind='semantic') reports every code file +# of an AST-built corpus as changed — code never gets a semantic_hash — turning +# a 12-file delta into a full-corpus re-extraction (#2033); plain kind='ast' +# would instead skip the semantic catch-up for docs an AST-only rebuild +# downgraded (#2014). Per-type is the only mode that matches this pipeline. +result = detect_incremental(Path('INPUT_PATH'), kind='auto') new_total = result.get('new_total', 0) print(json.dumps(result, indent=2, ensure_ascii=False)) Path('graphify-out/.graphify_incremental.json').write_text(json.dumps(result, ensure_ascii=False), encoding=\"utf-8\") @@ -142,7 +148,18 @@ print(f'[graphify update] Merged extraction written ({len(merged_out[\"nodes\"]) # root= matches the build_merge call above so the manifest keys stay relative to # the scan root — portable across clones/machines, so --update keeps matching # cached files instead of missing every one after a move (#1417). -save_manifest(incremental['files'], root='INPUT_PATH') +# Two stamps, not one kind='both' pass (#2033): 'both' would forge a +# semantic_hash for every file in the corpus, including docs this run never +# semantically extracted, silently cancelling their future catch-up. +# 1) kind='ast' over the full corpus: refreshes ast_hash; an untouched file +# keeps its semantic_hash, a changed one has it cleared. +# 2) kind='semantic' over just the files the semantic pass covered this run — +# the changed docs/papers/images (and transcribed videos). On a code-only +# update that set is empty and the second stamp is skipped. +save_manifest(incremental['files'], root='INPUT_PATH', kind='ast') +sem_files = {t: incremental.get('new_files', {}).get(t, []) for t in ('document', 'paper', 'image', 'video')} +if any(sem_files.values()): + save_manifest(sem_files, root='INPUT_PATH', kind='semantic') print('[graphify update] Manifest saved.') " ``` diff --git a/graphify/skills/vscode/references/update.md b/graphify/skills/vscode/references/update.md index fa2612180..f7db138a6 100644 --- a/graphify/skills/vscode/references/update.md +++ b/graphify/skills/vscode/references/update.md @@ -12,7 +12,13 @@ import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path -result = detect_incremental(Path('INPUT_PATH')) +# kind='auto' compares code files against ast_hash and docs/papers/images +# against semantic_hash. The default (kind='semantic') reports every code file +# of an AST-built corpus as changed — code never gets a semantic_hash — turning +# a 12-file delta into a full-corpus re-extraction (#2033); plain kind='ast' +# would instead skip the semantic catch-up for docs an AST-only rebuild +# downgraded (#2014). Per-type is the only mode that matches this pipeline. +result = detect_incremental(Path('INPUT_PATH'), kind='auto') new_total = result.get('new_total', 0) print(json.dumps(result, indent=2, ensure_ascii=False)) Path('graphify-out/.graphify_incremental.json').write_text(json.dumps(result, ensure_ascii=False), encoding=\"utf-8\") @@ -142,7 +148,18 @@ print(f'[graphify update] Merged extraction written ({len(merged_out[\"nodes\"]) # root= matches the build_merge call above so the manifest keys stay relative to # the scan root — portable across clones/machines, so --update keeps matching # cached files instead of missing every one after a move (#1417). -save_manifest(incremental['files'], root='INPUT_PATH') +# Two stamps, not one kind='both' pass (#2033): 'both' would forge a +# semantic_hash for every file in the corpus, including docs this run never +# semantically extracted, silently cancelling their future catch-up. +# 1) kind='ast' over the full corpus: refreshes ast_hash; an untouched file +# keeps its semantic_hash, a changed one has it cleared. +# 2) kind='semantic' over just the files the semantic pass covered this run — +# the changed docs/papers/images (and transcribed videos). On a code-only +# update that set is empty and the second stamp is skipped. +save_manifest(incremental['files'], root='INPUT_PATH', kind='ast') +sem_files = {t: incremental.get('new_files', {}).get(t, []) for t in ('document', 'paper', 'image', 'video')} +if any(sem_files.values()): + save_manifest(sem_files, root='INPUT_PATH', kind='semantic') print('[graphify update] Manifest saved.') " ``` diff --git a/graphify/skills/windows/references/update.md b/graphify/skills/windows/references/update.md index fa2612180..f7db138a6 100644 --- a/graphify/skills/windows/references/update.md +++ b/graphify/skills/windows/references/update.md @@ -12,7 +12,13 @@ import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path -result = detect_incremental(Path('INPUT_PATH')) +# kind='auto' compares code files against ast_hash and docs/papers/images +# against semantic_hash. The default (kind='semantic') reports every code file +# of an AST-built corpus as changed — code never gets a semantic_hash — turning +# a 12-file delta into a full-corpus re-extraction (#2033); plain kind='ast' +# would instead skip the semantic catch-up for docs an AST-only rebuild +# downgraded (#2014). Per-type is the only mode that matches this pipeline. +result = detect_incremental(Path('INPUT_PATH'), kind='auto') new_total = result.get('new_total', 0) print(json.dumps(result, indent=2, ensure_ascii=False)) Path('graphify-out/.graphify_incremental.json').write_text(json.dumps(result, ensure_ascii=False), encoding=\"utf-8\") @@ -142,7 +148,18 @@ print(f'[graphify update] Merged extraction written ({len(merged_out[\"nodes\"]) # root= matches the build_merge call above so the manifest keys stay relative to # the scan root — portable across clones/machines, so --update keeps matching # cached files instead of missing every one after a move (#1417). -save_manifest(incremental['files'], root='INPUT_PATH') +# Two stamps, not one kind='both' pass (#2033): 'both' would forge a +# semantic_hash for every file in the corpus, including docs this run never +# semantically extracted, silently cancelling their future catch-up. +# 1) kind='ast' over the full corpus: refreshes ast_hash; an untouched file +# keeps its semantic_hash, a changed one has it cleared. +# 2) kind='semantic' over just the files the semantic pass covered this run — +# the changed docs/papers/images (and transcribed videos). On a code-only +# update that set is empty and the second stamp is skipped. +save_manifest(incremental['files'], root='INPUT_PATH', kind='ast') +sem_files = {t: incremental.get('new_files', {}).get(t, []) for t in ('document', 'paper', 'image', 'video')} +if any(sem_files.values()): + save_manifest(sem_files, root='INPUT_PATH', kind='semantic') print('[graphify update] Manifest saved.') " ``` diff --git a/tests/test_detect.py b/tests/test_detect.py index 837d8cf3d..5fd5acfbf 100644 --- a/tests/test_detect.py +++ b/tests/test_detect.py @@ -1295,6 +1295,69 @@ def test_save_manifest_without_filter_unchanged_for_code(tmp_path): manifest = json.loads(Path(manifest_path).read_text()) assert str(py) in manifest assert manifest[str(py)]["ast_hash"] != "" + + +# Regression tests for #2033 - kind="auto" compares ast_hash for code and +# semantic_hash for everything else, so the skill's --update runbook neither +# re-extracts a whole AST-built corpus (code has no semantic_hash by design) +# nor loses the semantic catch-up for docs downgraded by an AST-only rebuild. + +def test_detect_incremental_auto_code_with_ast_hash_only_is_unchanged(tmp_path): + """#2033: after an AST-only stamp (graphify update), an untouched code + file has ast_hash but no semantic_hash. kind="auto" must report it + unchanged — code never receives semantic extraction, so the missing + semantic_hash is not a pending obligation.""" + py = tmp_path / "main.py" + py.write_text("print('hello')") + manifest_path = str(tmp_path / "graphify-out" / "manifest.json") + save_manifest({"code": [str(py)]}, manifest_path, root=tmp_path, kind="ast") + + inc = detect_incremental(tmp_path, manifest_path, kind="auto") + assert inc["new_files"]["code"] == [] + assert [Path(f).name for f in inc["unchanged_files"]["code"]] == ["main.py"] + + +def test_detect_incremental_auto_doc_missing_semantic_hash_is_changed(tmp_path): + """#2033: an AST-only stamp clears/omits semantic_hash for a changed doc. + kind="auto" must still queue the doc for semantic re-extraction — that + catch-up is the whole point of semantic-hash bookkeeping (#2014).""" + doc = tmp_path / "notes.md" + doc.write_text("# Notes\n\nsome content") + manifest_path = str(tmp_path / "graphify-out" / "manifest.json") + save_manifest({"document": [str(doc)]}, manifest_path, root=tmp_path, kind="ast") + + inc = detect_incremental(tmp_path, manifest_path, kind="auto") + assert [Path(f).name for f in inc["new_files"]["document"]] == ["notes.md"] + + +def test_detect_incremental_auto_changed_code_is_changed(tmp_path): + """kind="auto" still reports a code file whose content changed since the + last ast stamp.""" + py = tmp_path / "main.py" + py.write_text("print('hello')") + manifest_path = str(tmp_path / "graphify-out" / "manifest.json") + save_manifest({"code": [str(py)]}, manifest_path, root=tmp_path, kind="ast") + + py.write_text("print('changed')") + os.utime(py, (os.path.getmtime(py) + 5, os.path.getmtime(py) + 5)) + + inc = detect_incremental(tmp_path, manifest_path, kind="auto") + assert [Path(f).name for f in inc["new_files"]["code"]] == ["main.py"] + + +def test_detect_incremental_auto_doc_with_semantic_hash_is_unchanged(tmp_path): + """kind="auto" trusts a valid semantic_hash: an untouched doc stamped by + a real semantic pass is not re-queued.""" + doc = tmp_path / "notes.md" + doc.write_text("# Notes\n\nsome content") + manifest_path = str(tmp_path / "graphify-out" / "manifest.json") + save_manifest({"document": [str(doc)]}, manifest_path, root=tmp_path, kind="both") + + inc = detect_incremental(tmp_path, manifest_path, kind="auto") + assert inc["new_files"]["document"] == [] + assert [Path(f).name for f in inc["unchanged_files"]["document"]] == ["notes.md"] + + # Regression tests for #945 - .gitignore fallback when no .graphifyignore exists def test_gitignore_fallback_when_no_graphifyignore(tmp_path): diff --git a/tools/skillgen/expected/graphify__skill-aider.md b/tools/skillgen/expected/graphify__skill-aider.md index 8db8f73f2..4be4eccd4 100644 --- a/tools/skillgen/expected/graphify__skill-aider.md +++ b/tools/skillgen/expected/graphify__skill-aider.md @@ -748,7 +748,10 @@ import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path -result = detect_incremental(Path('INPUT_PATH')) +# kind='auto': ast_hash for code, semantic_hash for docs/papers/images. The +# semantic default would report every code file of an AST-built corpus as +# changed and re-extract the whole corpus (#2033). +result = detect_incremental(Path('INPUT_PATH'), kind='auto') new_total = result.get('new_total', 0) print(json.dumps(result, indent=2)) Path('.graphify_incremental.json').write_text(json.dumps(result)) diff --git a/tools/skillgen/expected/graphify__skill-devin.md b/tools/skillgen/expected/graphify__skill-devin.md index c444a852d..2c9d57f8d 100644 --- a/tools/skillgen/expected/graphify__skill-devin.md +++ b/tools/skillgen/expected/graphify__skill-devin.md @@ -887,7 +887,10 @@ import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path -result = detect_incremental(Path('INPUT_PATH')) +# kind='auto': ast_hash for code, semantic_hash for docs/papers/images. The +# semantic default would report every code file of an AST-built corpus as +# changed and re-extract the whole corpus (#2033). +result = detect_incremental(Path('INPUT_PATH'), kind='auto') new_total = result.get('new_total', 0) print(json.dumps(result, indent=2)) Path('graphify-out/.graphify_incremental.json').write_text(json.dumps(result)) diff --git a/tools/skillgen/expected/graphify__skills__agents__references__update.md b/tools/skillgen/expected/graphify__skills__agents__references__update.md index fa2612180..f7db138a6 100644 --- a/tools/skillgen/expected/graphify__skills__agents__references__update.md +++ b/tools/skillgen/expected/graphify__skills__agents__references__update.md @@ -12,7 +12,13 @@ import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path -result = detect_incremental(Path('INPUT_PATH')) +# kind='auto' compares code files against ast_hash and docs/papers/images +# against semantic_hash. The default (kind='semantic') reports every code file +# of an AST-built corpus as changed — code never gets a semantic_hash — turning +# a 12-file delta into a full-corpus re-extraction (#2033); plain kind='ast' +# would instead skip the semantic catch-up for docs an AST-only rebuild +# downgraded (#2014). Per-type is the only mode that matches this pipeline. +result = detect_incremental(Path('INPUT_PATH'), kind='auto') new_total = result.get('new_total', 0) print(json.dumps(result, indent=2, ensure_ascii=False)) Path('graphify-out/.graphify_incremental.json').write_text(json.dumps(result, ensure_ascii=False), encoding=\"utf-8\") @@ -142,7 +148,18 @@ print(f'[graphify update] Merged extraction written ({len(merged_out[\"nodes\"]) # root= matches the build_merge call above so the manifest keys stay relative to # the scan root — portable across clones/machines, so --update keeps matching # cached files instead of missing every one after a move (#1417). -save_manifest(incremental['files'], root='INPUT_PATH') +# Two stamps, not one kind='both' pass (#2033): 'both' would forge a +# semantic_hash for every file in the corpus, including docs this run never +# semantically extracted, silently cancelling their future catch-up. +# 1) kind='ast' over the full corpus: refreshes ast_hash; an untouched file +# keeps its semantic_hash, a changed one has it cleared. +# 2) kind='semantic' over just the files the semantic pass covered this run — +# the changed docs/papers/images (and transcribed videos). On a code-only +# update that set is empty and the second stamp is skipped. +save_manifest(incremental['files'], root='INPUT_PATH', kind='ast') +sem_files = {t: incremental.get('new_files', {}).get(t, []) for t in ('document', 'paper', 'image', 'video')} +if any(sem_files.values()): + save_manifest(sem_files, root='INPUT_PATH', kind='semantic') print('[graphify update] Manifest saved.') " ``` diff --git a/tools/skillgen/expected/graphify__skills__amp__references__update.md b/tools/skillgen/expected/graphify__skills__amp__references__update.md index fa2612180..f7db138a6 100644 --- a/tools/skillgen/expected/graphify__skills__amp__references__update.md +++ b/tools/skillgen/expected/graphify__skills__amp__references__update.md @@ -12,7 +12,13 @@ import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path -result = detect_incremental(Path('INPUT_PATH')) +# kind='auto' compares code files against ast_hash and docs/papers/images +# against semantic_hash. The default (kind='semantic') reports every code file +# of an AST-built corpus as changed — code never gets a semantic_hash — turning +# a 12-file delta into a full-corpus re-extraction (#2033); plain kind='ast' +# would instead skip the semantic catch-up for docs an AST-only rebuild +# downgraded (#2014). Per-type is the only mode that matches this pipeline. +result = detect_incremental(Path('INPUT_PATH'), kind='auto') new_total = result.get('new_total', 0) print(json.dumps(result, indent=2, ensure_ascii=False)) Path('graphify-out/.graphify_incremental.json').write_text(json.dumps(result, ensure_ascii=False), encoding=\"utf-8\") @@ -142,7 +148,18 @@ print(f'[graphify update] Merged extraction written ({len(merged_out[\"nodes\"]) # root= matches the build_merge call above so the manifest keys stay relative to # the scan root — portable across clones/machines, so --update keeps matching # cached files instead of missing every one after a move (#1417). -save_manifest(incremental['files'], root='INPUT_PATH') +# Two stamps, not one kind='both' pass (#2033): 'both' would forge a +# semantic_hash for every file in the corpus, including docs this run never +# semantically extracted, silently cancelling their future catch-up. +# 1) kind='ast' over the full corpus: refreshes ast_hash; an untouched file +# keeps its semantic_hash, a changed one has it cleared. +# 2) kind='semantic' over just the files the semantic pass covered this run — +# the changed docs/papers/images (and transcribed videos). On a code-only +# update that set is empty and the second stamp is skipped. +save_manifest(incremental['files'], root='INPUT_PATH', kind='ast') +sem_files = {t: incremental.get('new_files', {}).get(t, []) for t in ('document', 'paper', 'image', 'video')} +if any(sem_files.values()): + save_manifest(sem_files, root='INPUT_PATH', kind='semantic') print('[graphify update] Manifest saved.') " ``` diff --git a/tools/skillgen/expected/graphify__skills__claude__references__update.md b/tools/skillgen/expected/graphify__skills__claude__references__update.md index fa2612180..f7db138a6 100644 --- a/tools/skillgen/expected/graphify__skills__claude__references__update.md +++ b/tools/skillgen/expected/graphify__skills__claude__references__update.md @@ -12,7 +12,13 @@ import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path -result = detect_incremental(Path('INPUT_PATH')) +# kind='auto' compares code files against ast_hash and docs/papers/images +# against semantic_hash. The default (kind='semantic') reports every code file +# of an AST-built corpus as changed — code never gets a semantic_hash — turning +# a 12-file delta into a full-corpus re-extraction (#2033); plain kind='ast' +# would instead skip the semantic catch-up for docs an AST-only rebuild +# downgraded (#2014). Per-type is the only mode that matches this pipeline. +result = detect_incremental(Path('INPUT_PATH'), kind='auto') new_total = result.get('new_total', 0) print(json.dumps(result, indent=2, ensure_ascii=False)) Path('graphify-out/.graphify_incremental.json').write_text(json.dumps(result, ensure_ascii=False), encoding=\"utf-8\") @@ -142,7 +148,18 @@ print(f'[graphify update] Merged extraction written ({len(merged_out[\"nodes\"]) # root= matches the build_merge call above so the manifest keys stay relative to # the scan root — portable across clones/machines, so --update keeps matching # cached files instead of missing every one after a move (#1417). -save_manifest(incremental['files'], root='INPUT_PATH') +# Two stamps, not one kind='both' pass (#2033): 'both' would forge a +# semantic_hash for every file in the corpus, including docs this run never +# semantically extracted, silently cancelling their future catch-up. +# 1) kind='ast' over the full corpus: refreshes ast_hash; an untouched file +# keeps its semantic_hash, a changed one has it cleared. +# 2) kind='semantic' over just the files the semantic pass covered this run — +# the changed docs/papers/images (and transcribed videos). On a code-only +# update that set is empty and the second stamp is skipped. +save_manifest(incremental['files'], root='INPUT_PATH', kind='ast') +sem_files = {t: incremental.get('new_files', {}).get(t, []) for t in ('document', 'paper', 'image', 'video')} +if any(sem_files.values()): + save_manifest(sem_files, root='INPUT_PATH', kind='semantic') print('[graphify update] Manifest saved.') " ``` diff --git a/tools/skillgen/expected/graphify__skills__claw__references__update.md b/tools/skillgen/expected/graphify__skills__claw__references__update.md index fa2612180..f7db138a6 100644 --- a/tools/skillgen/expected/graphify__skills__claw__references__update.md +++ b/tools/skillgen/expected/graphify__skills__claw__references__update.md @@ -12,7 +12,13 @@ import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path -result = detect_incremental(Path('INPUT_PATH')) +# kind='auto' compares code files against ast_hash and docs/papers/images +# against semantic_hash. The default (kind='semantic') reports every code file +# of an AST-built corpus as changed — code never gets a semantic_hash — turning +# a 12-file delta into a full-corpus re-extraction (#2033); plain kind='ast' +# would instead skip the semantic catch-up for docs an AST-only rebuild +# downgraded (#2014). Per-type is the only mode that matches this pipeline. +result = detect_incremental(Path('INPUT_PATH'), kind='auto') new_total = result.get('new_total', 0) print(json.dumps(result, indent=2, ensure_ascii=False)) Path('graphify-out/.graphify_incremental.json').write_text(json.dumps(result, ensure_ascii=False), encoding=\"utf-8\") @@ -142,7 +148,18 @@ print(f'[graphify update] Merged extraction written ({len(merged_out[\"nodes\"]) # root= matches the build_merge call above so the manifest keys stay relative to # the scan root — portable across clones/machines, so --update keeps matching # cached files instead of missing every one after a move (#1417). -save_manifest(incremental['files'], root='INPUT_PATH') +# Two stamps, not one kind='both' pass (#2033): 'both' would forge a +# semantic_hash for every file in the corpus, including docs this run never +# semantically extracted, silently cancelling their future catch-up. +# 1) kind='ast' over the full corpus: refreshes ast_hash; an untouched file +# keeps its semantic_hash, a changed one has it cleared. +# 2) kind='semantic' over just the files the semantic pass covered this run — +# the changed docs/papers/images (and transcribed videos). On a code-only +# update that set is empty and the second stamp is skipped. +save_manifest(incremental['files'], root='INPUT_PATH', kind='ast') +sem_files = {t: incremental.get('new_files', {}).get(t, []) for t in ('document', 'paper', 'image', 'video')} +if any(sem_files.values()): + save_manifest(sem_files, root='INPUT_PATH', kind='semantic') print('[graphify update] Manifest saved.') " ``` diff --git a/tools/skillgen/expected/graphify__skills__codex__references__update.md b/tools/skillgen/expected/graphify__skills__codex__references__update.md index fa2612180..f7db138a6 100644 --- a/tools/skillgen/expected/graphify__skills__codex__references__update.md +++ b/tools/skillgen/expected/graphify__skills__codex__references__update.md @@ -12,7 +12,13 @@ import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path -result = detect_incremental(Path('INPUT_PATH')) +# kind='auto' compares code files against ast_hash and docs/papers/images +# against semantic_hash. The default (kind='semantic') reports every code file +# of an AST-built corpus as changed — code never gets a semantic_hash — turning +# a 12-file delta into a full-corpus re-extraction (#2033); plain kind='ast' +# would instead skip the semantic catch-up for docs an AST-only rebuild +# downgraded (#2014). Per-type is the only mode that matches this pipeline. +result = detect_incremental(Path('INPUT_PATH'), kind='auto') new_total = result.get('new_total', 0) print(json.dumps(result, indent=2, ensure_ascii=False)) Path('graphify-out/.graphify_incremental.json').write_text(json.dumps(result, ensure_ascii=False), encoding=\"utf-8\") @@ -142,7 +148,18 @@ print(f'[graphify update] Merged extraction written ({len(merged_out[\"nodes\"]) # root= matches the build_merge call above so the manifest keys stay relative to # the scan root — portable across clones/machines, so --update keeps matching # cached files instead of missing every one after a move (#1417). -save_manifest(incremental['files'], root='INPUT_PATH') +# Two stamps, not one kind='both' pass (#2033): 'both' would forge a +# semantic_hash for every file in the corpus, including docs this run never +# semantically extracted, silently cancelling their future catch-up. +# 1) kind='ast' over the full corpus: refreshes ast_hash; an untouched file +# keeps its semantic_hash, a changed one has it cleared. +# 2) kind='semantic' over just the files the semantic pass covered this run — +# the changed docs/papers/images (and transcribed videos). On a code-only +# update that set is empty and the second stamp is skipped. +save_manifest(incremental['files'], root='INPUT_PATH', kind='ast') +sem_files = {t: incremental.get('new_files', {}).get(t, []) for t in ('document', 'paper', 'image', 'video')} +if any(sem_files.values()): + save_manifest(sem_files, root='INPUT_PATH', kind='semantic') print('[graphify update] Manifest saved.') " ``` diff --git a/tools/skillgen/expected/graphify__skills__copilot__references__update.md b/tools/skillgen/expected/graphify__skills__copilot__references__update.md index fa2612180..f7db138a6 100644 --- a/tools/skillgen/expected/graphify__skills__copilot__references__update.md +++ b/tools/skillgen/expected/graphify__skills__copilot__references__update.md @@ -12,7 +12,13 @@ import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path -result = detect_incremental(Path('INPUT_PATH')) +# kind='auto' compares code files against ast_hash and docs/papers/images +# against semantic_hash. The default (kind='semantic') reports every code file +# of an AST-built corpus as changed — code never gets a semantic_hash — turning +# a 12-file delta into a full-corpus re-extraction (#2033); plain kind='ast' +# would instead skip the semantic catch-up for docs an AST-only rebuild +# downgraded (#2014). Per-type is the only mode that matches this pipeline. +result = detect_incremental(Path('INPUT_PATH'), kind='auto') new_total = result.get('new_total', 0) print(json.dumps(result, indent=2, ensure_ascii=False)) Path('graphify-out/.graphify_incremental.json').write_text(json.dumps(result, ensure_ascii=False), encoding=\"utf-8\") @@ -142,7 +148,18 @@ print(f'[graphify update] Merged extraction written ({len(merged_out[\"nodes\"]) # root= matches the build_merge call above so the manifest keys stay relative to # the scan root — portable across clones/machines, so --update keeps matching # cached files instead of missing every one after a move (#1417). -save_manifest(incremental['files'], root='INPUT_PATH') +# Two stamps, not one kind='both' pass (#2033): 'both' would forge a +# semantic_hash for every file in the corpus, including docs this run never +# semantically extracted, silently cancelling their future catch-up. +# 1) kind='ast' over the full corpus: refreshes ast_hash; an untouched file +# keeps its semantic_hash, a changed one has it cleared. +# 2) kind='semantic' over just the files the semantic pass covered this run — +# the changed docs/papers/images (and transcribed videos). On a code-only +# update that set is empty and the second stamp is skipped. +save_manifest(incremental['files'], root='INPUT_PATH', kind='ast') +sem_files = {t: incremental.get('new_files', {}).get(t, []) for t in ('document', 'paper', 'image', 'video')} +if any(sem_files.values()): + save_manifest(sem_files, root='INPUT_PATH', kind='semantic') print('[graphify update] Manifest saved.') " ``` diff --git a/tools/skillgen/expected/graphify__skills__droid__references__update.md b/tools/skillgen/expected/graphify__skills__droid__references__update.md index fa2612180..f7db138a6 100644 --- a/tools/skillgen/expected/graphify__skills__droid__references__update.md +++ b/tools/skillgen/expected/graphify__skills__droid__references__update.md @@ -12,7 +12,13 @@ import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path -result = detect_incremental(Path('INPUT_PATH')) +# kind='auto' compares code files against ast_hash and docs/papers/images +# against semantic_hash. The default (kind='semantic') reports every code file +# of an AST-built corpus as changed — code never gets a semantic_hash — turning +# a 12-file delta into a full-corpus re-extraction (#2033); plain kind='ast' +# would instead skip the semantic catch-up for docs an AST-only rebuild +# downgraded (#2014). Per-type is the only mode that matches this pipeline. +result = detect_incremental(Path('INPUT_PATH'), kind='auto') new_total = result.get('new_total', 0) print(json.dumps(result, indent=2, ensure_ascii=False)) Path('graphify-out/.graphify_incremental.json').write_text(json.dumps(result, ensure_ascii=False), encoding=\"utf-8\") @@ -142,7 +148,18 @@ print(f'[graphify update] Merged extraction written ({len(merged_out[\"nodes\"]) # root= matches the build_merge call above so the manifest keys stay relative to # the scan root — portable across clones/machines, so --update keeps matching # cached files instead of missing every one after a move (#1417). -save_manifest(incremental['files'], root='INPUT_PATH') +# Two stamps, not one kind='both' pass (#2033): 'both' would forge a +# semantic_hash for every file in the corpus, including docs this run never +# semantically extracted, silently cancelling their future catch-up. +# 1) kind='ast' over the full corpus: refreshes ast_hash; an untouched file +# keeps its semantic_hash, a changed one has it cleared. +# 2) kind='semantic' over just the files the semantic pass covered this run — +# the changed docs/papers/images (and transcribed videos). On a code-only +# update that set is empty and the second stamp is skipped. +save_manifest(incremental['files'], root='INPUT_PATH', kind='ast') +sem_files = {t: incremental.get('new_files', {}).get(t, []) for t in ('document', 'paper', 'image', 'video')} +if any(sem_files.values()): + save_manifest(sem_files, root='INPUT_PATH', kind='semantic') print('[graphify update] Manifest saved.') " ``` diff --git a/tools/skillgen/expected/graphify__skills__kilo__references__update.md b/tools/skillgen/expected/graphify__skills__kilo__references__update.md index fa2612180..f7db138a6 100644 --- a/tools/skillgen/expected/graphify__skills__kilo__references__update.md +++ b/tools/skillgen/expected/graphify__skills__kilo__references__update.md @@ -12,7 +12,13 @@ import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path -result = detect_incremental(Path('INPUT_PATH')) +# kind='auto' compares code files against ast_hash and docs/papers/images +# against semantic_hash. The default (kind='semantic') reports every code file +# of an AST-built corpus as changed — code never gets a semantic_hash — turning +# a 12-file delta into a full-corpus re-extraction (#2033); plain kind='ast' +# would instead skip the semantic catch-up for docs an AST-only rebuild +# downgraded (#2014). Per-type is the only mode that matches this pipeline. +result = detect_incremental(Path('INPUT_PATH'), kind='auto') new_total = result.get('new_total', 0) print(json.dumps(result, indent=2, ensure_ascii=False)) Path('graphify-out/.graphify_incremental.json').write_text(json.dumps(result, ensure_ascii=False), encoding=\"utf-8\") @@ -142,7 +148,18 @@ print(f'[graphify update] Merged extraction written ({len(merged_out[\"nodes\"]) # root= matches the build_merge call above so the manifest keys stay relative to # the scan root — portable across clones/machines, so --update keeps matching # cached files instead of missing every one after a move (#1417). -save_manifest(incremental['files'], root='INPUT_PATH') +# Two stamps, not one kind='both' pass (#2033): 'both' would forge a +# semantic_hash for every file in the corpus, including docs this run never +# semantically extracted, silently cancelling their future catch-up. +# 1) kind='ast' over the full corpus: refreshes ast_hash; an untouched file +# keeps its semantic_hash, a changed one has it cleared. +# 2) kind='semantic' over just the files the semantic pass covered this run — +# the changed docs/papers/images (and transcribed videos). On a code-only +# update that set is empty and the second stamp is skipped. +save_manifest(incremental['files'], root='INPUT_PATH', kind='ast') +sem_files = {t: incremental.get('new_files', {}).get(t, []) for t in ('document', 'paper', 'image', 'video')} +if any(sem_files.values()): + save_manifest(sem_files, root='INPUT_PATH', kind='semantic') print('[graphify update] Manifest saved.') " ``` diff --git a/tools/skillgen/expected/graphify__skills__kiro__references__update.md b/tools/skillgen/expected/graphify__skills__kiro__references__update.md index fa2612180..f7db138a6 100644 --- a/tools/skillgen/expected/graphify__skills__kiro__references__update.md +++ b/tools/skillgen/expected/graphify__skills__kiro__references__update.md @@ -12,7 +12,13 @@ import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path -result = detect_incremental(Path('INPUT_PATH')) +# kind='auto' compares code files against ast_hash and docs/papers/images +# against semantic_hash. The default (kind='semantic') reports every code file +# of an AST-built corpus as changed — code never gets a semantic_hash — turning +# a 12-file delta into a full-corpus re-extraction (#2033); plain kind='ast' +# would instead skip the semantic catch-up for docs an AST-only rebuild +# downgraded (#2014). Per-type is the only mode that matches this pipeline. +result = detect_incremental(Path('INPUT_PATH'), kind='auto') new_total = result.get('new_total', 0) print(json.dumps(result, indent=2, ensure_ascii=False)) Path('graphify-out/.graphify_incremental.json').write_text(json.dumps(result, ensure_ascii=False), encoding=\"utf-8\") @@ -142,7 +148,18 @@ print(f'[graphify update] Merged extraction written ({len(merged_out[\"nodes\"]) # root= matches the build_merge call above so the manifest keys stay relative to # the scan root — portable across clones/machines, so --update keeps matching # cached files instead of missing every one after a move (#1417). -save_manifest(incremental['files'], root='INPUT_PATH') +# Two stamps, not one kind='both' pass (#2033): 'both' would forge a +# semantic_hash for every file in the corpus, including docs this run never +# semantically extracted, silently cancelling their future catch-up. +# 1) kind='ast' over the full corpus: refreshes ast_hash; an untouched file +# keeps its semantic_hash, a changed one has it cleared. +# 2) kind='semantic' over just the files the semantic pass covered this run — +# the changed docs/papers/images (and transcribed videos). On a code-only +# update that set is empty and the second stamp is skipped. +save_manifest(incremental['files'], root='INPUT_PATH', kind='ast') +sem_files = {t: incremental.get('new_files', {}).get(t, []) for t in ('document', 'paper', 'image', 'video')} +if any(sem_files.values()): + save_manifest(sem_files, root='INPUT_PATH', kind='semantic') print('[graphify update] Manifest saved.') " ``` diff --git a/tools/skillgen/expected/graphify__skills__opencode__references__update.md b/tools/skillgen/expected/graphify__skills__opencode__references__update.md index fa2612180..f7db138a6 100644 --- a/tools/skillgen/expected/graphify__skills__opencode__references__update.md +++ b/tools/skillgen/expected/graphify__skills__opencode__references__update.md @@ -12,7 +12,13 @@ import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path -result = detect_incremental(Path('INPUT_PATH')) +# kind='auto' compares code files against ast_hash and docs/papers/images +# against semantic_hash. The default (kind='semantic') reports every code file +# of an AST-built corpus as changed — code never gets a semantic_hash — turning +# a 12-file delta into a full-corpus re-extraction (#2033); plain kind='ast' +# would instead skip the semantic catch-up for docs an AST-only rebuild +# downgraded (#2014). Per-type is the only mode that matches this pipeline. +result = detect_incremental(Path('INPUT_PATH'), kind='auto') new_total = result.get('new_total', 0) print(json.dumps(result, indent=2, ensure_ascii=False)) Path('graphify-out/.graphify_incremental.json').write_text(json.dumps(result, ensure_ascii=False), encoding=\"utf-8\") @@ -142,7 +148,18 @@ print(f'[graphify update] Merged extraction written ({len(merged_out[\"nodes\"]) # root= matches the build_merge call above so the manifest keys stay relative to # the scan root — portable across clones/machines, so --update keeps matching # cached files instead of missing every one after a move (#1417). -save_manifest(incremental['files'], root='INPUT_PATH') +# Two stamps, not one kind='both' pass (#2033): 'both' would forge a +# semantic_hash for every file in the corpus, including docs this run never +# semantically extracted, silently cancelling their future catch-up. +# 1) kind='ast' over the full corpus: refreshes ast_hash; an untouched file +# keeps its semantic_hash, a changed one has it cleared. +# 2) kind='semantic' over just the files the semantic pass covered this run — +# the changed docs/papers/images (and transcribed videos). On a code-only +# update that set is empty and the second stamp is skipped. +save_manifest(incremental['files'], root='INPUT_PATH', kind='ast') +sem_files = {t: incremental.get('new_files', {}).get(t, []) for t in ('document', 'paper', 'image', 'video')} +if any(sem_files.values()): + save_manifest(sem_files, root='INPUT_PATH', kind='semantic') print('[graphify update] Manifest saved.') " ``` diff --git a/tools/skillgen/expected/graphify__skills__pi__references__update.md b/tools/skillgen/expected/graphify__skills__pi__references__update.md index fa2612180..f7db138a6 100644 --- a/tools/skillgen/expected/graphify__skills__pi__references__update.md +++ b/tools/skillgen/expected/graphify__skills__pi__references__update.md @@ -12,7 +12,13 @@ import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path -result = detect_incremental(Path('INPUT_PATH')) +# kind='auto' compares code files against ast_hash and docs/papers/images +# against semantic_hash. The default (kind='semantic') reports every code file +# of an AST-built corpus as changed — code never gets a semantic_hash — turning +# a 12-file delta into a full-corpus re-extraction (#2033); plain kind='ast' +# would instead skip the semantic catch-up for docs an AST-only rebuild +# downgraded (#2014). Per-type is the only mode that matches this pipeline. +result = detect_incremental(Path('INPUT_PATH'), kind='auto') new_total = result.get('new_total', 0) print(json.dumps(result, indent=2, ensure_ascii=False)) Path('graphify-out/.graphify_incremental.json').write_text(json.dumps(result, ensure_ascii=False), encoding=\"utf-8\") @@ -142,7 +148,18 @@ print(f'[graphify update] Merged extraction written ({len(merged_out[\"nodes\"]) # root= matches the build_merge call above so the manifest keys stay relative to # the scan root — portable across clones/machines, so --update keeps matching # cached files instead of missing every one after a move (#1417). -save_manifest(incremental['files'], root='INPUT_PATH') +# Two stamps, not one kind='both' pass (#2033): 'both' would forge a +# semantic_hash for every file in the corpus, including docs this run never +# semantically extracted, silently cancelling their future catch-up. +# 1) kind='ast' over the full corpus: refreshes ast_hash; an untouched file +# keeps its semantic_hash, a changed one has it cleared. +# 2) kind='semantic' over just the files the semantic pass covered this run — +# the changed docs/papers/images (and transcribed videos). On a code-only +# update that set is empty and the second stamp is skipped. +save_manifest(incremental['files'], root='INPUT_PATH', kind='ast') +sem_files = {t: incremental.get('new_files', {}).get(t, []) for t in ('document', 'paper', 'image', 'video')} +if any(sem_files.values()): + save_manifest(sem_files, root='INPUT_PATH', kind='semantic') print('[graphify update] Manifest saved.') " ``` diff --git a/tools/skillgen/expected/graphify__skills__trae__references__update.md b/tools/skillgen/expected/graphify__skills__trae__references__update.md index fa2612180..f7db138a6 100644 --- a/tools/skillgen/expected/graphify__skills__trae__references__update.md +++ b/tools/skillgen/expected/graphify__skills__trae__references__update.md @@ -12,7 +12,13 @@ import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path -result = detect_incremental(Path('INPUT_PATH')) +# kind='auto' compares code files against ast_hash and docs/papers/images +# against semantic_hash. The default (kind='semantic') reports every code file +# of an AST-built corpus as changed — code never gets a semantic_hash — turning +# a 12-file delta into a full-corpus re-extraction (#2033); plain kind='ast' +# would instead skip the semantic catch-up for docs an AST-only rebuild +# downgraded (#2014). Per-type is the only mode that matches this pipeline. +result = detect_incremental(Path('INPUT_PATH'), kind='auto') new_total = result.get('new_total', 0) print(json.dumps(result, indent=2, ensure_ascii=False)) Path('graphify-out/.graphify_incremental.json').write_text(json.dumps(result, ensure_ascii=False), encoding=\"utf-8\") @@ -142,7 +148,18 @@ print(f'[graphify update] Merged extraction written ({len(merged_out[\"nodes\"]) # root= matches the build_merge call above so the manifest keys stay relative to # the scan root — portable across clones/machines, so --update keeps matching # cached files instead of missing every one after a move (#1417). -save_manifest(incremental['files'], root='INPUT_PATH') +# Two stamps, not one kind='both' pass (#2033): 'both' would forge a +# semantic_hash for every file in the corpus, including docs this run never +# semantically extracted, silently cancelling their future catch-up. +# 1) kind='ast' over the full corpus: refreshes ast_hash; an untouched file +# keeps its semantic_hash, a changed one has it cleared. +# 2) kind='semantic' over just the files the semantic pass covered this run — +# the changed docs/papers/images (and transcribed videos). On a code-only +# update that set is empty and the second stamp is skipped. +save_manifest(incremental['files'], root='INPUT_PATH', kind='ast') +sem_files = {t: incremental.get('new_files', {}).get(t, []) for t in ('document', 'paper', 'image', 'video')} +if any(sem_files.values()): + save_manifest(sem_files, root='INPUT_PATH', kind='semantic') print('[graphify update] Manifest saved.') " ``` diff --git a/tools/skillgen/expected/graphify__skills__vscode__references__update.md b/tools/skillgen/expected/graphify__skills__vscode__references__update.md index fa2612180..f7db138a6 100644 --- a/tools/skillgen/expected/graphify__skills__vscode__references__update.md +++ b/tools/skillgen/expected/graphify__skills__vscode__references__update.md @@ -12,7 +12,13 @@ import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path -result = detect_incremental(Path('INPUT_PATH')) +# kind='auto' compares code files against ast_hash and docs/papers/images +# against semantic_hash. The default (kind='semantic') reports every code file +# of an AST-built corpus as changed — code never gets a semantic_hash — turning +# a 12-file delta into a full-corpus re-extraction (#2033); plain kind='ast' +# would instead skip the semantic catch-up for docs an AST-only rebuild +# downgraded (#2014). Per-type is the only mode that matches this pipeline. +result = detect_incremental(Path('INPUT_PATH'), kind='auto') new_total = result.get('new_total', 0) print(json.dumps(result, indent=2, ensure_ascii=False)) Path('graphify-out/.graphify_incremental.json').write_text(json.dumps(result, ensure_ascii=False), encoding=\"utf-8\") @@ -142,7 +148,18 @@ print(f'[graphify update] Merged extraction written ({len(merged_out[\"nodes\"]) # root= matches the build_merge call above so the manifest keys stay relative to # the scan root — portable across clones/machines, so --update keeps matching # cached files instead of missing every one after a move (#1417). -save_manifest(incremental['files'], root='INPUT_PATH') +# Two stamps, not one kind='both' pass (#2033): 'both' would forge a +# semantic_hash for every file in the corpus, including docs this run never +# semantically extracted, silently cancelling their future catch-up. +# 1) kind='ast' over the full corpus: refreshes ast_hash; an untouched file +# keeps its semantic_hash, a changed one has it cleared. +# 2) kind='semantic' over just the files the semantic pass covered this run — +# the changed docs/papers/images (and transcribed videos). On a code-only +# update that set is empty and the second stamp is skipped. +save_manifest(incremental['files'], root='INPUT_PATH', kind='ast') +sem_files = {t: incremental.get('new_files', {}).get(t, []) for t in ('document', 'paper', 'image', 'video')} +if any(sem_files.values()): + save_manifest(sem_files, root='INPUT_PATH', kind='semantic') print('[graphify update] Manifest saved.') " ``` diff --git a/tools/skillgen/expected/graphify__skills__windows__references__update.md b/tools/skillgen/expected/graphify__skills__windows__references__update.md index fa2612180..f7db138a6 100644 --- a/tools/skillgen/expected/graphify__skills__windows__references__update.md +++ b/tools/skillgen/expected/graphify__skills__windows__references__update.md @@ -12,7 +12,13 @@ import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path -result = detect_incremental(Path('INPUT_PATH')) +# kind='auto' compares code files against ast_hash and docs/papers/images +# against semantic_hash. The default (kind='semantic') reports every code file +# of an AST-built corpus as changed — code never gets a semantic_hash — turning +# a 12-file delta into a full-corpus re-extraction (#2033); plain kind='ast' +# would instead skip the semantic catch-up for docs an AST-only rebuild +# downgraded (#2014). Per-type is the only mode that matches this pipeline. +result = detect_incremental(Path('INPUT_PATH'), kind='auto') new_total = result.get('new_total', 0) print(json.dumps(result, indent=2, ensure_ascii=False)) Path('graphify-out/.graphify_incremental.json').write_text(json.dumps(result, ensure_ascii=False), encoding=\"utf-8\") @@ -142,7 +148,18 @@ print(f'[graphify update] Merged extraction written ({len(merged_out[\"nodes\"]) # root= matches the build_merge call above so the manifest keys stay relative to # the scan root — portable across clones/machines, so --update keeps matching # cached files instead of missing every one after a move (#1417). -save_manifest(incremental['files'], root='INPUT_PATH') +# Two stamps, not one kind='both' pass (#2033): 'both' would forge a +# semantic_hash for every file in the corpus, including docs this run never +# semantically extracted, silently cancelling their future catch-up. +# 1) kind='ast' over the full corpus: refreshes ast_hash; an untouched file +# keeps its semantic_hash, a changed one has it cleared. +# 2) kind='semantic' over just the files the semantic pass covered this run — +# the changed docs/papers/images (and transcribed videos). On a code-only +# update that set is empty and the second stamp is skipped. +save_manifest(incremental['files'], root='INPUT_PATH', kind='ast') +sem_files = {t: incremental.get('new_files', {}).get(t, []) for t in ('document', 'paper', 'image', 'video')} +if any(sem_files.values()): + save_manifest(sem_files, root='INPUT_PATH', kind='semantic') print('[graphify update] Manifest saved.') " ``` diff --git a/tools/skillgen/fragments/core/aider.md b/tools/skillgen/fragments/core/aider.md index 8db8f73f2..4be4eccd4 100644 --- a/tools/skillgen/fragments/core/aider.md +++ b/tools/skillgen/fragments/core/aider.md @@ -748,7 +748,10 @@ import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path -result = detect_incremental(Path('INPUT_PATH')) +# kind='auto': ast_hash for code, semantic_hash for docs/papers/images. The +# semantic default would report every code file of an AST-built corpus as +# changed and re-extract the whole corpus (#2033). +result = detect_incremental(Path('INPUT_PATH'), kind='auto') new_total = result.get('new_total', 0) print(json.dumps(result, indent=2)) Path('.graphify_incremental.json').write_text(json.dumps(result)) diff --git a/tools/skillgen/fragments/core/devin.md b/tools/skillgen/fragments/core/devin.md index c444a852d..2c9d57f8d 100644 --- a/tools/skillgen/fragments/core/devin.md +++ b/tools/skillgen/fragments/core/devin.md @@ -887,7 +887,10 @@ import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path -result = detect_incremental(Path('INPUT_PATH')) +# kind='auto': ast_hash for code, semantic_hash for docs/papers/images. The +# semantic default would report every code file of an AST-built corpus as +# changed and re-extract the whole corpus (#2033). +result = detect_incremental(Path('INPUT_PATH'), kind='auto') new_total = result.get('new_total', 0) print(json.dumps(result, indent=2)) Path('graphify-out/.graphify_incremental.json').write_text(json.dumps(result)) diff --git a/tools/skillgen/fragments/references/shared/update.md b/tools/skillgen/fragments/references/shared/update.md index fa2612180..f7db138a6 100644 --- a/tools/skillgen/fragments/references/shared/update.md +++ b/tools/skillgen/fragments/references/shared/update.md @@ -12,7 +12,13 @@ import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path -result = detect_incremental(Path('INPUT_PATH')) +# kind='auto' compares code files against ast_hash and docs/papers/images +# against semantic_hash. The default (kind='semantic') reports every code file +# of an AST-built corpus as changed — code never gets a semantic_hash — turning +# a 12-file delta into a full-corpus re-extraction (#2033); plain kind='ast' +# would instead skip the semantic catch-up for docs an AST-only rebuild +# downgraded (#2014). Per-type is the only mode that matches this pipeline. +result = detect_incremental(Path('INPUT_PATH'), kind='auto') new_total = result.get('new_total', 0) print(json.dumps(result, indent=2, ensure_ascii=False)) Path('graphify-out/.graphify_incremental.json').write_text(json.dumps(result, ensure_ascii=False), encoding=\"utf-8\") @@ -142,7 +148,18 @@ print(f'[graphify update] Merged extraction written ({len(merged_out[\"nodes\"]) # root= matches the build_merge call above so the manifest keys stay relative to # the scan root — portable across clones/machines, so --update keeps matching # cached files instead of missing every one after a move (#1417). -save_manifest(incremental['files'], root='INPUT_PATH') +# Two stamps, not one kind='both' pass (#2033): 'both' would forge a +# semantic_hash for every file in the corpus, including docs this run never +# semantically extracted, silently cancelling their future catch-up. +# 1) kind='ast' over the full corpus: refreshes ast_hash; an untouched file +# keeps its semantic_hash, a changed one has it cleared. +# 2) kind='semantic' over just the files the semantic pass covered this run — +# the changed docs/papers/images (and transcribed videos). On a code-only +# update that set is empty and the second stamp is skipped. +save_manifest(incremental['files'], root='INPUT_PATH', kind='ast') +sem_files = {t: incremental.get('new_files', {}).get(t, []) for t in ('document', 'paper', 'image', 'video')} +if any(sem_files.values()): + save_manifest(sem_files, root='INPUT_PATH', kind='semantic') print('[graphify update] Manifest saved.') " ``` diff --git a/tools/skillgen/gen.py b/tools/skillgen/gen.py index 93e403f27..86675b655 100644 --- a/tools/skillgen/gen.py +++ b/tools/skillgen/gen.py @@ -918,6 +918,29 @@ def _is_semantic_cache_scope_fix_line(line: str) -> bool: ) or stripped.startswith("saved = save_semantic_cache(") +def _is_incremental_kind_fix_line(line: str) -> bool: + """Whether a line is part of the incremental-detection kind fix (#2033). + + The monolith --update flow called ``detect_incremental(...)`` with the + default ``kind="semantic"``, so on an AST-built corpus — where code files + never receive a semantic_hash — every file reported as changed and a + 12-file delta became a full-corpus re-extraction. The call now passes + ``kind='auto'`` (ast_hash for code, semantic_hash for docs/papers/images). + Both the old bare call (removed) and the new call (added) match on the + ``detect_incremental(`` clause with an ``import`` guard for the ``from + graphify.detect import ...`` line; the three explanatory comment lines + match verbatim so any rewording still trips the round-trip. + """ + stripped = line.strip() + if "detect_incremental(" in stripped and "import" not in stripped: + return True + return stripped in ( + "# kind='auto': ast_hash for code, semantic_hash for docs/papers/images. The", + "# semantic default would report every code file of an AST-built corpus as", + "# changed and re-extract the whole corpus (#2033).", + ) + + # Every line that may differ between a rendered monolith and its pristine v8 # baseline. Each predicate documents one sanctioned change-class; a blank line is # allowed because the multi-line fix blocks insert spacing. Anything else failing @@ -936,6 +959,7 @@ def _is_semantic_cache_scope_fix_line(line: str) -> bool: _is_obsidian_usage_comment_line, _is_uv_from_interpreter_fix_line, _is_semantic_cache_scope_fix_line, + _is_incremental_kind_fix_line, )