Commit Graph

819 Commits

Author SHA1 Message Date
safishamsi 22a58ffc20 feat: parallel community labeling via --max-concurrency / --batch-size (#1390)
label_communities ran batches one LLM call at a time, so a large graph needed
hundreds of sequential calls even on backends that allow heavy concurrency. It
now fans batches out across a thread pool, mirroring extract_corpus_parallel:
results are returned per batch and merged on the main thread (labels dict is
never mutated concurrently, no lock), and workers==1 keeps the original
sequential path verbatim. ollama and claude-cli are forced serial unless the
matching GRAPHIFY_*_PARALLEL env opt-in is set (same guard as extract).

generate_community_labels threads max_concurrency + batch_size through, and the
cluster-only/label CLI parses --max-concurrency and --batch-size (both `--flag N`
and `--flag=N` forms; the space form is parsed explicitly so the value is not
mistaken for the positional scan path by the arg-walk's catch-all).

Output is deterministic regardless of concurrency (keyed by community id). Tests:
parallel == sequential result, batch-size controls batch count, batches actually
run concurrently, ollama forced serial, and the CLI parses both new flags. Full
suite 2393 passed; ruff clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 13:53:33 +01:00
safishamsi 1a14e94e53 fix(reflect): dedupe dead-ends and corrections by question
Saving the same Q&A more than once duplicated lines in the "known dead ends" and
"corrections" sections: both lists were appended per memory doc with no key, while
node scoring already dedups by node. They now collapse by question, keeping the
most recent entry (docs are processed oldest-first, so a re-corrected question
shows its latest correction). Output stays deterministic, ordered by (date,
question). Applied to both the flat lists and the per-community buckets.

Found by a user running it on a 104-file Go codebase. Added a regression test
covering the dedupe and the recency-wins correction. Full suite 2389 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 12:48:57 +01:00
safishamsi d193b6277d feat(reflect): --if-stale to skip redundant runs; agent uses it at session start
Both the agent (session start) and the post-commit hook can run reflect; the runs
are deterministic and idempotent, but back-to-back ones are wasted work. Add
`graphify reflect --if-stale`, which no-ops when LESSONS.md is already at least as
new as every input (the memory docs and the graph). The skill's session-start
guidance now uses `--if-stale`, so when the hook just refreshed the file the
agent's run costs almost nothing, while a skill-only install still refreshes
on demand.

New lessons_fresh() helper + 5 tests (mtime freshness in each direction, and the
CLI skip/run behavior). Regenerated per-host references + re-blessed expected/;
all five skillgen guards pass; full suite 2388 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 12:26:39 +01:00
safishamsi c06db05351 fix(skill): agent self-refreshes LESSONS.md so work-memory works without the hook
A skill-only install (no `graphify hook install`) recorded outcomes via
save-result but never ran reflect, so LESSONS.md was never generated or
refreshed and the lessons never surfaced. The query reference now instructs the
agent to run `graphify reflect` itself at the start of graph work (cheap,
deterministic, no-op with no saved outcomes) before reading LESSONS.md. The
post-commit hook stays as a between-session freshness optimization, not a
requirement. Regenerated per-host references/query.md + re-blessed expected/;
all five skillgen guards pass; full suite 2383 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 12:17:16 +01:00
safishamsi f87011b38e Release 0.8.47
Work-memory (#1441): save-result --outcome + graphify reflect, with recency-decayed
scoring, corroboration threshold, label-matched node-existence gate, contested
handling, and zero-config adoption (skill reads LESSONS.md + records outcomes; git
hooks auto-reflect). Fixes: Python ClassName.method() qualified-call edges (#1446);
validate_extraction/build crash on non-hashable id (#1447); graphify update now
prunes a symbol removed from a surviving file without --force.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v0.8.47
2026-06-24 11:53:53 +01:00
safishamsi 6d9617a44f feat: zero-config work-memory adoption via skill + git hooks (#1441)
Make the self-improving loop "just work" once graphify is installed:

- Skill: the query reference now tells the agent to read
  graphify-out/reflections/LESSONS.md at the start of graph work (start from
  preferred sources, skip known dead ends, see prior corrections) and to record
  --outcome useful|dead_end|corrected (+ --correction) on save-result.
- Hooks: the post-commit and post-checkout rebuild bodies now auto-run reflect
  after _rebuild_code — best-effort, only when graphify-out/memory/ holds saved
  outcomes, and never fails the hook — so LESSONS.md refreshes on every rebuild
  without a manual `graphify reflect`.

Regenerated the per-host references/query.md and re-blessed expected/; all five
skillgen guards pass (check, audit-coverage, schema-singleton, monolith-roundtrip,
always-on-roundtrip). Verified end-to-end: install hook, save-result --outcome,
commit a code change -> hook rebuilds and writes LESSONS.md with the outcome.
Full suite 2383 passed; ruff clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 11:50:07 +01:00
safishamsi 89dd00f140 feat: self-improving work-memory — save-result outcomes + graphify reflect (#1441)
Adds the deterministic work-memory loop: `save-result --outcome
useful|dead_end|corrected [--correction]` records how a saved Q&A turned out, and
`graphify reflect` aggregates graphify-out/memory/ into a deterministic
reflections/LESSONS.md an agent loads next session.

Source nodes are scored, not counted: signed, recency-decayed (useful +,
dead_end/corrected -, configurable --half-life-days, default 30), so a fresh dead
end outweighs a stale useful. A node is "preferred" only once corroborated by
>=--min-corroboration distinct results (default 2); others are "tentative", and
mixed-signal nodes render once as "contested" (recency-wins). Source nodes are
matched to the graph by label OR id, and citations whose node no longer exists are
dropped, so a plain `graphify update` after deleting code clears stale lessons.
Deterministic, no LLM; bare save-result and existing behavior unchanged.

Rigorously verified end-to-end on real data: corroboration boundary, recency flip,
contested verdict, foreign/malformed memory docs, cold start, 300-doc scale +
byte-stable output, and the node-gate dropping deleted-code lessons after update.
Full suite 2383 passed; skillgen --check clean; ruff clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 11:43:13 +01:00
safishamsi 533859dc51 fix(update): file-aware shrink-guard so removed symbols prune without --force
`graphify update` after deleting a function left the stale node in graph.json.
The build correctly dropped it (#1116), but _check_shrink then refused to write
the smaller graph ("Refusing to overwrite — you may be missing chunk files"),
so the deletion never persisted without --force. That also starved the
work-memory node-existence gate, which relies on graph.json reflecting deletions.

The shrink-guard now takes the set of source files re-extracted this run
(rebuilt_sources). A net shrink is allowed when every lost node belongs to a
rebuilt source (a symbol genuinely removed) or a deleted file; it is still
refused when a node vanishes from a file we did NOT touch — the silent
failed/partial-extraction case the guard exists to catch.

The #1116 e2e test now asserts the prune happens with force=False (was force=True);
added two _check_shrink unit tests (allowed within rebuilt sources, refused
outside). Full suite 2339 passed; skillgen --check clean; ruff clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 11:29:08 +01:00
safishamsi b448c16cbd docs(changelog): add #1447 entry (non-hashable id/endpoint crash fix)
The #1447 cherry-pick only touched build.py/validate.py + tests, so it
shipped without a CHANGELOG note. Add it under Unreleased.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 09:10:54 +01:00
safishamsi c390456c6c fix: resolve Python ClassName.method() qualified calls to class-method nodes (#1446)
Cross-class qualified static calls like `CustomerTaskActions.approve(...)` did
not produce an EXTRACTED `calls` edge. Two compounding causes:

1. The shared cross-file pass skips all member calls (the #543/#1219 god-node
   guard against bare `obj.method()` name collisions), and there was no Python
   receiver-based resolver to recover the qualified ones.
2. When the called method shared its name with an in-file node — e.g. a viewset
   action `approve()` delegating to a service `Service.approve()` — the in-file
   bare-name lookup matched the caller's own node (tgt == caller), so the call
   was silently dropped before any raw_call was recorded.

Fix: capture a simple-identifier receiver in the call walk (new
`call_accessor_object_field`, set to `object` for Python), defer capitalized-
receiver member calls to a new `_resolve_python_member_calls` pass (mirroring the
Swift resolver), and emit an EXTRACTED edge only when the receiver resolves to
exactly one class that owns the method (single-definition god-node guard).
Instance/module calls (`self.x()`, `obj.x()`, lowercase receivers) are unaffected.

Tests: cross-class resolution, the same-method-name collision shape from the
issue, instance-call non-over-connection, and the ambiguous-class guard.
Full suite 2337 passed; skillgen --check clean; ruff clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 09:09:48 +01:00
dschwartzi 2c7cbb6530 Fix: validate_extraction crashes on non-hashable node id / edge endpoint
validate_extraction() is documented to return a list of error strings ("empty
list means valid"), but raised TypeError: unhashable type: 'list' when a node
id -- or an edge source/target -- was a non-hashable value such as a list. This
occurs in practice when an LLM extraction subagent emits malformed JSON like
{"id": ["foo", "bar"], ...}. Crash sites: the node_ids set comprehension and
the `edge[...] not in node_ids` membership tests.

Because build_from_json() validates at its start, a single malformed node
aborted the entire build, losing an otherwise-complete extraction of a large
corpus. build_from_json() itself would also raise (G.add_node(<list>) and the
`not in node_set` test) if the validator were bypassed.

- validate.py: build node_ids during the node pass, adding only hashable ids;
  report a non-hashable id/endpoint as an error string instead of crashing.
  All existing messages and the dangling-edge checks are preserved.
- build.py: skip dict nodes with a missing/non-hashable id and edges with
  non-hashable endpoints (stderr warning). Non-dict nodes are deliberately
  left to raise so the multigraph diagnostic still observes shape errors.
- tests: 3 cases in test_validate.py and 2 in test_build.py.
2026-06-24 08:54:11 +01:00
tpateeq ad6cb753c0 feat(install): add cross-framework agents platform (+ skills alias)
`graphify install --platform agents` installs the skill to the generic
Agent-Skills locations: the spec's user-global ~/.agents/skills (global) and
./.agents/skills (--project) — the directories `npx skills` and spec-compliant
frameworks read. `--platform skills` is an alias. Previously that user-global
location was only reachable as an accidental side effect of the gemini-on-Windows
branch. Bare `graphify install` is unchanged (still single-platform claude/windows).

The platform is registered in tools/skillgen/platforms.toml (split, mirroring
amp's agents-md body) and rendered through the skillgen drift/coverage guards.
Since it is a post-v8 platform with no own v8 body, its --audit-coverage baseline
is amp's v8 body (the body it re-homes). The rendered skill body is byte-identical
to amp's; only the on-demand hooks reference differs (its own `graphify agents
install` wording).

The `graphify agents install` / `graphify skills install` subcommand is the
amp-twin: it also wires an AGENTS.md always-on section, keeping it honest with the
hooks reference it points at. The `--platform agents` path stays skill-only,
exactly as amp's `--platform amp` does.

Also: `skill-agents.md` added to package-data, and the wheel-packaging guard now
covers every platform's skill body (not just references/always-on), so a missing
skill body fails CI instead of only breaking install for real users.

Closes #1405. Implements #1432.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 17:26:49 +01:00
safishamsi 29fd98fd04 Release 0.8.46
Query perf, a skill graph-health gate, and a batch of install/extraction fixes:
- #1431 trigram query prefilter (faster, results unchanged)
- #1437 Step 4.5 graph-health gate + semantic-cache anchored on the scan root
- #1411 CUDA (.cu/.cuh) via the C++ extractor
- #1402 cross-file type-annotation refs no longer duplicate nodes
- #1403 hermes install -> %LOCALAPPDATA% on Windows
- #1409 no punctuation-only Obsidian/Canvas filenames
- #1413 opencode reminder backtick command-substitution fix
- #1428 graphify extract --cargo handles missing Cargo.toml
- #1429 prs.py F821 cleanup

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v0.8.46
2026-06-23 13:21:38 +01:00
safishamsi 87fe887919 Changelog for the #1431 query prefilter and #1437 health-gate/cache-anchor merges
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 12:58:16 +01:00
Mamadou Bah e7ba16bfa3 skill: add Step 4.5 graph-health gate + anchor caches on the scan root
Two integrity improvements to the core skill runbook (rendered via skillgen):

1. Graph-health gate (new Step 4.5): runs diagnostics.diagnose_extraction
   read-only after build, before labeling, and surfaces dangling/missing/
   self-loop and same-endpoint-collapsed edges — the silent-corruption modes of
   incremental updates and AST/LLM id mismatches. Never aborts.
2. Anchor the AST and semantic caches on the scan root (root/cache_root=
   'INPUT_PATH') instead of the cwd, matching the CLI extract path
   (ast cache_root=out_root, check/save_semantic_cache(root=out_root)) and
   build_from_json/save_manifest (#1361 parity). Without this, running from a
   cwd != scan root cold-misses the cache and can split AST vs semantic caches.

Source edited in tools/skillgen/fragments/core/core.md; artifacts + expected/
regenerated via 'python -m tools.skillgen' (+ --bless). Full suite green
(2229 passed); skillgen + cache suites green (80 passed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 12:47:02 +01:00
Paulo Pinto 53edd27e6a perf(serve): trigram candidate prefilter to cut O(N) query latency
_score_nodes and _find_node scan every node per query (O(nodes x terms)),
so query latency scales with total graph size regardless of where the
answer lives. Add a lazily-built character-trigram index (cached on the
graph object, auto-invalidated on hot-reload like _idf_cache) that narrows
each query to a small candidate superset before the unchanged scoring loop.

Results are byte-identical: the index is a pure candidate generator over
the exact fields the scorer reads (norm_label, label_tokens, nid,
source_file); a non-candidate node always scores 0, and IDF stays a
whole-graph statistic. _find_node candidates are returned in graph
iteration order so its exact/prefix/substring ordering, and matches[0],
stay unchanged.

A selectivity guard falls back to the full scan when a query term is too
short to trigram or its rarest trigram is still common (broad terms like
model/client), preserving a never-worse contract.

The index builds eagerly at load and before a reloaded graph is swapped
in, so neither the first query nor the first post-reload query pays the
one-time build cost. Storage format is unchanged (in-memory index only).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 12:46:51 +01:00
safishamsi a4d09aefd8 Install the hermes skill to %LOCALAPPDATA% on Windows (#1403)
graphify install --platform hermes always wrote the skill to ~/.hermes/skills,
the POSIX path. On Windows, Hermes scans %LOCALAPPDATA%\hermes\skills, so the
installed skill was never discovered. _platform_skill_destination now has a
hermes branch: Windows -> %LOCALAPPDATA%\hermes\skills, other OSes unchanged
(~/.hermes/skills). Pure path logic — no skillgen regeneration.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 00:18:47 +01:00
safishamsi 0aeda15c10 Resolve cross-file type-annotation refs to a single node, not phantom duplicates (#1402)
A class defined once but referenced via type annotations in N other files appeared
as 1+N nodes — the extras carrying the referencing file's path (with extension)
baked into the id (e.g. pkg_a_py_thing). ensure_named_node's cross-file fallback
called add_node, which stamps the referencing file as source_file; that sourced
stub then collided in _disambiguate_colliding_node_ids (baking the .py path into
the id) and _rewire_unique_stub_nodes skipped it (a node with a source_file is
treated as a real definition, not a stub).

The fallback now emits a SOURCELESS stub (mirroring the inheritance-base path), so
disambiguation ignores it and the rewire collapses it onto the canonical
definition. The helper is duplicated across all six language extractors, so the
fix is applied to all six. Genuinely-defined duplicates (same name, different
files) still stay separate — only cross-file references collapse.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 00:17:59 +01:00
safishamsi 739230e17a Add regression tests for punctuation-only Obsidian/Canvas filenames (#1409)
Cover the #1410 fix: to_obsidian and to_canvas must never emit a punctuation-only
filename (e.g. `@.md` from a `@/*` tsconfig paths key) — valid on disk but empty
once a downstream tool re-slugs on word chars (crashes `qmd update`). Both tests
exercise the public exporters and fail against the pre-fix safe_name().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 23:15:59 +01:00
Luca Zanotti 30cc4c0670 fix(export): never emit punctuation-only Obsidian filenames (e.g. @.md)
An all-punctuation node label (e.g. `@/*` from a tsconfig paths entry) survived
the unsafe-char strip in `to_obsidian`'s `safe_name()` as a bare `@`, producing
`@.md`. That filename is valid on disk but empty once a downstream tool re-slugs
on word chars — qmd's handelize() reduces "@" -> "" and raises, aborting the
entire `qmd update` (every collection on the machine stops reindexing).

Require at least one word char in the stem; otherwise fall back to "unnamed"
(the existing dedup handles collisions). Applied to both safe_name occurrences.

Fixes #1409

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 23:10:49 +01:00
DhruvTilva 0b96b61609 fix: resolve F821 undefined name 'nx' in prs.py 2026-06-22 22:57:19 +01:00
raylei50653 dbce4532f4 Add CUDA (.cu/.cuh) support via the C++ extractor
CUDA is a C++ superset, so .cu/.cuh files parse cleanly with
tree-sitter-cpp (already a dependency). Two registrations wire it up:

- detect.py: add .cu/.cuh to CODE_EXTENSIONS so they're detected and
  watched (watch.py's _WATCHED_EXTENSIONS derives from CODE_EXTENSIONS).
- extract.py: route .cu/.cuh through extract_cpp in _DISPATCH, which
  also makes collect_files() pick them up (_EXTENSIONS = _DISPATCH.keys()).

Adds tests/fixtures/sample.cu (kernel + __device__/host functions +
struct + includes) and CUDA cases in test_languages.py covering kernel/
device function extraction, structs, includes, and host call edges.
Documents the new extensions in the README extension table and CHANGELOG.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 22:57:06 +01:00
WSHAPER 2323ce1937 fix(opencode): strip backticks from plugin reminder to prevent silent command substitution (#1413)
The opencode plugin template embedded the reminder string with backticks
around `graphify query "<question>"`. Because the plugin prepends
`echo "<reminder>" && <cmd>` to the user's bash command, those backticks
triggered bash command substitution: every grep/rg/find invocation silently
ran `graphify query "<question>"` and substituted its output
("No matching nodes found.") into the reminder text shown to the agent.

This both corrupted tool output with graphify noise, and actually spawned
a graphify process + loaded graph.json + ran a BFS traversal with the
literal token <question> on every search.

Fix: remove the backticks. Adds a guard comment in the template so future
editors don't reintroduce the bug, and a regression test that asserts the
reminder string contains no backticks and no $() constructs.
2026-06-22 22:54:03 +01:00
DhruvTilva aa06e10fa5 fix: catch OSError when Cargo.toml is missing during --cargo introspection 2026-06-22 22:54:03 +01:00
safishamsi 5862bce5ca Release 0.8.45
Path-portability cluster + native-backend hyperedge prompt fix:
- #1417 portable manifest.json from the skill runbooks
- #1418 hyperedge source_file relativization
- #1419 GRAPH_REPORT.md header shows the scan root
- #1423 GRAPHIFY_OUT honoured end-to-end
- native-backend extraction prompt now requests hyperedges (#1430)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v0.8.45
2026-06-22 22:32:52 +01:00
safishamsi aad3b47098 Request hyperedges in the native-backend extraction prompt (#1418 follow-up)
`graphify extract --backend <gemini|claude|claude-cli|openai|kimi|...>` produced
zero hyperedges for any corpus: llm._EXTRACTION_SYSTEM only showed
"hyperedges":[] in its output schema and never described what a hyperedge is, so
every model returned the empty array. Meanwhile the agent/skill path, whose
references/extraction-spec.md fully documents hyperedges ("3 or more nodes
participate together..."), produced them — the two prompts had drifted.

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

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 22:20:40 +01:00
safishamsi b8dc31f760 Honour GRAPHIFY_OUT end-to-end, not just in the path guards (#1423)
The GRAPHIFY_OUT override (custom output-dir name / absolute path, #686) was only
respected by some readers. `graphify extract` and several commands hardcoded the
literal "graphify-out", so `GRAPHIFY_OUT=custom-out graphify extract` still wrote
to graphify-out/ and downstream query/serve/update looked in the wrong place.

Resolve the output-dir name through graphify.paths everywhere it matters:
- new graphify.paths.out_path()/default_graph_json() helpers
- __main__: extract write dir, cluster-only/label, query/affected/benchmark
  defaults, save-result --memory-dir, uninstall --purge, cache-check
- detect: _MANIFEST_PATH, memory/ + converted/ dirs, and the scan-exclude (a
  renamed output dir is no longer re-ingested as source input)
- transcribe._TRANSCRIPTS_DIR; build_merge/serve/benchmark/prs graph-path defaults

Default behaviour is unchanged: with no env var everything still uses graphify-out/.
Verified end-to-end (extract -> cluster-only -> query under GRAPHIFY_OUT=custom-out
writes/reads custom-out/, no stray graphify-out/) and added a CLI regression test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 21:15:51 +01:00
safishamsi 6954a28da8 Show the scan root in the GRAPH_REPORT.md header instead of '.' (#1419)
The split-skill runbook passed '.' as report.generate's root argument in
Steps 4 and 5, so `/graphify /some/path` produced a report titled
"# Graph Report - ." regardless of the scanned directory. It now passes
'INPUT_PATH' (the Aider/Devin monoliths were already correct). Display-only:
no path written to graph.json or manifest.json was affected. Regenerated +
blessed the split-skill artifacts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 18:00:59 +01:00
safishamsi 1f42a2da2b Write a portable manifest.json from the skill runbooks (#1417)
The skill runbooks called save_manifest(...) with no root=, so manifest keys
were stored as absolute paths (e.g. /Users/.../main.go). Cloning or moving the
repo then broke `graphify --update`: detect_incremental matched none of the
cached keys, so the entire corpus re-extracted (and the report showed ghost
nodes). The native `graphify extract`/`update` CLI already passed root=target;
the agent-executed runbooks did not.

All four runbook call sites now pass root='INPUT_PATH', relativizing manifest
keys to the scan root (portable forward-slash form, per save_manifest's #777
support): the lean-core skill.md Step 9, the shared --update reference, and the
Aider/Devin monoliths.

The monolith edit is registered as a new sanctioned change-class
(_is_manifest_root_fix_line) in the round-trip guard, mirroring how the #1392
runbook fixes were sanctioned. Regenerated + blessed all artifacts; added a
regression test asserting every shipped runbook threads root= into save_manifest.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 17:29:33 +01:00
safishamsi d168de999a Relativize hyperedge source_file and honour GRAPHIFY_OUT everywhere (#1418, #1423)
#1418: build_from_json relativized source_file on nodes and edges but stored
graph.hyperedges[] verbatim, so a semantic subagent's absolute path leaked into
graph.json. Relativize hyperedges in build_from_json (to_json has no root to
relativize against), mirroring the existing node/edge handling.

#1423: consolidate the GRAPHIFY_OUT output-dir name into a single graphify.paths
module (was duplicated in __main__, cache, watch) and route the path guards
through it — security.validate_graph_path's base=None discovery + fallback,
callflow_html's project-root resolution, and the post-commit/post-checkout hook
bodies (which now read the env var at hook-run time). A renamed output dir is no
longer validated against the wrong base or missed by the hook.

Tests: hyperedge relativization (test_hypergraph), GRAPHIFY_OUT discovery
(test_security), updated the hook-body contract assertion (test_hooks).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 17:02:29 +01:00
Safi 5d053721ab Apply the #1392 runbook fixes to the Aider and Devin monolith skills
The monoliths are hand-maintained single files frozen against a pinned pristine-v8 blob by the round-trip guard, so they were excluded from the 0.8.44 #1392 batch. Evolve the guard from a positional zip (line-count-exact, single-line-class allowlist) to a multiset diff that classifies every added/removed line against documented sanctioned change-classes, so the multi-line fixes can land while any unsanctioned drift still fails. Add predicates for the four fix classes and broaden the enum/chunk-cleanup predicates to match both the v8 and rewritten forms.

Both monoliths now: thread directed=IS_DIRECTED through every build_from_json call (a --directed run no longer collapses reciprocal edges), scope semantic extraction to document/paper/image, unlink a stale .graphify_cached.json on a cache miss, and run Step 4's zero-node guard before any write with the report/analysis gated on to_json persisting the graph (#1392).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 16:17:49 +01:00
Safi dce54a007e Release 0.8.44
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v0.8.44
2026-06-19 15:45:24 +01:00
Safi 382b669481 Fix remaining #1392 skill bugs: --directed propagation, content-only semantic scope, cache staleness, video update, transcribe robustness
Closes the non-crash tier of #1392 in the Claude-path skill fragments:
- #6/#7: build_from_json/build_merge now take directed=IS_DIRECTED in Step 4, Step 5 rebuild, and the --update merge/diff, with prose telling the agent to substitute IS_DIRECTED like INPUT_PATH (a --directed run no longer silently rebuilds undirected and collapses reciprocal edges)
- #10: semantic extraction only flattens document/paper/image, not code (AST already covers code) so subagents stop re-reading every source file
- #12: .graphify_cached.json is deleted on a cache miss so Part C never merges a stale cache from a prior run
- #11: --update now transcribes changed video files and moves transcripts to documents before the semantic pipeline
- #4/#5/#23: transcribe writes via write_text (no shell redirect), uses GRAPHIFY_WHISPER_MODEL/PROMPT env, status to stderr
- #2/#3: add-watch and exports use $(cat graphify-out/.graphify_python) explicitly; MCP Desktop config documents the absolute interpreter path
- #21: extraction-spec example id namespaced (auth_session_validatetoken)
- #22: query term split keeps tokens >= 3 chars

aider/devin monoliths are pinned by the roundtrip invariant and excluded; their own Step 1 already instructs replacing python3 with the resolved interpreter.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 15:37:11 +01:00
Safi 30fe8bab61 Fix crash/data-loss bugs in the generated skill runbooks (#1392)
Five fixes in the skillgen fragments (re-rendered): chunk paths now derive from
cwd to match where Part C globs (a non-cwd scan produced "no nodes"); the
code-only fast path writes an empty .graphify_semantic.json so Part C doesn't
FileNotFoundError; --cluster-only relies on the self-contained CLI instead of
re-running Steps 5-9 against deleted intermediate files; and Step 4 runs the
zero-node guard before any write and only writes GRAPH_REPORT.md / analysis when
to_json actually persisted the graph (honoring the #479 shrink-guard).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 15:19:07 +01:00
Safi 7d07a24708 Don't Path()-coerce FileSlice units in the extract entry points (#1397, #1399)
The 0.8.43 str-path coercion (#1386) ran Path(f) over every item in
extract_files_direct, but extract_corpus_parallel feeds it FileSlice units from
the oversized-doc slicing (#1369), and Path(FileSlice) raises TypeError -- so
semantic extraction of any Markdown file larger than _FILE_CHAR_CAP crashed.
Coerce only non-Path/non-FileSlice entries. The #1386 tests used small files, so
slicing never ran; added a regression test with a file that actually slices.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 15:03:38 +01:00
Safi 435da06c70 Release 0.8.43
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v0.8.43
2026-06-19 10:31:01 +01:00
Safi 8e6ba9d714 Parse package manifests into canonical package nodes + depends_on edges (#1377)
apm.yml was a .yml document handled by the LLM, so the same package got a
different file-anchored node id from its own manifest than from each dependent's
dependency reference and split into duplicate nodes. New manifest_ingest module
parses apm.yml/pyproject.toml/go.mod/pom.xml deterministically into ONE package
node per package, keyed by name via ids.make_id, plus depends_on edges; routed
to the AST path (CODE) so the LLM never sees them. Package nodes are exempt from
the file-stem prefix remap so the canonical id is stable across manifests and
dedup collapses references to a single hub node.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 10:25:09 +01:00
Safi a78956424d Reject Windows-style git hooks paths instead of creating a junk dir (#1385)
On WSL/POSIX, Path("C:\\...").is_absolute() is False, so a Windows absolute
core.hooksPath (or rev-parse --git-path output) was joined under the repo root
and mkdir'd as a literal backslash-named junk directory while install reported
success and the real .git/hooks got nothing. Both branches of _hooks_dir now
reject drive-letter / backslash paths with a clear error so the failure is loud.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 10:06:07 +01:00
Safi e6eaad3d7e Emit edges for markdown links so hub docs connect (#1376)
extract_markdown only emitted heading nodes + contains edges and never parsed
link syntax, so a doc full of [text](./other.md) links (index.md,
table-of-contents.md) had no edges to the docs it links and never became a hub.
Add a deterministic link pass: inline, reference-style, and [[wikilinks]],
resolved relative to the source file, external URLs/anchors/images skipped, with
the target id built via the same _make_id recipe so the edge merges onto the
real doc node instead of an orphan.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 10:02:27 +01:00
Safi 5038129c7e Accept str paths in the semantic extract entry points (#1386)
extract_corpus_parallel and extract_files_direct are typed list[Path] but
crashed with AttributeError on str paths (f.suffix in slicing/partition, f.parent
in packing). Coerce files = [Path(f) for f in files] at both public entry points;
the AST extract() already coerced.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 10:01:31 +01:00
Safi 8883991c2e Unify node-ID normalization into a single source of truth (#1378)
extract._make_id and build._normalize_id were copy-pasted forks of the same
NFKC/casefold recipe kept in sync by mirrored docstrings -- the root of the
recurring ID-drift ghost-node bug class (#811/#550/#1033/#1104). Move the recipe
to graphify/ids.py and have all four producers delegate to it: extract, build,
and (completing the migration) mcp_ingest and symbol_resolution, whose
"avoid an import cycle" copies are moot now that ids.py is dependency-free. The
contract test asserts all four resolve to the shared recipe, with hypothesis
property tests for make_id == normalize_id and idempotency.

Co-Authored-By: danielnguyenfinhub <danielnguyenfinhub@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 22:55:44 +01:00
Safi ab1e0ec588 Adaptive split-and-retry for community labeling on parse failure (#1280, #1278)
label_communities logged-and-skipped any batch whose LLM response was malformed
JSON, silently losing ~100 names per failed batch on large graphs. Split the
batch at the midpoint and retry each half (smaller prompt -> smaller output),
mirroring _extract_with_adaptive_retry; the base case re-raises so the caller
skips just that batch. Removed leftover scaffolding and corrected the docstring.

Co-Authored-By: CJdev232 <CJdev232@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 22:50:52 +01:00
Safi 9e8fa2a28c Bump vulnerable dependencies to patched versions (#1375)
pypdf 6.11.0->6.13.3 (CVE-2026-48155/48156), yt-dlp 2026.3.17->2026.6.9,
pyjwt 2.12.1->2.13.0, cryptography 48.0.0->49.0.0, python-multipart
0.0.28->0.0.32, with lower-bound floors for the direct deps (pypdf, yt-dlp) so
installs get the patched versions. Lockfile regenerated for 0.8.42.

Co-Authored-By: hypnwtykvmpr <hypnwtykvmpr@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 22:48:50 +01:00
Safi baf2410f08 Release 0.8.42
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v0.8.42
2026-06-18 20:05:25 +01:00
Safi d12eb28c2d Note intra-file slicing in the JSON-truncation troubleshooting entry (#1369)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 02:26:01 +01:00
Safi 4f539e729f Slice oversized text documents so the whole file is extracted (#1369)
_read_files capped every file at 20,000 chars, so a Markdown/text/rST document
longer than that lost everything past the cap with no recovery. Oversized
splittable-text files are now split at heading/paragraph boundaries into units
that each fit the cap and together cover the whole file; every slice reports its
parent file as source_file so the graph isn't fragmented per-slice, and a slice
that still overflows output is bisected and retried. Code and PDFs are never
sliced.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 01:55:56 +01:00
Safi 897483fc96 Merge #1366: prune deleted-only and root the full build so --update stops duplicating
After #1361 added root= to the --update build_merge call, build_merge's prune
(which runs after the merge) began matching freshly re-extracted changed-file
nodes and deleting them — a regression in 0.8.41 where --update on a changed
file wiped its nodes. This drops `changed` from prune_sources (replace-on-
re-extract from #1344 already reconciles changed files), passes root= to the
full build's build_from_json so its node-key base matches the update side, and
pins the extraction-spec source_file to the verbatim path so the two runs never
drift. Re-blessed skillgen expected/ snapshots.

Co-Authored-By: RelywOo <RelywOo@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 01:28:19 +01:00
Safi 895a60f4e3 Model Java records as type nodes and extract constructor calls (#1373)
Add record_declaration to the Java class types so a record becomes a first-class
type node instead of an isolated file node, and add object_creation_expression
to call types with a dedicated branch that reads the callee from the `type`
field (new Foo() has no `name` field), so `new Foo(...)` emits a calls edge to
the constructed type.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 01:18:48 +01:00
Safi b2a1722903 Merge .graphifyignore with .gitignore instead of replacing it (#1363)
A .graphifyignore made graphify skip that directory's .gitignore entirely, so a
file excluded only by .gitignore (including neutrally-named secrets the
sensitive-file heuristic misses) got indexed into the graph and could leak into
committed graph artifacts. Read .gitignore first and .graphifyignore last so
their patterns merge and graphifyignore negations still win.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 01:15:24 +01:00
Safi 9e0b8766f4 Release 0.8.41
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v0.8.41
2026-06-17 19:09:43 +01:00