Add an exact wikilink and frontmatter layer to the vault graph #1545
Labels
No labels
P0
P1
P2
P3
bug
category/code-change
category/cross-repo
category/eval-run
category/investigation
category/no-test-target
category/owner-ruling
category/prose-artifact
category/recurring-chore
category/structure
category/umbrella
crc-refactor
create
delete
enhancement
filed-by/agent
filed-by/user
frozen
lint-rule
needs-info
needs-triage
next
plugin/cc-architect
plugin/os
plugin/os-adr
plugin/os-aidd-lint
plugin/os-backlog
plugin/os-context
plugin/os-doc-hygiene
plugin/os-sdlc
plugin/os-vault
project/cc-os
ready-for-agent
ready-for-human
recurring
review
rule-proposal
update
waiting
wayfinder:grilling
wayfinder:map
wayfinder:map
wayfinder:research
wayfinder:task
wayfinder:task
wontfix
worklist/deviations
worklist/lint-rule
worklist/new-implement-build
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
jared/cc-os#1545
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
LLM extraction drops most links that the vault notes already state. The vault graph therefore misses exact relationships that need no model.
The planned Outcome Engine needs edges from an outcome node to the notes it used (
docs/decision-system/vision.md). A graph that omits real links cannot give those edges reliably.Observed
An eval on 2026-11-04 measured recall on 103 real wikilinks in 40 notes. Results are in
plugins/os-vault/eval/graph-extraction/results/README.mdon branchfix-vault-rebuild-stale-lock.qwen3.5:9b, thinking offgemma4-e4b-16kqwen25-coder-7b-16kThe models also disagreed. Of 426 claims, 94.4% came from one model, 5.6% came from some models, and 0 came from all models.
graphify extractsends.mdfiles to the LLM. It does not run the deterministic Markdown extractor (graphify/extractors/markdown.py) on them. That extractor reads frontmatter, headings, and wikilinks. It runs throughwatch._rebuild_code, whichgraphify updateand the git hooks use. It skips docs that already have semantic nodes.Reproduce
n/a: this is a design gap. The eval README gives the measurement.
Approach
Goal: each real wikilink becomes an edge in the vault graph with no model call. The LLM layer adds only concepts that the notes do not state as links.
Open question, answered: plain
graphify updateadds no edges, because it skips docs that have semantic nodes (graphify 0.9.102watch.py:2507-2631).graphify update --forceadds them. It runs_reconcile_markdown_links(watch.py:785-900), which moves each authored link onto the semantic node of the note. An experiment on a copied sample confirmed this, with hand-made semantic nodes and AST target pages. The case of a target note with only semantic nodes needs confirmation on a real extraction graph.Design: run graphify's own Markdown pass after the LLM pass. Add
graphify update "$VAULT_PATH" --forceto_REBUILD_SCRIPTinplugins/os-vault/hooks/session_start.py, and chain it with&&. Each rebuild derives the edges again, so a rebuild keeps them. Do not write a new wikilink parser.Verify two facts before you commit the order of the steps:
graphify updatehas no code path to a model. The rebuild script comment warns thatcluster-onlycan select a cloud backend from an API key. Ifupdatecan do the same, stop and report it.update --forcekeeps the community data fromcluster-only. If it does not, or ifGRAPH_REPORT.mdomits the new edges, runupdatebeforecluster-only.Frontmatter stays a node attribute. Wikilinks in frontmatter become edges. Facet-tag edges are out of scope.
Depends on #1540.
_REBUILD_SCRIPTas changed here and the eval harness exist only on branchfix-vault-rebuild-stale-lock. #1540 forbids a merge of that branch alone. Build on that branch and do not merge to main before #1540 ships.~/.cache/graphify/vault-rebuild.lockis a deliberate hold lock: leave it in place.Measure with wikilink recall from the eval harness. Do not use hand labels.
Acceptance criteria
updatestep makes no model call.graph.jsonafter the step, and community data is present. Graphify may merge a semantic node into an AST heading node with the same file and label, and a genericreferencesrelation may lose to a specific relation on the same pair (build.py:233-250,build.py:1757-1884).updatestep fails, the rebuild writes no stamp, and the log states the failure.plugins/os-vault/eval/graph-extraction/bin/wikilink_recall.rbreads agraph.jsonand the sample notes with no LLM. It scores a link as found when any edge joins a node of the source note to a node of the target note. On the live vault graph after the rebuild script runs (not on a copy, which re-scans every note), with link targets resolved against that vault, it reports at least 95% recall on the 103 wikilinks of the 40-note sample. The 28.2% baseline used label matching, so it is not the same measure. Only 14 of the 103 links have a target inside the 40-note sample, so a graph of the sample alone cannot reach 95%.graphify query,graphify path, andgraphify explainrun on the resulting graph.Owner sketch
Owner:
_REBUILD_SCRIPTinplugins/os-vault/hooks/session_start.py. Language: Python holding a bash string. Caller: the detached rebuild thatmain()starts at SessionStart.New file:
plugins/os-vault/eval/graph-extraction/bin/wikilink_recall.rb. Language: Ruby, as the other scripts in that directory. It takes the wikilink regex and note walk from thedetblock ofbin/compare.rb, and it prints micro and macro recall. It counts connected note pairs, not relation names, because a specific LLM edge replaces a genericreferencesedge on the same pair (build.py:241). Caller: the harness README.Tests:
plugins/os-vault/tests/test_session_start_rebuild.pyand the fixture shim args (plugins/os-vault/tests/generate-fixtures.sh).Non-goals
Choosing the extraction model. #1540 covers that.
Origin
plugins/os-vault/eval/graph-extractionrun on 2026-11-04graphify extractroutes docs to the semantic pass (graphify 0.9.102cli.py:4356) ← UPSTREAM-TOOLplugins/os-vault/hooks/session_start.pyVERDICT: CREATE
Work started on branch ticket-1545.
Resolution
Done: Added graphify update --force as the third step of the vault rebuild and added bin/wikilink_recall.rb. Commits
cc6e38f3andb8335e06on branch ticket-1545, not merged.Evidence: Recall 100/103 (97.1%) on a full-vault copy; 100/100 of links with a target; 3 links have no target note. os-vault pytest: 12 passed. update ran with no network under unshare. AC3 and AC4 checked on a copy. A real rebuild is still unmeasured because the hold lock blocks rebuilds.
Follow-ups: Repeatable AC3 and AC4 tests: dropped, they would test graphify. Title-form links: dropped, vault content. Full LLM rebuild measure: covered by the #1540 rebuild.
Needs review: Merge branch ticket-1545 only after #1540 ships. #1540 forbids a merge of fix-vault-rebuild-stale-lock alone, and ticket-1545 contains that branch.
Correction to the recorded evidence, session b10276a0. The 97.1% recall came from a vault copy. The copy treated all 336 notes as new, so it does not represent the real rebuild. On the live vault,
graphify update --forceskipped notes that have semantic nodes and extracted 12 files. Live recall is 40.8%, up from 18.45%. AC7 is therefore not met on the live graph. Copy: 3734 nodes, 3814 edges. Live: 1583 nodes, 1589 edges. Live results by the fixed harness: linked pairs found 21 of 30 (baseline 9), original questions 12 of 12 (baseline 9), reworded questions 8 of 13 (no change). The copy's heading nodes lowered the rank on 4 questions. The live graph has no regression. The live step is applied to the graph only; no branch merged. Before this merges, the design must state how to reach full recall on a graph with semantic nodes without the copy's heading noise.Body edit, session b10276a0: AC7 now says the live vault graph after the rebuild script, not a copy. A copy re-scans every note and gave the false 97.1% (see the correction above). Fix on branch ticket-1545, commits
c284fa56andfe3d6555(not merged): stub_markdown_pages.py adds a page node to each note with no unique representative node; rebuild order is extract, cluster-only, stub step, update --force, update --force. Applied to the live graph. Live result: 100/103 (97.1%) on the 103-link sample, 660/687 (96.1%) on the whole vault, 660/663 (99.5%) of links whose target note exists. Graph 1583 to 1686 nodes, 1589 to 1814 edges, 330 communities, no old node or connected pair lost. Not merged.