On Windows, to_wiki crashes with OSError [Errno 22] when a node label
contains any of < > : " / \ | ? * (Windows reserved chars in
filenames). Repro: an Obsidian note titled "Czy Wii jeszcze ok?.md"
becomes a community label, then write_text fails because Windows
rejects the question mark in the path.
The previous _safe_filename only handled three characters (/ space :)
which is enough on Linux/macOS but not on Windows. Extended the
substitution to cover all Windows-reserved chars plus trailing dots
and spaces (also reserved on Windows), and added a 200-char length
cap to stay well under MAX_PATH-segment limits.
Falls back to 'unnamed' if sanitization produces an empty string,
so we never try to create a file named "" or ".".
Tested on a 1773-file vault with mixed Polish/English content where
~12 community labels and several god node labels contained reserved
characters. Generates 763 wiki articles cleanly.
* Fix#563: prevent rationale-node leakage and preserve calls direction
Two AST-extractor bugs were inflating god-node centrality:
1. Rationale leakage in cross-file resolvers
_resolve_cross_file_imports indexed any node whose label didn't end in
')' or '.py' as an importable entity, so rationale nodes (whose labels
are docstring text) ended up in both stem_to_entities and local_classes.
Result: phantom '<file>_rationale_N --uses--> ImportedThing' edges for
every imported entity in every file with docstrings.
The same leak existed in cross-file call resolution via
global_label_to_nid, where rationale labels could collide with callee
names.
Fix: skip nodes with file_type == 'rationale' in all three index loops.
2. Calls / rationale_for direction lost at export
to_json() called nx.json_graph.node_link_data() on a NetworkX undirected
Graph, which canonicalizes endpoint order. The build path already
stashes _src and _tgt on every edge for exactly this case (with a
comment to that effect at build.py:100), but the exporter never
consulted them, so 'calls' and 'rationale_for' edges were emitted with
arbitrary direction.
Fix: in to_json(), restore link['source'] / link['target'] from
_src / _tgt, then pop those internal fields before writing graph.json.
Repro on a minimal 3-file project (queue.py + contact_form.py + a test
file with multi-paragraph docstrings) showed:
Before fix: 31 edges, 12 incident on SQLiteQueue, 7 phantom uses-edges
from rationale nodes, 1 inverted calls edge.
After fix: 24 edges, 5 incident on SQLiteQueue, 0 phantom edges,
all calls edges go caller -> callee.
Adds tests/test_issue_563.py with 5 regression tests covering both bugs
plus a direction-pin on the existing sample_calls.py fixture. Full suite
passes (446/446).
* Address Copilot review on #576
- tests: identify rationale nodes by file_type metadata, not id substring
(decouples regression from current `<stem>_rationale_<line>` naming)
- tests: assert contact_form -> SQLiteQueue.enqueue calls edge directly
(previously only inversions were guarded; a dropped enqueue edge would
have passed silently)
- extract.py: clarify cross-file import indexing comment — the filter
intentionally indexes only class-level entities (function/method labels
end in "()" and are excluded by design)
* Fix#563 (HTML export): restore arrow direction in graph.html
to_json was patched to consult _src/_tgt before serializing edge
endpoints, but to_html still read endpoints directly from G.edges(),
so calls and rationale_for arrows in the rendered graph.html still
pointed in canonicalized (often inverted) order.
Apply the same direction restore in to_html when building vis.js
edges. Use data.get (not pop) since G.edges(data=True) yields the
live attribute dict and other exporters may run after to_html.
Adds test_to_html_preserves_calls_and_rationale_for_direction:
parses RAW_EDGES out of the rendered HTML and pins both the
caller->callee assertions from #563 and the rationale->parent
direction. Without the fix, the test reports 11 flipped arrows
on the 3-file repro.
to_wiki() writes a fresh set of community + god-node articles each call but
never deletes old files from previous runs. Since community labels are
LLM-generated and non-deterministic across rebuilds (per skill.md Step 5),
the same conceptual community is often named differently each time, leaving
its previous file as an orphan. After N rebuilds, wiki/ contains roughly N
times the active article count, with index.md only referencing the most
recent run's labels.
Real-world: a knowledge corpus accumulated 822 wiki .md files over 5
rebuilds, of which only 111 were referenced by index.md (710 orphans).
Fix: clear *.md files in the output directory at the start of to_wiki().
This is consistent with its existing fully-regenerative behavior — it
always writes the full set of articles + index, never partial updates.
Subdirectories and non-.md files are preserved (only top-level .md is
touched), so any user-placed auxiliary assets survive.
Tests: two new regression tests cover (1) stale article cleanup across
runs with different labels, and (2) preservation of non-.md user files
and nested subdirectories.
Closes#540.
Production audit on a 10,129-edge graph showed the INFERRED
confidence_score distribution is bimodal, not graded:
| Score bucket | Count | % of INFERRED |
|--------------|-------|---------------|
| <0.4 | 0 | 0% |
| 0.4-0.6 | 5,807 | 57% |
| 0.6-0.8 | 14 | 0.1% |
| 0.8+ | 4,308 | 42% |
Subagents collapse the continuous "0.4-0.9" guidance to a binary:
0.5 for "uncertain", 0.85+ for "confident", almost nothing in between.
Downstream filtering by confidence is therefore an on/off switch, not
the gradient the prompt promises.
Replace continuous ranges with a forced-rank discrete set:
0.95 direct structural evidence
0.85 strong inference
0.75 reasonable inference
0.65 weak inference
0.55 speculative but plausible
Models follow discrete rubrics far better than continuous ranges
(documented in calibration literature; same reason MCQ rubrics
outperform 0-100 scales). The set is anchored at non-round midpoints
to discourage 0.5 as a default.
Applied uniformly across all 10 skill-*.md files:
- 7 long-form (skill.md, skill-codex.md, skill-copilot.md,
skill-droid.md, skill-opencode.md, skill-windows.md, skill-trae.md):
full forced-rank table.
- 3 short-form (skill-claw.md, skill-aider.md, skill-kiro.md):
inline set notation INFERRED ∈ {0.55, 0.65, 0.75, 0.85, 0.95}.
Pure prompt edit — no code changes, no test impact. Effect is
observable only via re-extraction and inspection of the new
confidence_score distribution.
Closes#538.
The full-pipeline path's Step 9 already calls save_manifest, so the
NEXT --update can diff against the full-rebuild's baseline.
But the --update flow itself does NOT save the manifest after merging
the incremental result. Consequence: a file deleted between
--update #1 and --update #2 keeps reappearing in #2's deleted_files
list (since the manifest still records its old mtime), generating a
spurious ghost-node prune attempt every run.
Add save_manifest(incremental['files']) at the end of the --update
merge step in both skill.md and skill-copilot.md (the only two skill
files carrying the merge step verbatim). The 'incremental' dict is
already in scope from the prune block above; its 'files' key is the
full current file list, which is exactly what save_manifest needs.
Pure prompt edit — no code changes, no test impact.
Closes#539.
The current prune message in the --update flow is ambiguous:
Pruned 0 ghost nodes from 13 deleted file(s)
A reader can't tell whether (a) 13 files deleted, 0 of them had nodes
worth pruning (benign — graph already clean), or (b) 13 ghost nodes
still exist and only 0 got pruned (bug). The denominator is opaque.
Split into two messages so the semantics are explicit:
- Drift case: "Pruned N ghost node(s) from M deleted file(s) — drift
detected and corrected."
- No-drift case: "M file(s) deleted since last run, but no ghost
nodes were present in the graph — no drift."
Applied to both skill.md and skill-copilot.md (the only two skill
files carrying the --update merge step verbatim).
When a user configures core.hooksPath = ~/gitconfig/hooks in .gitconfig,
_hooks_dir() was constructing Path("~/gitconfig/hooks") without calling
expanduser(), so hooks were installed into <repo>/~/gitconfig/hooks instead
of the intended absolute path.
Add Path.expanduser() call immediately after reading the raw string from
git config, before the is_absolute() / root-relative fallback logic.
Fixes#547
AST and semantic entries now write to cache/ast/ and cache/semantic/
respectively. Previously both used the flat cache/ dir causing semantic
results to overwrite AST entries for code files on mixed corpora, making
the shrink guard fire on every subsequent update run.
Migration: load_cached falls back to legacy flat cache/ for AST reads
so existing cache entries are not lost on upgrade.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Grep and Glob tools removed in CC v2.1.117; searches now go through Bash.
Hook now reads stdin tool_input and pattern-matches on search commands.
Uninstall/reinstall handles both old and new matcher for clean upgrades.
Closes#578
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- #550: _file_stem() includes parent dir to prevent node ID collisions for same-named files
- #555: extract() relativizes source_file paths before returning for cross-machine portability
- #562: to_json() returns bool; _rebuild_code() writes report/html only if json succeeded
- #563: skill prompts store rationale as node attribute, not separate node; enforce calls direction
- #566: Show All / Hide All buttons added to HTML community panel
- #575: _import_js() resolves tsconfig.json compilerOptions.paths aliases before external fallback
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- analyze.py: add seed=42 to betweenness_centrality() — eliminates non-deterministic GRAPH_REPORT.md diffs on graphs >1000 nodes (#499)
- extract.py: fix common-root inference to stop at first diverging segment not sum of all matches (#502)
- extract.py: resolve root to absolute path; post-process file node IDs to project-relative after extraction so graph.json edge endpoints are stable across machines (#502)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>