Skip to content

feat: vault metabolism Phases 0-1 (link resolution + hebb health linters) - #1

Merged
cizer merged 7 commits into
mainfrom
feat/vault-metabolism
Jun 15, 2026
Merged

feat: vault metabolism Phases 0-1 (link resolution + hebb health linters)#1
cizer merged 7 commits into
mainfrom
feat/vault-metabolism

Conversation

@cizer

@cizer cizer commented Jun 15, 2026

Copy link
Copy Markdown
Owner

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 LIKE match 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 nullable links.target_path column (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 legacy index.db without a rebuild.

Phase 1 — hebb health linters
Deterministic, read-only detectors surfaced as hebb health (text worklist + --json for the future dashboard):

  • dangling links (NULL target_path)
  • 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 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 health binary 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 inside IndexFile so 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 the hebb health binary 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

cizer and others added 5 commits June 15, 2026 21:21
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>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread core/db.go Outdated
Comment thread core/links.go Outdated
Comment thread core/links.go Outdated
@cizer
cizer requested a review from Copilot June 15, 2026 21:00
@cizer cizer self-assigned this Jun 15, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.go and surfaces them via a new hebb health CLI (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.

Comment thread core/db.go Outdated
Comment thread core/health.go Outdated
Comment thread cli/health.go
Comment thread core/links.go
Comment thread core/links.go Outdated
cizer and others added 2 commits June 15, 2026 22:41
…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>
@cizer

cizer commented Jun 15, 2026

Copy link
Copy Markdown
Owner Author

Thanks both. All findings addressed in a83e3dd and d013d4b:

  • Legacy index.db migration now backfills target_path on the upgrade, so existing links resolve on open instead of reporting as dangling (core/db.go, a83e3dd).
  • noteKeys includes directory-anchored suffixes so [[dir/Note]] re-resolves incrementally, matching FullReindex (a83e3dd).
  • Incremental re-resolution reconsiders non-NULL links and handles target removal, converging to the same state as FullReindex (a83e3dd).
  • IndexFile builds the note index once per call (no more O(links x notes)); inbound candidates are scoped in SQL to the note's keys instead of scanning every dangling target, and the no-op refresh still does zero work (a83e3dd).
  • Dangling vs ambiguous links are now distinct findings with accurate wording (core/health.go, d013d4b).
  • hebb health returns the RefreshChanged error instead of discarding it, so a refresh failure exits non-zero (d013d4b).

Regression tests added for each; full suite green and verified end to end, including a reconstructed pre-Phase-0 index. Resolving these threads.

@cizer
cizer merged commit e7346c3 into main Jun 15, 2026
5 checks passed
@cizer
cizer deleted the feat/vault-metabolism branch June 21, 2026 13:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants