feat: vault metabolism Phases 0-1 (link resolution + hebb health linters) - #1
Conversation
Staged plan for the capture/consolidate/decay loop: foundational link resolution, deterministic linters, a worklist-first health dashboard, an access-log observer, and the two-week falsification experiment that gates the scoring, consolidation, and decay phases. Code-checked against the current MCP handlers, links schema, and context resolution. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…x time Phase 0 of METABOLISM.md: the foundational graph fix. Link targets were stored raw and resolved with a substring LIKE match that could multi- or mis-resolve, so any graph metric built on the link table would be subtly wrong. Now each [[target]] resolves to a canonical note path, stored in a new nullable links.target_path column (NULL = dangling or ambiguous). - new core/links.go: shared exact resolver (strip #fragment, then exact path, basename with directory anchoring, then exact title; one match resolves, zero or many leaves NULL), in both an in-memory and a per-call DB form. Ambiguity is exposed to callers via a Resolution enum. - fullReindex resolves in a second pass over the complete notes set, so a forward reference to a note parsed later still resolves; IndexFile resolves per link against the live corpus. - idempotent migration adds target_path (and its index) to legacy index.db without a rebuild. - context.go outgoing()/resolvePath() now join on target_path instead of the substring LIKE, tightening seed and link resolution to exact. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase 0 resolved target_path correctly in fullReindex (two-pass, after all notes exist) but the incremental path did not. IndexFile resolved only a file's outgoing links against the notes present at that moment, with no revisit, so a link to a not-yet-indexed note stayed dangling forever: on a cold build via RefreshChanged (files indexed one at a time) and in steady state when a target note is created later. The file watcher and the read-time refresh both use this path, so valid links were reported as dangling. IndexFile now also re-resolves inbound dangling links whose canonical target matches the just-indexed note's keys (path, path without .md, basename, title), reusing the shared resolver. The work lives in IndexFile, which only runs for changed files, so the no-op read-time refresh stays free. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…zed) Phase 1 of METABOLISM.md: deterministic, read-only vault-health detectors surfaced as `hebb health`, the certain wins that need no access signal. - core/health.go: RunHealth returns []Finding for three detectors. Dangling links (target_path IS NULL after Phase 0). PARA drift (a 1-Projects note whose frontmatter status is done/closed/complete/archived, or untouched past project_stale_days). Oversized (estimated tokens over size_threshold AND three or more substantial H2/H3 sections, counted from the raw file since the indexed body has heading markers stripped). - cli/health.go: `hebb health` prints a worklist grouped by type, with a --json flag for the future dashboard. Advisory: exits 0 even with findings (unlike doctor), non-zero only on operational failure. - [health] config block (project_stale_days, size_threshold) with defaults. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 972b3f8629
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Pull request overview
Implements Phases 0–1 of the “vault metabolism” plan: making link resolution exact (by persisting a canonical links.target_path) and adding a new hebb health command that reports deterministic, read-only vault “linters” (dangling links, PARA drift, oversized notes).
Changes:
- Adds exact link-target resolution with a new nullable
links.target_path, resolves in batch on full reindex and incrementally on per-file indexing (including inbound re-resolution for forward references). - Introduces Phase 1 health detectors in
core/health.goand surfaces them via a newhebb healthCLI (text +--json), with configurable thresholds via a new[health]config block. - Adds extensive unit/CLI tests covering link resolution precedence, forward references, and health detector outputs.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| METABOLISM.md | Documents the staged “vault metabolism” plan and how Phases 0–1 fit into it. |
| core/vaultconfig.go | Adds [health] config block and defaulting accessors for detector thresholds. |
| core/config.go | Plumbs the committed [health] config into the runtime core.Config. |
| core/db.go | Adds schema migration for links.target_path and an index for it. |
| core/index.go | Adds a second-pass link resolution step after full reindex has written all notes. |
| core/single.go | Updates incremental indexing to resolve target_path at write time and re-resolve inbound links when a target appears. |
| core/links.go | Implements exact resolver logic (path/basename/title precedence), full-reindex resolution, and inbound re-resolution helpers. |
| core/context.go | Switches outgoing link traversal to join via links.target_path (exact resolution). |
| core/links_test.go | Adds resolver + indexing tests (forward references, ambiguity, migration behavior, incremental re-resolution). |
| core/health.go | Adds Phase 1 health detectors (dangling links, PARA drift, oversized). |
| core/health_test.go | Adds unit tests for all health detectors and config defaults/custom values. |
| cli/root.go | Registers the new health subcommand. |
| cli/health.go | Implements hebb health command with grouped text output and JSON mode. |
| cli/health_test.go | Adds end-to-end tests for hebb health (text, JSON, exit code behavior). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…d re-resolve Addresses PR #1 review (Codex + Copilot) on the Phase 0 resolver: - Legacy migration now backfills target_path once when the column is newly added, so an upgraded pre-Phase-0 index.db resolves existing links on open instead of reporting every valid link as dangling until a full reindex. - noteKeys emits every directory-suffix form (x/dir/Note, dir/Note, Note), so directory-anchored [[dir/Note]] links re-resolve incrementally exactly as a full reindex would. - Incremental re-resolution reconsiders all matching links, not only NULL ones, so a newly-ambiguous target flips a stale pointer back to NULL; RemoveFile re-resolves links that pointed at a removed note so they dangle or fall back. Incremental now converges to the same state as FullReindex. - IndexFile builds the in-memory note index once per call (was a full notes scan per link, O(links x notes)); inbound candidates are scoped in SQL to the note's keys (with escaped LIKE and an exact Go-side recheck) instead of scanning all dangling targets. The no-op refresh still does zero resolution work, so the read hot path is unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… errors Addresses PR #1 review: - The dangling detector reclassifies NULL target_path links by re-running the resolver: a genuinely unresolved link is dangling_link ("resolves to no note"), a multi-match is ambiguous_link ("is ambiguous (matches multiple notes)"), so the worklist wording is accurate. - hebb health now returns the RefreshChanged error instead of discarding it, so a refresh failure exits non-zero (an operational failure) rather than silently running detectors on a stale index. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks both. All findings addressed in
Regression tests added for each; full suite green and verified end to end, including a reconstructed pre-Phase-0 index. Resolving these threads. |
First two phases of the vault-metabolism plan in METABOLISM.md: ship the certain, zero-risk wins and the foundational graph fix they depend on. The risky scoring/consolidation/decay phases stay gated behind the two-week access-signal experiment and are not in this PR.
What's here
Phase 0 — exact link resolution (foundational)
Link targets were stored raw and resolved with a substring
LIKEmatch that could multi- or mis-resolve, so any graph metric built on the link table would be subtly wrong. Each[[target]]now resolves to a canonical note path stored in a new nullablelinks.target_pathcolumn (NULL = dangling or ambiguous). Exact precedence: strip#fragment, then exact path, basename with directory anchoring, then exact title. Full reindex resolves in a second pass (forward references resolve); an idempotent migration upgrades a legacyindex.dbwithout a rebuild.Phase 1 —
hebb healthlintersDeterministic, read-only detectors surfaced as
hebb health(text worklist +--jsonfor the future dashboard):target_path)1-Projects/note whose frontmatterstatusis done/closed/complete/archived, or untouched pastproject_stale_days)size_thresholdAND 3+ substantial H2/H3 sections)Advisory by design: exits 0 even with findings (unlike
doctor), non-zero only on operational failure. Thresholds live in a[health]config block.Bug caught during review (commit
a8f76a2)Unit tests were green, but running the real
hebb healthbinary against a throwaway vault showed a valid[[Note B]]link reported as dangling. Phase 0 resolved correctly in a full reindex but not in the incremental path that the file watcher and read-time refresh actually use, so any link to a note indexed or created later stayed permanently dangling. Fixed insideIndexFileso inbound links re-resolve when their target appears, without adding work to the no-op refresh hot path.Verification
gofmt,go vet,go build,go test ./...all clean (uncached), plus an end-to-end smoke test of thehebb healthbinary confirming all three detectors fire with no false positives.Not in scope
Phase 2 (structural metrics + dashboard) and Phases 3-5 (access log, the experiment, consolidation, decay). See METABOLISM.md for the gated sequencing.
🤖 Generated with Claude Code