@theglitchking/semantic-memory 1.2.1 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/.claude-plugin/marketplace.json +4 -4
  2. package/.claude-plugin/plugin.json +3 -3
  3. package/CHANGELOG.md +144 -0
  4. package/ROADMAP.md +5 -2
  5. package/commands/healthcheck.md +13 -11
  6. package/dist/{chunk-EVAJGOCB.js → chunk-2EELGTLX.js} +49 -5
  7. package/dist/chunk-2EELGTLX.js.map +1 -0
  8. package/dist/chunk-5ZKNO6ZD.js +96 -0
  9. package/dist/chunk-5ZKNO6ZD.js.map +1 -0
  10. package/dist/chunk-7M3JZOYH.js +105 -0
  11. package/dist/chunk-7M3JZOYH.js.map +1 -0
  12. package/dist/chunk-A3KYHKB5.js +176 -0
  13. package/dist/chunk-A3KYHKB5.js.map +1 -0
  14. package/dist/{chunk-CYWPDFNI.js → chunk-CSXZ7QUU.js} +4 -203
  15. package/dist/chunk-CSXZ7QUU.js.map +1 -0
  16. package/dist/chunk-FM26NFTR.js +272 -0
  17. package/dist/chunk-FM26NFTR.js.map +1 -0
  18. package/dist/chunk-NTYYS53O.js +91 -0
  19. package/dist/chunk-NTYYS53O.js.map +1 -0
  20. package/dist/{chunk-E2Z4J4MK.js → chunk-OWZWMB4P.js} +70 -2
  21. package/dist/chunk-OWZWMB4P.js.map +1 -0
  22. package/dist/chunk-S4KW7VH4.js +203 -0
  23. package/dist/chunk-S4KW7VH4.js.map +1 -0
  24. package/dist/{chunk-HODDRXJK.js → chunk-V45NJIHI.js} +4 -2
  25. package/dist/chunk-V45NJIHI.js.map +1 -0
  26. package/dist/chunk-WSU4QIPB.js +311 -0
  27. package/dist/chunk-WSU4QIPB.js.map +1 -0
  28. package/dist/cli/index.js +265 -65
  29. package/dist/cli/index.js.map +1 -1
  30. package/dist/core/index.d.ts +39 -2
  31. package/dist/core/index.js +17 -11
  32. package/dist/decay-ELJUKVQG.js +14 -0
  33. package/dist/dossier-2UG724LB.js +39 -0
  34. package/dist/healthcheck-IMB47LZE.js +19 -0
  35. package/dist/indexer-YW4O3VVM.js +8 -0
  36. package/dist/lexicon-5PXQEMUF.js +26 -0
  37. package/dist/lexicon-5PXQEMUF.js.map +1 -0
  38. package/dist/lint-XUQ7JDYX.js +16 -0
  39. package/dist/lint-XUQ7JDYX.js.map +1 -0
  40. package/dist/mcp/server.js +478 -40
  41. package/dist/mcp/server.js.map +1 -1
  42. package/dist/profile-LXUEF7TX.js +20 -0
  43. package/dist/profile-LXUEF7TX.js.map +1 -0
  44. package/dist/{schema-F3RTY6SQ.js → schema-ATFCLC6Y.js} +2 -2
  45. package/dist/schema-ATFCLC6Y.js.map +1 -0
  46. package/dist/{search-27WYAI24.js → search-AHKEFI2B.js} +2 -2
  47. package/dist/telemetry-GB2TEHVE.js +21 -0
  48. package/dist/telemetry-GB2TEHVE.js.map +1 -0
  49. package/hooks/vault-context.js +296 -12
  50. package/package.json +2 -1
  51. package/skills/vault-first/SKILL.md +18 -0
  52. package/dist/chunk-CAN2GWUG.js +0 -88
  53. package/dist/chunk-CAN2GWUG.js.map +0 -1
  54. package/dist/chunk-CYWPDFNI.js.map +0 -1
  55. package/dist/chunk-E2Z4J4MK.js.map +0 -1
  56. package/dist/chunk-EVAJGOCB.js.map +0 -1
  57. package/dist/chunk-HODDRXJK.js.map +0 -1
  58. package/dist/healthcheck-7YOPNDXN.js +0 -14
  59. package/dist/indexer-KD6UTXZB.js +0 -8
  60. package/dist/lint-GMXQF4RW.js +0 -11
  61. /package/dist/{healthcheck-7YOPNDXN.js.map → decay-ELJUKVQG.js.map} +0 -0
  62. /package/dist/{indexer-KD6UTXZB.js.map → dossier-2UG724LB.js.map} +0 -0
  63. /package/dist/{lint-GMXQF4RW.js.map → healthcheck-IMB47LZE.js.map} +0 -0
  64. /package/dist/{schema-F3RTY6SQ.js.map → indexer-YW4O3VVM.js.map} +0 -0
  65. /package/dist/{search-27WYAI24.js.map → search-AHKEFI2B.js.map} +0 -0
@@ -5,14 +5,14 @@
5
5
  "email": "theglitchking@users.noreply.github.com"
6
6
  },
7
7
  "metadata": {
8
- "description": "Single-plugin marketplace for semantic-memory — the unified memory-layer plugin. v1.1.0 absorbs brain's strengths: AGENTS.md contract artifact, hard-gated verification sessions, multi-agent skill bundler (codex/copilot/pi), and SessionStart drift detection — all backwards-compatible with v1.0.x via deprecation shims.",
9
- "version": "1.2.1"
8
+ "description": "Single-plugin marketplace for semantic-memory — the unified memory-layer plugin. v1.1.0 absorbs brain's strengths: AGENTS.md contract artifact, hard-gated verification sessions, multi-agent skill bundler (codex/copilot/pi), and SessionStart drift detection. v1.3 adds confidence decay — age-aware retrieval ranking with verify_note to reset the clock. All backwards-compatible with v1.0.x via deprecation shims.",
9
+ "version": "1.5.0"
10
10
  },
11
11
  "plugins": [
12
12
  {
13
13
  "name": "semantic-memory",
14
- "description": "Unified memory-layer plugin for Claude Code. One MCP server, 40 tools across multi-corpus search/read/write/graph/lint/sessions/contract. v1.1.0 adds: AGENTS.md contract artifact, hard-gated verification sessions (session_start/run/finish/status), proposal-mode synthesize_note + synthesize_promote, multi-agent skill bundler (codex/copilot/pi via the new `skills` CLI), and SessionStart drift detection. Backwards-compatible with v1.0.x — every previous tool name remains callable through deprecation shims (removed in v2.0.0).",
15
- "version": "1.2.1",
14
+ "description": "Unified memory-layer plugin for Claude Code. One MCP server, 44 tools across multi-corpus search/read/write/graph/lint/sessions/contract. v1.1.0 adds: AGENTS.md contract artifact, hard-gated verification sessions (session_start/run/finish/status), proposal-mode synthesize_note + synthesize_promote, multi-agent skill bundler (codex/copilot/pi via the new `skills` CLI), and SessionStart drift detection. v1.2 completes the hygiene line: `/healthcheck --fix` auto-remediation, `lint_vault({checks:['code_symbols']})` code-path drift, and a fresh-install CI smoke gate. v1.3 adds confidence decay: age-aware search ranking (0.5^(age/half-life), type-aware, floored, composes with load_priority), a `decay` block surfaced on decayed results, `verify_note` to reset the clock, `evergreen` opt-out, and `decay-config`/`decay-trace` CLI introspection. v1.3.1 adds local-only selection-logging telemetry (which note each search result actually got used), the `decay_candidates` lint, and a `selection-stats` CLI. v1.4.0 is the resident-expert bridge: a learned human→artifact lexicon (`manage_lexicon`) with propose-and-confirm alias capture, verbatim symptom capture + symptom-keyed indexing (terse symptom queries match the note), Tier-1 lexicon query expansion in the hook, archive path-class down-weight, injection hygiene (score-gate + dedup + top-3 + compact instructions), section-targeted `read_note`, and opt-in conditional tool registration. v1.5.0 is the expert-character layer: entity dossiers (`manage_dossier`) — per-component living notes (purpose / failure modes / knobs / incident log / current state) that accrete in place, with two-hop retrieval (utterance→entity→dossier injected first); usage-feedback ranking that boosts repeatedly-cited notes (capped, composes with decay/path-class) plus a `decoys` lint for retrieved-but-never-cited notes (surfaced, never auto-down-ranked); session paging (Stop drafts a durable session digest as a proposal, SessionStart pages in a curated digest instead of a broad sweep); and a speaker profile (`manage_profile`) that models how the human communicates — severity calibration, chronic omissions, verbosity, shorthand — injected at SessionStart. Backwards-compatible with v1.0.x — every previous tool name remains callable through deprecation shims (removed in v2.0.0).",
15
+ "version": "1.5.0",
16
16
  "author": {
17
17
  "name": "TheGlitchKing"
18
18
  },
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "semantic-memory",
3
3
  "description": "Unified memory-layer plugin for Claude Code. One MCP server, multiple typed corpora (vault / code / plans / docs / research / project-map), unified cross-corpus knowledge graph, conditional tool registration, in-process drift detection, and human↔code translation. Rebrand of `semantic-sidekick` at v1.0.0 — see CHANGELOG for migration.",
4
- "version": "1.0.1",
4
+ "version": "1.5.0",
5
5
  "author": {
6
6
  "name": "TheGlitchKing",
7
7
  "email": "theglitchking@users.noreply.github.com"
8
8
  },
9
9
  "license": "MIT",
10
- "homepage": "https://github.com/TheGlitchKing/semantic-sidekick",
11
- "repository": "https://github.com/TheGlitchKing/semantic-sidekick",
10
+ "homepage": "https://github.com/TheGlitchKing/semantic-memory",
11
+ "repository": "https://github.com/TheGlitchKing/semantic-memory",
12
12
  "keywords": [
13
13
  "mcp",
14
14
  "memory-layer",
package/CHANGELOG.md CHANGED
@@ -2,6 +2,150 @@
2
2
 
3
3
  All notable changes to semantic-memory (formerly semantic-sidekick) will be documented here.
4
4
 
5
+ ## [1.5.0] - 2026-07-11 — Expert-character layer (v1.5.0: Phases 8–11)
6
+
7
+ Where v1.4.0 taught the plugin *how this human talks about the system*, v1.5.0 gives it the other two traits of a resident expert: it organizes memory by **component** (not by file) and **learns from its own usage**. Tool surface 42 → 44 (`manage_dossier`, `manage_profile`; read-only unchanged at 21). Additive throughout; `usage_boost.enabled: false` reproduces byte-identical pre-v1.5 ranking.
8
+
9
+ ### Added
10
+
11
+ - **Entity dossiers** (`src/core/dossier.ts`, **`manage_dossier`**). Per-component living notes at `<vault>/dossiers/<slug>.md` (`dossier` note type) with fixed sections — Purpose / Failure modes / Knobs & commands / Incident log / Current state. Knowledge accretes **in place** (incidents append, current-state is revised; never a new note per event). `manage_dossier` actions: `init | append_incident | set_state | get | list`. CLI: `semantic-memory dossier init <entity> [--alias …] [--seeded-from …]` / `dossier list`. Dossier `aliases:` fold into the lexicon compiler so Tier-1 expansion routes to them.
12
+ - **Two-hop retrieval** (`resolveDossierForPrompt` + `formatDossierHead` in `vault-context.js`). When an utterance names a tracked entity/alias, the dossier head (Purpose + Current state + a read-the-Incident-log pointer) is injected **first**, ahead of semantic hits — utterance → entity → dossier. Finally makes the graph load-bearing at query time.
13
+ - **Usage-feedback ranking** (`src/core/usage-boost.ts`, `applyUsageBoost`). Consumes the v1.3.1 selection log: repeatedly-cited notes earn a bounded boost `min(cap, 1 + citations·per_citation)` (default `cap: 1.5`, `per_citation: 0.1`, `usage_boost` block in `vault.schema.yml`), composed **multiplicatively** with decay + path-class + `load_priority` at the shared `context.ts` rank site. TTL-cached (15s) citation snapshot; boosted results carry a `usage: {citations, multiplier}` block. The cap stops a feedback runaway.
14
+ - **Decoys lint** (`findDecoys`, `lint_vault({checks:["decoys"]})`). Notes retrieved 3+ times but never cited are **surfaced for review, never auto-down-ranked** — down-rank on evidence, never on ambiguous inference (decision Q5).
15
+ - **Session paging.** Stop: the session-close prompt drafts a durable digest (decisions / resolutions / task-state) via `synthesize_note` as a reviewable **proposal** (`proposal_subdir: "sessions"`), graduated by `synthesize_promote` (decision Q10). SessionStart: `buildSessionStartDigest` pages in a curated block (active task + last session digest + dossier current-states + mode) instead of a broad whole-vault sweep when there's durable state; falls back otherwise.
16
+ - **Speaker profile** (`src/core/profile.ts`, **`manage_profile`**). `profile` note type at `<vault>/profile/speaker.md` (evergreen), fixed sections — Severity calibration / Chronic omissions / Verbosity preference / Shorthand & terms. Injected at SessionStart (`<vault-speaker-profile>`, capped, silent until filled). `manage_profile` actions: `init | get | update_section`. CLI: `profile init|show`. Updated via a correction cue ("when I say X I mean Y") folded into the existing capture-cue machinery, routed to `manage_profile` rather than `synthesize_note`. One human, one profile.
17
+
18
+ ### Decisions (locked at kickoff, 2026-07-11)
19
+
20
+ Q5 — decoys stay lint-only. Q9 — conditional tool registration stays default OFF (`tools/list_changed` reliability unverified). Q10 — session digest is an automatic draft filed as a proposal. Q11 — learned aliases stay propose-and-confirm regardless of `evidence_count`.
21
+
22
+ ### Tests
23
+
24
+ - New: `dossier`, `two-hop-dossier`, `usage-boost`, `lint-decoys`, `profile` unit suites; `dossier-mcp`, `usage-boost-ranking`, `profile-mcp` integration; `phase10/session-paging`, `phase11/speaker-profile` hook suites. Regression shape snapshots made citation-independent.
25
+ - Tool counts 42 → 44 (read-only unchanged at 21). Golden eval: zero regression (recall@1=86% recall@3/@5=100% MRR=0.929). **418 tests green; fresh-install smoke PASS.**
26
+
27
+ ## [1.4.0] - 2026-07-10 — Resident-expert bridge (v1.4.0: Phases 1–7)
28
+
29
+ The human→LLM bridge arc: learn how *this human* talks about the system, map that phrasing to concrete artifacts, and pay down the plugin's own token overhead. Tool surface 41 → 42 (`manage_lexicon`). Additive throughout; default ranking behavior is unchanged except for the new archive down-weight (opt-out).
30
+
31
+ ### Added
32
+
33
+ - **Golden retrieval eval harness** (`test/retrieval-eval/`, `npm run eval`). Pure metrics engine (recall@k, MRR, per case-class) + a class-tagged golden set driven through the real `search_semantic` stack. The measurement foundation — every ranking/expansion change now shows a delta. Baseline captured at recall@1=86%, MRR=0.929.
34
+ - **Path-class ranking** (`src/core/path-class.ts`). `archive/**` → 0.3× down-weight (configurable via `path_class` in `vault.schema.yml`), composed multiplicatively with decay + `load_priority` at the same rank site. Directly fixes retired-doc copies dominating results.
35
+ - **Injection hygiene** in `formatContextBlock` (the `<vault-context>` render point): score-gates weak prompts (`SEMANTIC_MEMORY_INJECT_MIN_SCORE`, default 0.35), dedupes archive twins by basename, caps at top-3, and shrinks the instructions from ~150 to ~45 words — cutting per-prompt token overhead at the source while preserving the cite-or-deflect scoping.
36
+ - **Lexicon corpus** — learned human→artifact aliases. `alias` note type under `<vault>/lexicon/`, a derived `lexicon-cache.json`, and **`manage_lexicon`** (`action: add|lookup|list|remove|compile`, one tool). CLI: `semantic-memory lexicon list|compile|add`. Repeat observations bump `evidence_count` + confidence.
37
+ - **Alias capture loop** — propose-and-confirm: a `vault-first` skill instruction offers a `manage_lexicon add` when the LLM resolves the user's vague phrasing to a concrete artifact (never auto-adds). Conflict detection via opt-in `lint_vault({checks:["alias_conflicts"]})` (a phrase mapping to >1 target is surfaced, never auto-resolved).
38
+ - **Verbatim symptom capture + symptom-keyed indexing.** `synthesize_note` gains a `symptoms` param → `symptoms:` frontmatter; the indexer adds each verbatim symptom phrase as its own chunk pointing at the note (synthetic child chunks, no index-format change). A note surfaces on a terse symptom query even when its prose never uses those words — the asymmetric-retrieval fix.
39
+ - **Query expansion (two tiers).** Tier 1 (deterministic, in `vault-context.js`): `expandQueryViaLexicon` appends alias canonical targets to a user utterance before embedding. Tier 2 (LLM-side): a `vault-first` instruction to rewrite a terse utterance with conversation context and re-search once before concluding "not in vault".
40
+ - **Token-frugal tool surface.** `read_note` gains a `section` param (return one heading's section, not the whole file). Conditional tool registration (`tools.conditional` in `vault.schema.yml`, **default OFF** per the risk of mid-session mode switches): when enabled, outage-silence mode registers only the core search/read surface.
41
+
42
+ ### Deferred within the arc (documented)
43
+
44
+ Near-duplicate collapse at index time (injection-layer basename dedup covers the observed case), rolling-topic query expansion (`topic.json` — start dumb, KQ7), and compact-search-output as the default shape. v1.5.0 (dossiers, usage-feedback ranking, session paging, speaker profile) is the next arc.
45
+
46
+ ### Tests
47
+
48
+ - New unit suites: eval-metrics, path-class, injection-hygiene, lexicon, lint-alias-conflicts, query-expansion, section, tools-config. New integration: symptom-keyed retrieval, archive down-weight, section reads, conditional registration. Golden eval wired into `npm run eval`.
49
+ - Tool-surface + output snapshots updated additively (manage_lexicon; alias_conflicts enum/byRule; read_note `section` + synthesize_note `symptoms` params). Tool counts 41 → 42 (read-only unchanged at 21). **371 tests green; fresh-install smoke PASS.**
50
+
51
+ ## [1.3.2] - 2026-07-10 — Fix hook double-registration (double vault-context injection)
52
+
53
+ ### Fixed
54
+
55
+ - **`vault-context.js` no longer fires twice per prompt.** On a plugin install the plugin's own `hooks/hooks.json` registers SessionStart/UserPromptSubmit/Stop — but this repo's committed `.claude/settings.json` *also* registered the same three events (legacy dogfooding scaffolding from the Phase 2 era, before the plugin was marketplace-installable). With the plugin enabled, both sources fired, so `vault-context.js` ran twice and the `<vault-context>` block was injected **twice on every prompt** (~1.5k tokens of duplicated overhead per turn). Removed the redundant `hooks` block from `.claude/settings.json`; the plugin's `hooks/hooks.json` is now the single source of truth.
56
+
57
+ ### Added
58
+
59
+ - **`hook_double_registration` healthcheck finding** (fast tier). Detects the same hook event declared in BOTH `.claude/settings.json` and the plugin's `hooks/hooks.json` and warns that each hook fires twice, pointing at the fix. Only fires in a plugin context (`CLAUDE_PLUGIN_ROOT` present); a silent no-op for npm-dependency installs, which legitimately register via `settings.json`. This is the durable guard so the double-injection can't silently recur on any install — the item flagged as v1.4 Phase 0.
60
+
61
+ ### Tests
62
+
63
+ - New: `test/unit/healthcheck-double-hook.test.ts` (warns on overlap; no-op without `CLAUDE_PLUGIN_ROOT`; no warn on disjoint events). 331 tests green.
64
+
65
+ ## [1.3.1] - 2026-07-10 — Selection-logging telemetry (the v1.4 precursor)
66
+
67
+ Ships the two pieces deferred out of v1.3.0 so retrieval can learn from its own outcomes. v1.3.1 only **observes** — there is no ranking change (usage-feedback ranking is v1.4). Strictly local, append-only, opt-out.
68
+
69
+ ### Added
70
+
71
+ - **Selection logging** (`src/core/telemetry.ts`) → `.claude/.semantic-memory/selection.jsonl` (local, append-only JSONL, gitignored, never leaves the machine). Two event kinds: a `search` event (`{tool, query, results:[{path, score, decay?}]}`) logged by `search_semantic`/`search_hybrid` — reusing the decay multiplier already computed — and a `selection` event (`{note_path, via, correlated}`) logged by `read_note`. `correlated: true` marks a read that followed a search which returned that path (best-effort, 60s in-process window). Awaited so the signal is durable; `appendEvent` never throws and respects the opt-out, so it can never fail the tool. Config: `telemetry.enabled` in `vault.schema.yml` (default true; `false` disables all logging). **No network** — enforced by a unit test that greps the module.
72
+ - **`lint_vault({checks:["decay_candidates"]})`** — new opt-in lint rule. Cross-references the selection log (notes that appeared in recent search results) against each note's current decay multiplier, and flags notes retrieved frequently but decayed to ≤0.5 ("retrieved N× recently but decayed to M — verify_note or revise"), sorted most-retrieved first. Index-free (no embedder); a silent no-op when there's no selection log. Never in the default report, same as `code_symbols`.
73
+ - **`semantic-memory selection-stats --notes <path>`** — CLI rollup of the log: searches, selections, most-cited notes, and retrieved-but-never-cited notes (`--json` for raw).
74
+
75
+ ### Fixed
76
+
77
+ - **`regenerate_contract` no longer hardcodes the contract version** (`src/mcp/tools/contract.ts` had `pluginVersion: "1.1.0"` frozen, so AGENTS.md was stamped 1.1.0 on every regeneration — the same class of freeze bug as the `plugin.json` fix in 1.2.2). Now resolves the real version by walking up from the module's runtime location to the package's own `package.json` (matched by name), robust to tsup bundling/splitting. (Landed on main ahead of this release; AGENTS.md was regenerated to 1.3.0.)
78
+
79
+ ### Deferred (still, to v1.4+)
80
+
81
+ - Backlink hotness (wired but flag-off) and the **usage-feedback ranking** that consumes this log — the latter is v1.4 Phase 9. v1.3.1 deliberately produces the signal without acting on it, so the log can accumulate real data before ranking tunes against it.
82
+
83
+ ### Tests
84
+
85
+ - New: `test/unit/telemetry.test.ts` (append/read, opt-out, fire-and-forget-never-throws, correlation window, stats rollup, no-network grep); `test/unit/lint-decay-candidates.test.ts` (flags retrieved-but-decayed, spares fresh, opt-in gating, no-log no-op).
86
+ - Extended: `test/integration/mcp-server.test.ts` (search→read lands both events; the selection is `correlated`).
87
+ - Updated: tool-surface + tool-output regression snapshots (additive `decay_candidates` enum value + byRule key). 328 tests green.
88
+
89
+ ## [1.3.0] - 2026-07-10 — Confidence decay: age-aware retrieval ranking
90
+
91
+ Replaces age-blind ranking with smooth, type-aware confidence decay. Notes age out of relevance gracefully; explicitly-verified notes reset the clock; agents see decay state and can re-verify. Opt-out via one config line.
92
+
93
+ ### Added
94
+
95
+ - **Decay engine (`src/core/decay.ts`).** Pure, side-effect-free: `multiplier = 0.5^(age_days / half_life)`, keyed on time since `last_verified`. Per-type half-life (`decision`/`note` 365d, `gotcha` 180d, `source` never, `proposal` 14d), floored at 0.1 (notes down-weight, never disappear). Fails open (multiplier 1.0) on any uncertainty — disabled config, missing/invalid/**future** `last_verified` — so decay can only down-rank a note we're confident is old.
96
+ - **Decay applied at search time.** `search_semantic` and `search_hybrid` multiply decay into the score after the existing `load_priority` boost (composes, never replaces). `search_text` (exact-match intent) and `search_graph` (graph distance is the signal) are deliberately unaffected.
97
+ - **`decay` block surfaced in results.** Decayed results (multiplier < 1) carry `{ multiplier, age_days, effective_half_life, reason }`; fresh results stay clean.
98
+ - **`verify_note` MCP tool** (tool count 40 → 41). Stamps `last_verified` to today WITHOUT touching content or any other field, logs a `verify` event, and returns the note's new decay multiplier. The explicit clock-reset.
99
+ - **`evergreen` frontmatter.** `evergreen: true` pins a note at multiplier 1.0 while its `last_verified` is within 365 days; past that it decays normally (the evergreen claim itself expires, forcing periodic re-affirmation).
100
+ - **CLI introspection.** `semantic-memory decay-config --notes <path>` prints the active config (defaults vs. `vault.schema.yml` override); `semantic-memory decay-trace <note> --notes <path>` prints the full calculation for one note.
101
+ - **Config in `vault.schema.yml`.** A documented `decay:` block ships in the default schema. `decay.enabled: false` restores byte-identical pre-v1.3 ranking.
102
+
103
+ ### Fixed
104
+
105
+ - **Unquoted-date frontmatter no longer silently disables decay.** YAML parses `last_verified: 2019-01-01` (unquoted) into a `Date`, not a string, so a naive `typeof === "string"` check dropped it and every such note read as "no last_verified (fail open)". New `normalizeVerifiedDate()` coerces string **or** Date at all read sites (search, `verify_note`, `decay-trace`). Caught by the `decay-trace` smoke test during development.
106
+
107
+ ### Semantics note: `last_verified` vs. edits
108
+
109
+ Decay keys on `last_verified`, which is stamped only at note creation (`synthesize_note`, `ingest_source`) and by `verify_note` — **no edit path bumps it**. So ordinary edits do not reset the decay clock, which is the intended v1.3 correctness property. The separate `last_modified` frontmatter field from the original plan (Phase 2) is therefore not required for correct decay and is deferred; it can be added later for edit-recency use cases without affecting decay.
110
+
111
+ ### Deliberately deferred (documented, not dropped)
112
+
113
+ - **Backlink hotness boost (Phase 7)** — the engine supports it (`hotness_boost` config), but it ships **flag-off by default**; hub notes (index.md) distort the signal, so it must be earned via telemetry before defaulting on.
114
+ - **`decay_candidates` lint (Phase 8)** and **selection-logging telemetry (Phase 10)** — both need query-log history / weeks of passive collection to be meaningful, and telemetry is the dependency for the v1.4 usage-feedback ranking. Staged as a v1.3.x follow-up.
115
+
116
+ ### Tests
117
+
118
+ - New: `test/unit/decay.test.ts` (12 cases — half-life curve, floor, per-type, never-decay, fail-open, evergreen + expiry, hotness, Date-normalization regression).
119
+ - Extended: `test/integration/mcp-server.test.ts` (verify_note round-trip; `search_semantic` attaches a `decay` block to an old-`last_verified` note).
120
+ - Updated: tool-surface regression snapshots (additive `verify_note` tool + decay-aware search descriptions); tool-count assertions 40 → 41 (read-only mode unchanged at 21 — `verify_note` is write-gated).
121
+
122
+ ## [1.2.3] - 2026-07-10 — Complete the v1.2 hygiene line: `--fix`, code-symbol drift, CI smoke
123
+
124
+ Ships the three v1.2 roadmap items that slipped when 1.2.1/1.2.2 were preempted by defect fixes. All three are additive; no tool is removed and default behavior is unchanged unless a flag is passed.
125
+
126
+ ### Added
127
+
128
+ - **`healthcheck --fix` — auto-remediation of safe drift.** `/healthcheck --fix` (and the `semantic-memory healthcheck --fix` CLI) now applies **safe, idempotent, non-destructive** fixes for fixable findings and re-runs drift detection to show the post-fix state. Actions: re-link skills (`skill-link`), reconcile `.mcp.json` (`mcp-reconcile`), reindex the vault (`reindex`), and migrate legacy state (`state-migrate`). Findings that touch user-authored content — stale notes, broken wikilinks, hand-edited `AGENTS.md` — are reported for human review, never auto-changed. The decision logic lives in a pure, unit-tested planner (`src/core/healthcheck-fix.ts`); the CLI executes the plan. The `legacy_state_files` finding's `fixable_via` changed from `none` to `state-migrate`.
129
+ - **`lint_vault({checks: ["code_symbols"]})` — code-path drift detection.** New opt-in lint rule: scans note inline-code spans for repo-relative file-path references and flags ones whose first segment IS a real directory in the repo but whose full path no longer exists (a stale reference to a moved/deleted file). Anchoring on an existing first segment keeps false positives low — paths belonging to other repos are skipped. Fails open (silent no-op) outside a code repo. **Opt-in only:** never part of the default `lint_vault` report or the healthcheck slow tier, so existing behavior is byte-stable. Scope note: this validates *path* references; fine-grained symbol-name checking needs a real symbol index and is deferred to the v1.4 lexicon arc, which will extend this same `code_symbols` rule.
130
+ - **Fresh-install CI smoke test.** New `scripts/smoke-install.sh` + `.github/workflows/ci.yml` `smoke` job: `npm pack` → install the tarball into a throwaway consumer project → assert (1) `bin --version` reports the current version, (2) the CLI loads all subcommands, (3) every runtime file is actually in the tarball, and (4) the SessionStart `reconcile` wiring populates `.mcp.json` on a fresh install. This mechanically prevents the v1.1.0-class packaging bug (the highest-ROI item on the v1.2 roadmap).
131
+
132
+ ### Fixed
133
+
134
+ - **Drift detection now runs on healthy installs.** The `healthcheck` CLI command called `process.exit(0)` immediately after the install smoke-test succeeded, so the drift-detection `postAction` hook (added in v1.1) never ran on a clean install — it only surfaced when the smoke-test failed. The command is now a single coherent action: smoke-test → drift → optional `--fix` → emit, exiting once at the end. The `formatDriftBanner` line advertising `/healthcheck --fix` is no longer a promise for a flag that doesn't exist.
135
+
136
+ ### Tests
137
+
138
+ - New: `test/unit/healthcheck-fix.test.ts` (planner: safe-action mapping, human-review routing, ok-finding filtering).
139
+ - New: `test/unit/lint-code-symbols.test.ts` (drift on a stale path, no-flag on an existing path, other-repo skip, URL/glob rejection, opt-in gating, fail-open outside a code repo).
140
+ - Updated: tool-surface regression snapshots (additive `code_symbols` enum value + description on `lint_vault`).
141
+
142
+ ## [1.2.2] - 2026-06-27 — Plugin-aware hook-registration check + manifest version sync
143
+
144
+ ### Fixed
145
+
146
+ - **`hook_registration` drift no longer false-positives on plugin-style installs.** The check (both the SessionStart fast path in `hooks/vault-context.js` and the CLI `checkHookRegistration` in `src/core/healthcheck.ts`) only inspected the project's `.claude/settings.json`. When semantic-memory is installed as a Claude Code plugin, its SessionStart/UserPromptSubmit/Stop hooks live in the plugin's own `hooks/hooks.json` (resolved via `CLAUDE_PLUGIN_ROOT`) and fire correctly, so the check warned about "missing" hooks that were in fact registered. Both sites now treat events declared in the plugin's `hooks/hooks.json` as registered. Npm-dependency installs (no `CLAUDE_PLUGIN_ROOT`) are unaffected and still require the hooks in `settings.json`.
147
+ - **Plugin manifest version sync.** `.claude-plugin/plugin.json` was frozen at `1.0.1` while `package.json`, git tags, and `marketplace.json` advanced — Claude Code reads the plugin version from `plugin.json`, so `claude plugin update` always reported "already at latest (1.0.1)" and never pulled newer code. Bumped `plugin.json` to match, and corrected its stale `homepage`/`repository` URLs (they pointed at the old `semantic-sidekick` repo).
148
+
5
149
  ## [1.2.1] - 2026-06-11 — Capture-cue false-positive fixes
6
150
 
7
151
  Fixes two classes of false positives in the Stop-hook capture nudge (`hooks/vault-context.js`), where the cue scanner over-fired and asked to synthesize moments that carried no new knowledge.
package/ROADMAP.md CHANGED
@@ -2,8 +2,11 @@
2
2
 
3
3
  This file tracks the committed near-term direction for `@theglitchking/semantic-memory`. It's a planning surface — not a contract. Items move between sections as the work clarifies; the canonical state lives here in git.
4
4
 
5
- > **Current shipped version:** [v1.1.1](https://github.com/TheGlitchKing/semantic-sidekick/releases/tag/v1.1.1) (npm `latest`)
6
- > **Last updated:** 2026-05-09
5
+ > **Current shipped version:** v1.2.3 on `main` (npm `latest`); v1.3.0 (confidence-decay core) in review on `feat/v1.3-confidence-decay`.
6
+ > **Last updated:** 2026-07-10
7
+ >
8
+ > **v1.2 status:** COMPLETE — state consolidation (1.2.0), `/healthcheck --fix` + `code_symbols` lint + fresh-install CI smoke (1.2.3). The three items that slipped past 1.2.0/1.2.1/1.2.2 landed in 1.2.3.
9
+ > **v1.3 status:** core SHIPPED (decay engine, search integration, `decay` surfacing, `verify_note`, `evergreen`, `decay-config`/`decay-trace`). Deferred within the v1.3 line: backlink hotness (flag-off), `decay_candidates` lint, and selection-logging telemetry (dependency for the v1.4 usage-feedback ranking). The `last_modified` split proved unnecessary for decay correctness — `last_verified` is already never bumped on edits.
7
10
 
8
11
  ## Legend
9
12
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: Verify the local install + run drift detection. Self-heals npx-cache ERR_MODULE_NOT_FOUND. Pass --fast to skip the slow tier (full vault lint).
2
+ description: Verify the local install + run drift detection. Self-heals npx-cache ERR_MODULE_NOT_FOUND. Pass --fast to skip the slow tier, --fix to auto-apply safe fixes.
3
3
  allowed-tools: Bash(npx:*), Bash(node:*)
4
4
  ---
5
5
 
@@ -7,18 +7,20 @@ When the user runs `/healthcheck`:
7
7
 
8
8
  - `/healthcheck` (no arg) — Run the local install verification AND the full drift check (fast tier + slow tier). Report the results: install status, drift findings (if any), and remediation pointers (e.g. `/relink`, `/normalize-config`, `regenerate_contract`).
9
9
  - `/healthcheck --fast` — Run the install verification + ONLY the fast-tier drift checks. Skips the full vault lint (slow tier). Use when the agent only wants quick install/manifest checks without the multi-second vault scan.
10
+ - `/healthcheck --fix` — Run drift detection, then auto-apply **safe** fixes for fixable findings and report what was done. Combinable with `--fast`.
10
11
 
11
12
  The fast-tier auto-check also runs on every `SessionStart` via `vault-context.js` — healthy installs see nothing at session start, drifted installs see a one-block warning with a pointer to this command.
12
13
 
13
- Implementation: shell out to `node ./node_modules/@theglitchking/semantic-memory/bin/semantic-memory healthcheck [--fast]` (or `npx --no @theglitchking/semantic-memory healthcheck [--fast]` if no local install). Falls back to the legacy `@theglitchking/semantic-sidekick` package on machines mid-migration.
14
+ Implementation: shell out to `node ./node_modules/@theglitchking/semantic-memory/bin/semantic-memory healthcheck [--fast] [--fix]` (or `npx --no @theglitchking/semantic-memory healthcheck [...]` if no local install). Falls back to the legacy `@theglitchking/semantic-sidekick` package on machines mid-migration.
14
15
 
15
- Drift findings are categorized by check:
16
- - `mcp_json_entry` — `.mcp.json` missing the semantic-memory server entry. Fix: `/normalize-config`.
17
- - `hook_registration` — `.claude/settings.json` missing one of SessionStart/UserPromptSubmit/Stop. Fix: `/relink` or reinstall.
18
- - `agents_contract` — `AGENTS.md` exists but lacks managed-block markers. Fix: move custom content to a `Local Notes` section, or delete and re-run `regenerate_contract`.
19
- - `session_staleness` — A session has been open >24h with no activity. Fix: resume (run `session_run` / `session_finish`) or remove the stale session file.
20
- - `index_freshness` — No vector index built yet. Fix: call `reindex`.
21
- - `skill_manifest:*` — Per-agent skill install state. Informational unless missing.
22
- - `lint_vault` (slow tier only) — Vault lint findings. Many require human review (stale notes, broken wikilinks).
16
+ Drift findings are categorized by check (the `Fix?` column shows what `--fix` does automatically):
17
+ - `mcp_json_entry` — `.mcp.json` missing the semantic-memory server entry. **`--fix`: reconciles `.mcp.json`** (also `/normalize-config`).
18
+ - `hook_registration` — `.claude/settings.json` missing one of SessionStart/UserPromptSubmit/Stop. **`--fix`: re-links skills/hooks** (also `/relink`).
19
+ - `index_freshness` — No vector index built yet. **`--fix`: reindexes the vault.**
20
+ - `legacy_state_files` — Legacy `.claude/.sidekick-*` state files present. **`--fix`: runs `migrate-state`.**
21
+ - `skill_manifest:*` — Per-agent skill install state. **`--fix`: re-links skills** when flagged.
22
+ - `agents_contract` — `AGENTS.md` exists but lacks managed-block markers. **Not auto-fixed** (needs a human decision — move custom content to a `Local Notes` section, or delete and re-run `regenerate_contract`).
23
+ - `session_staleness` — A session has been open >24h with no activity. **Not auto-fixed** (resume via `session_run` / `session_finish`, or remove the stale session file).
24
+ - `lint_vault` (slow tier only) — Vault lint findings. **Not auto-fixed** — stale notes, broken wikilinks, and schema violations require human review.
23
25
 
24
- `--fix` flag is reserved for v1.2 — auto-applying safe fixes (re-link skills, reconcile `.mcp.json`, regenerate contract) is the planned v1.2 work. For v1.1, drift detection is read-only and points the user at the right manual command.
26
+ `--fix` is intentionally conservative: it only applies idempotent, non-destructive fixes derivable from the install itself. Anything touching user-authored content is reported for human review, never auto-changed. After applying fixes it re-runs drift detection so the printed findings reflect the post-fix state.
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  lintVault
3
- } from "./chunk-CAN2GWUG.js";
3
+ } from "./chunk-FM26NFTR.js";
4
4
 
5
5
  // src/core/healthcheck.ts
6
6
  import { readFile, writeFile, mkdir } from "fs/promises";
7
- import { existsSync } from "fs";
7
+ import { existsSync, readFileSync } from "fs";
8
8
  import { join } from "path";
9
9
  import { homedir } from "os";
10
10
  var CACHE_TTL_MS = 5 * 60 * 1e3;
@@ -52,6 +52,7 @@ async function runFastTier(opts) {
52
52
  checkPluginVersion(opts.projectRoot),
53
53
  checkMcpJsonEntry(opts.projectRoot),
54
54
  checkHookRegistration(opts.projectRoot),
55
+ checkHookDoubleRegistration(opts.projectRoot),
55
56
  checkIndexFreshness(opts.projectRoot, opts.vaultPath),
56
57
  checkSessionStaleness(opts.projectRoot),
57
58
  checkAgentsContract(opts.projectRoot),
@@ -123,6 +124,19 @@ async function checkMcpJsonEntry(projectRoot) {
123
124
  }
124
125
  return { check: "mcp_json_entry", severity: "ok", summary: ".mcp.json server entry present" };
125
126
  }
127
+ function pluginRegisteredHookEvents() {
128
+ try {
129
+ const root = process.env.CLAUDE_PLUGIN_ROOT;
130
+ if (!root) return [];
131
+ const hooksPath = join(root, "hooks", "hooks.json");
132
+ if (!existsSync(hooksPath)) return [];
133
+ const data = JSON.parse(readFileSync(hooksPath, "utf-8"));
134
+ const hooks = data?.hooks ?? {};
135
+ return Object.keys(hooks).filter((e) => Array.isArray(hooks[e]) && hooks[e].length > 0);
136
+ } catch {
137
+ return [];
138
+ }
139
+ }
126
140
  async function checkHookRegistration(projectRoot) {
127
141
  const settings = join(projectRoot, ".claude", "settings.json");
128
142
  if (!existsSync(settings)) return null;
@@ -130,10 +144,12 @@ async function checkHookRegistration(projectRoot) {
130
144
  const data = JSON.parse(await readFile(settings, "utf-8"));
131
145
  const hooks = data?.hooks ?? {};
132
146
  const required = ["SessionStart", "UserPromptSubmit", "Stop"];
147
+ const pluginEvents = pluginRegisteredHookEvents();
133
148
  const missing = [];
134
149
  for (const event of required) {
135
150
  const arr = hooks[event];
136
- if (!Array.isArray(arr) || arr.length === 0) missing.push(event);
151
+ const inSettings = Array.isArray(arr) && arr.length > 0;
152
+ if (!inSettings && !pluginEvents.includes(event)) missing.push(event);
137
153
  }
138
154
  if (missing.length > 0) {
139
155
  return {
@@ -148,6 +164,34 @@ async function checkHookRegistration(projectRoot) {
148
164
  return failOpenWarn(err);
149
165
  }
150
166
  }
167
+ async function checkHookDoubleRegistration(projectRoot) {
168
+ const settings = join(projectRoot, ".claude", "settings.json");
169
+ if (!existsSync(settings)) return null;
170
+ const pluginEvents = pluginRegisteredHookEvents();
171
+ if (pluginEvents.length === 0) return null;
172
+ try {
173
+ const data = JSON.parse(await readFile(settings, "utf-8"));
174
+ const hooks = data?.hooks ?? {};
175
+ const doubled = [];
176
+ for (const event of ["SessionStart", "UserPromptSubmit", "Stop"]) {
177
+ const arr = hooks[event];
178
+ const inSettings = Array.isArray(arr) && arr.length > 0;
179
+ if (inSettings && pluginEvents.includes(event)) doubled.push(event);
180
+ }
181
+ if (doubled.length > 0) {
182
+ return {
183
+ check: "hook_double_registration",
184
+ severity: "warn",
185
+ summary: `hook event(s) registered in BOTH settings.json and the plugin \u2014 fires twice: ${doubled.join(", ")}`,
186
+ detail: `The plugin's hooks/hooks.json already registers ${doubled.join(", ")}, so the duplicate registration in .claude/settings.json makes each hook run twice (e.g. the <vault-context> block injected twice per prompt). Remove the "hooks" block from .claude/settings.json \u2014 the plugin's hooks.json is the single source of truth for plugin installs.`,
187
+ fixable_via: "none"
188
+ };
189
+ }
190
+ return { check: "hook_double_registration", severity: "ok", summary: "no double-registered hooks" };
191
+ } catch (err) {
192
+ return failOpenWarn(err);
193
+ }
194
+ }
151
195
  async function checkIndexFreshness(projectRoot, vaultPath) {
152
196
  const vault = vaultPath ?? defaultVaultPath(projectRoot);
153
197
  if (!vault || !existsSync(vault)) return null;
@@ -213,7 +257,7 @@ async function checkLegacyStateFiles(projectRoot) {
213
257
  severity: "warn",
214
258
  summary: `${present.length} legacy state file${present.length === 1 ? "" : "s"} at .claude/.sidekick-*`,
215
259
  detail: `Files: ${present.join(", ")}. Run \`bin/semantic-memory migrate-state\` to move them under .claude/.semantic-memory/. These files are still being read for backwards-compat through v1.x; the legacy fallback is removed in v2.0.`,
216
- fixable_via: "none"
260
+ fixable_via: "state-migrate"
217
261
  };
218
262
  }
219
263
  async function checkAgentsContract(projectRoot) {
@@ -294,4 +338,4 @@ export {
294
338
  filterToDrift,
295
339
  formatDriftBanner
296
340
  };
297
- //# sourceMappingURL=chunk-EVAJGOCB.js.map
341
+ //# sourceMappingURL=chunk-2EELGTLX.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/healthcheck.ts"],"sourcesContent":["import { readFile, writeFile, mkdir, stat } from \"node:fs/promises\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { join, basename } from \"node:path\";\nimport { createHash } from \"node:crypto\";\nimport { homedir } from \"node:os\";\nimport { lintVault } from \"./lint.js\";\nimport { deriveProjectRoot } from \"./session.js\";\n\nexport type Tier = \"fast\" | \"slow\" | \"all\";\nexport type Severity = \"ok\" | \"warn\" | \"error\";\n\nexport interface DriftFinding {\n check: string;\n severity: Severity;\n summary: string;\n detail?: string;\n fixable_via?: \"skill-link\" | \"mcp-reconcile\" | \"regenerate-contract\" | \"reindex\" | \"state-migrate\" | \"none\";\n}\n\nexport interface HealthcheckResult {\n tier_run: Tier;\n ran_at: string;\n duration_ms: number;\n findings: DriftFinding[];\n counts: { ok: number; warn: number; error: number };\n cached?: boolean;\n}\n\nexport interface HealthcheckOptions {\n projectRoot: string;\n /** Vault path. When omitted, derived from projectRoot's .claude/.vault. */\n vaultPath?: string;\n tier?: Tier;\n /** Cache file path. Default: <projectRoot>/.claude/.semantic-memory/healthcheck-cache.json */\n cachePath?: string;\n /** Force re-run even if cache is <5 minutes old. */\n force?: boolean;\n /** Override \"now\" — used by tests. */\n nowMs?: number;\n}\n\nconst CACHE_TTL_MS = 5 * 60 * 1000;\n\n/**\n * Run the drift-detection healthcheck.\n *\n * - Fast tier: cheap file-system probes (~ms each). Plugin version, skill manifests,\n * .mcp.json registration, hook registration, index freshness, session staleness,\n * AGENTS.md managed-block sha. All independent, run in parallel. Total budget: <100ms\n * on a representative install.\n * - Slow tier: full vault lint. Adds tens of seconds on large vaults — invoked by\n * explicit /healthcheck, never by the SessionStart auto path.\n * - Cache: when force=false and the cache file is <5min old, return the cached result.\n *\n * Fail-open: any individual check that throws produces a `severity: \"warn\"` finding\n * with the error message; never propagates.\n */\nexport async function runHealthcheck(opts: HealthcheckOptions): Promise<HealthcheckResult> {\n const start = (opts.nowMs ?? Date.now());\n const tier: Tier = opts.tier ?? \"fast\";\n const cachePath = opts.cachePath ?? join(opts.projectRoot, \".claude\", \".semantic-memory\", \"healthcheck-cache.json\");\n\n if (!opts.force && existsSync(cachePath)) {\n try {\n const raw = await readFile(cachePath, \"utf-8\");\n const cached = JSON.parse(raw) as HealthcheckResult & { ran_at_ms: number };\n if (cached.tier_run === tier && start - cached.ran_at_ms < CACHE_TTL_MS) {\n return { ...cached, cached: true };\n }\n } catch {\n // ignore corrupt cache; will overwrite below\n }\n }\n\n const findings = tier === \"fast\" ? await runFastTier(opts) : tier === \"slow\" ? await runSlowTier(opts) : [...(await runFastTier(opts)), ...(await runSlowTier(opts))];\n\n const counts = countBySeverity(findings);\n const ranAt = new Date(start).toISOString();\n const result: HealthcheckResult = {\n tier_run: tier,\n ran_at: ranAt,\n duration_ms: Date.now() - start,\n findings,\n counts,\n };\n\n // Persist cache (best-effort; failure is non-fatal).\n try {\n await mkdir(join(opts.projectRoot, \".claude\", \".semantic-memory\"), { recursive: true });\n await writeFile(cachePath, JSON.stringify({ ...result, ran_at_ms: start }, null, 2), \"utf-8\");\n } catch {\n // never let cache write block the result\n }\n\n return result;\n}\n\n/**\n * Surface filter — return findings with severity >= warn. Used by the SessionStart hook\n * to decide whether to emit anything at all.\n */\nexport function filterToDrift(result: HealthcheckResult): DriftFinding[] {\n return result.findings.filter((f) => f.severity !== \"ok\");\n}\n\nfunction countBySeverity(findings: DriftFinding[]): { ok: number; warn: number; error: number } {\n const counts = { ok: 0, warn: 0, error: 0 };\n for (const f of findings) counts[f.severity]++;\n return counts;\n}\n\nasync function runFastTier(opts: HealthcheckOptions): Promise<DriftFinding[]> {\n const checks: Promise<DriftFinding | null>[] = [\n checkPluginVersion(opts.projectRoot),\n checkMcpJsonEntry(opts.projectRoot),\n checkHookRegistration(opts.projectRoot),\n checkHookDoubleRegistration(opts.projectRoot),\n checkIndexFreshness(opts.projectRoot, opts.vaultPath),\n checkSessionStaleness(opts.projectRoot),\n checkAgentsContract(opts.projectRoot),\n checkLegacyStateFiles(opts.projectRoot),\n ...(await skillManifestChecks(opts.projectRoot)),\n ];\n const results = await Promise.all(checks.map((p) => p.catch((err) => failOpenWarn(err))));\n return results.filter((r): r is DriftFinding => r !== null);\n}\n\nasync function runSlowTier(opts: HealthcheckOptions): Promise<DriftFinding[]> {\n const findings: DriftFinding[] = [];\n if (opts.vaultPath && existsSync(opts.vaultPath)) {\n try {\n const report = await lintVault(opts.vaultPath);\n const total = report.findings.length;\n if (total === 0) {\n findings.push({ check: \"lint_vault\", severity: \"ok\", summary: `vault lint clean (${report.counts.filesLinted} files)` });\n } else {\n findings.push({\n check: \"lint_vault\",\n severity: report.counts.errors > 0 ? \"error\" : \"warn\",\n summary: `vault lint: ${total} findings (errors=${report.counts.errors}, warnings=${report.counts.warnings})`,\n detail: `Run \\`lint_vault\\` for the full report. Many lint findings need human review (stale notes, broken wikilinks).`,\n });\n }\n } catch (err: any) {\n findings.push({ check: \"lint_vault\", severity: \"warn\", summary: `lint_vault threw: ${err?.message ?? err}` });\n }\n }\n return findings;\n}\n\nfunction failOpenWarn(err: unknown): DriftFinding {\n return {\n check: \"internal\",\n severity: \"warn\",\n summary: `healthcheck internal error: ${err instanceof Error ? err.message : String(err)}`,\n };\n}\n\n// --- individual checks ---\n\nasync function checkPluginVersion(projectRoot: string): Promise<DriftFinding | null> {\n const pkgPaths = [\n join(projectRoot, \"node_modules\", \"@theglitchking\", \"semantic-memory\", \"package.json\"),\n join(projectRoot, \"node_modules\", \"@theglitchking\", \"semantic-sidekick\", \"package.json\"),\n ];\n for (const p of pkgPaths) {\n if (!existsSync(p)) continue;\n try {\n const installed = JSON.parse(await readFile(p, \"utf-8\"));\n if (typeof installed?.version === \"string\") {\n return { check: \"plugin_version\", severity: \"ok\", summary: `installed=${installed.version}` };\n }\n } catch {\n // continue to next candidate\n }\n }\n return null; // no installation present — not a drift event for non-npm dev environments\n}\n\nasync function checkMcpJsonEntry(projectRoot: string): Promise<DriftFinding | null> {\n const mcpPath = join(projectRoot, \".mcp.json\");\n if (!existsSync(mcpPath)) return null; // no .mcp.json — fine for dev\n const data = JSON.parse(await readFile(mcpPath, \"utf-8\"));\n const servers = data?.mcpServers ?? {};\n const hasEntry = Object.keys(servers).some((k) => k === \"semantic-vault\" || k === \"semantic-sidekick\" || k === \"semantic-memory\");\n if (!hasEntry) {\n return {\n check: \"mcp_json_entry\",\n severity: \"warn\",\n summary: \".mcp.json present but no semantic-memory/sidekick/vault server entry\",\n fixable_via: \"mcp-reconcile\",\n };\n }\n return { check: \"mcp_json_entry\", severity: \"ok\", summary: \".mcp.json server entry present\" };\n}\n\n// Events the plugin registers through its own hooks/hooks.json (resolved via\n// CLAUDE_PLUGIN_ROOT). On a plugin-style install these live in the plugin, not\n// the project's .claude/settings.json, so they must count as registered.\nfunction pluginRegisteredHookEvents(): string[] {\n try {\n const root = process.env.CLAUDE_PLUGIN_ROOT;\n if (!root) return [];\n const hooksPath = join(root, \"hooks\", \"hooks.json\");\n if (!existsSync(hooksPath)) return [];\n const data = JSON.parse(readFileSync(hooksPath, \"utf-8\"));\n const hooks = data?.hooks ?? {};\n return Object.keys(hooks).filter((e) => Array.isArray(hooks[e]) && hooks[e].length > 0);\n } catch {\n return [];\n }\n}\n\nasync function checkHookRegistration(projectRoot: string): Promise<DriftFinding | null> {\n const settings = join(projectRoot, \".claude\", \"settings.json\");\n if (!existsSync(settings)) return null;\n try {\n const data = JSON.parse(await readFile(settings, \"utf-8\"));\n const hooks = data?.hooks ?? {};\n const required = [\"SessionStart\", \"UserPromptSubmit\", \"Stop\"];\n const pluginEvents = pluginRegisteredHookEvents();\n const missing: string[] = [];\n for (const event of required) {\n const arr = hooks[event];\n const inSettings = Array.isArray(arr) && arr.length > 0;\n if (!inSettings && !pluginEvents.includes(event)) missing.push(event);\n }\n if (missing.length > 0) {\n return {\n check: \"hook_registration\",\n severity: \"warn\",\n summary: `missing hook registrations: ${missing.join(\", \")}`,\n detail: `Run \\`/relink\\` or reinstall the plugin to refresh hook registrations.`,\n };\n }\n return { check: \"hook_registration\", severity: \"ok\", summary: \"all 3 hook events registered\" };\n } catch (err: any) {\n return failOpenWarn(err);\n }\n}\n\n/**\n * Detect the double-registration that fires every hook twice: the SAME event\n * declared in BOTH the project's .claude/settings.json AND the plugin's own\n * hooks/hooks.json. On a plugin install the plugin already registers its hooks,\n * so a duplicate in settings.json makes vault-context.js (and friends) run twice —\n * e.g. the <vault-context> block injected twice per prompt.\n *\n * Only fires in a plugin context (CLAUDE_PLUGIN_ROOT present). npm-dependency\n * installs legitimately register via settings.json and have no plugin hooks.json,\n * so `pluginRegisteredHookEvents()` is empty and this is a silent no-op for them.\n */\nasync function checkHookDoubleRegistration(projectRoot: string): Promise<DriftFinding | null> {\n const settings = join(projectRoot, \".claude\", \"settings.json\");\n if (!existsSync(settings)) return null;\n const pluginEvents = pluginRegisteredHookEvents();\n if (pluginEvents.length === 0) return null; // not a plugin install / no plugin hooks to collide with\n try {\n const data = JSON.parse(await readFile(settings, \"utf-8\"));\n const hooks = data?.hooks ?? {};\n const doubled: string[] = [];\n for (const event of [\"SessionStart\", \"UserPromptSubmit\", \"Stop\"]) {\n const arr = hooks[event];\n const inSettings = Array.isArray(arr) && arr.length > 0;\n if (inSettings && pluginEvents.includes(event)) doubled.push(event);\n }\n if (doubled.length > 0) {\n return {\n check: \"hook_double_registration\",\n severity: \"warn\",\n summary: `hook event(s) registered in BOTH settings.json and the plugin — fires twice: ${doubled.join(\", \")}`,\n detail:\n `The plugin's hooks/hooks.json already registers ${doubled.join(\", \")}, so the duplicate ` +\n `registration in .claude/settings.json makes each hook run twice (e.g. the <vault-context> ` +\n `block injected twice per prompt). Remove the \"hooks\" block from .claude/settings.json — the ` +\n `plugin's hooks.json is the single source of truth for plugin installs.`,\n fixable_via: \"none\",\n };\n }\n return { check: \"hook_double_registration\", severity: \"ok\", summary: \"no double-registered hooks\" };\n } catch (err: any) {\n return failOpenWarn(err);\n }\n}\n\nasync function checkIndexFreshness(projectRoot: string, vaultPath?: string): Promise<DriftFinding | null> {\n const vault = vaultPath ?? defaultVaultPath(projectRoot);\n if (!vault || !existsSync(vault)) return null;\n const indexDir = join(vault, \".semantic-sidekick-index\");\n const meta = join(indexDir, \"meta.json\");\n if (!existsSync(meta)) {\n return {\n check: \"index_freshness\",\n severity: \"warn\",\n summary: \"no index built yet\",\n detail: \"Run `reindex` to build the vector index.\",\n fixable_via: \"reindex\",\n };\n }\n try {\n const data = JSON.parse(await readFile(meta, \"utf-8\"));\n if (typeof data.indexedAt === \"string\") {\n return { check: \"index_freshness\", severity: \"ok\", summary: `last reindex ${data.indexedAt}` };\n }\n } catch {\n // ignore\n }\n return null;\n}\n\nasync function checkSessionStaleness(projectRoot: string): Promise<DriftFinding | null> {\n const sessionPath = join(projectRoot, \".claude\", \".semantic-memory\", \"session.json\");\n if (!existsSync(sessionPath)) return null;\n try {\n const state = JSON.parse(await readFile(sessionPath, \"utf-8\"));\n if (state?.closed_at) return null;\n const last = state?.last_activity_at;\n if (typeof last !== \"string\") return null;\n const ageMs = Date.now() - new Date(last).getTime();\n if (ageMs > 24 * 60 * 60 * 1000) {\n return {\n check: \"session_staleness\",\n severity: \"warn\",\n summary: `session ${state.id} stale (>${Math.round(ageMs / (60 * 60 * 1000))}h since activity)`,\n detail: `Either resume the session (run session_run / session_finish) or remove .claude/.semantic-memory/session.json to abandon it.`,\n };\n }\n return { check: \"session_staleness\", severity: \"ok\", summary: `session ${state.id} active` };\n } catch {\n return null;\n }\n}\n\nasync function checkLegacyStateFiles(projectRoot: string): Promise<DriftFinding | null> {\n // Detect v1.1-and-earlier state files at .claude/.sidekick-*. v1.2 moved these to\n // .claude/.semantic-memory/. The hook still reads them via fallback for backwards-\n // compat, but they should be migrated explicitly so the legacy paths stop existing.\n const legacy = [\n \".sidekick-mode\",\n \".sidekick-fingerprints.json\",\n \".sidekick-capture-pending.json\",\n ];\n const present: string[] = [];\n for (const name of legacy) {\n if (existsSync(join(projectRoot, \".claude\", name))) {\n present.push(name);\n }\n }\n if (present.length === 0) {\n return { check: \"legacy_state_files\", severity: \"ok\", summary: \"no legacy state files\" };\n }\n return {\n check: \"legacy_state_files\",\n severity: \"warn\",\n summary: `${present.length} legacy state file${present.length === 1 ? \"\" : \"s\"} at .claude/.sidekick-*`,\n detail:\n `Files: ${present.join(\", \")}. Run \\`bin/semantic-memory migrate-state\\` to move them under .claude/.semantic-memory/. ` +\n `These files are still being read for backwards-compat through v1.x; the legacy fallback is removed in v2.0.`,\n fixable_via: \"state-migrate\",\n };\n}\n\nasync function checkAgentsContract(projectRoot: string): Promise<DriftFinding | null> {\n const path = join(projectRoot, \"AGENTS.md\");\n if (!existsSync(path)) return null; // optional artifact\n try {\n const raw = await readFile(path, \"utf-8\");\n const begin = raw.indexOf(\"<!-- semantic-memory:begin contract -->\");\n const end = raw.indexOf(\"<!-- semantic-memory:end contract -->\");\n if (begin === -1 || end === -1) {\n return {\n check: \"agents_contract\",\n severity: \"warn\",\n summary: \"AGENTS.md exists but lacks managed-block markers\",\n detail: \"Either move custom content to a Local Notes section, or delete AGENTS.md and run `regenerate_contract` to bootstrap.\",\n fixable_via: \"regenerate-contract\",\n };\n }\n return { check: \"agents_contract\", severity: \"ok\", summary: \"AGENTS.md managed block intact\" };\n } catch {\n return null;\n }\n}\n\nasync function skillManifestChecks(projectRoot: string): Promise<Promise<DriftFinding | null>[]> {\n const home = homedir();\n const targets = [\n { agent: \"claude\", path: join(home, \".claude\", \"skills\") },\n { agent: \"codex\", path: join(home, \".codex\", \"skills\") },\n { agent: \"copilot\", path: join(home, \".copilot\", \"skills\") },\n { agent: \"pi\", path: join(home, \".pi\", \"agent\", \"skills\") },\n { agent: \"claude-local\", path: join(projectRoot, \".claude\", \"skills\") },\n ];\n const out: Promise<DriftFinding | null>[] = [];\n for (const t of targets) {\n out.push(checkOneSkillManifest(t.agent, t.path));\n }\n return out;\n}\n\nasync function checkOneSkillManifest(agentLabel: string, skillsRoot: string): Promise<DriftFinding | null> {\n const manifestPath = join(skillsRoot, \".semantic-memory-skill-manifest.json\");\n if (!existsSync(manifestPath)) return null; // not installed for this agent — fine\n try {\n const raw = await readFile(manifestPath, \"utf-8\");\n const manifest = JSON.parse(raw);\n if (typeof manifest?.installed_at !== \"string\") return null;\n const age = Date.now() - new Date(manifest.installed_at).getTime();\n // Manifest itself is the source of truth; deeper sha-recheck happens via slow tier\n return {\n check: `skill_manifest:${agentLabel}`,\n severity: \"ok\",\n summary: `${agentLabel} skills installed v${manifest.version} (${Math.round(age / (24 * 60 * 60 * 1000))}d ago)`,\n };\n } catch {\n return null;\n }\n}\n\nfunction defaultVaultPath(projectRoot: string): string {\n return join(projectRoot, \".claude\", \".vault\");\n}\n\n/**\n * Format a drift summary for the SessionStart hook. Returns the text to inject as\n * additionalContext, or an empty string when there's nothing to surface.\n */\nexport function formatDriftBanner(result: HealthcheckResult): string {\n const drift = filterToDrift(result);\n if (drift.length === 0) return \"\";\n const lines = [\n `<vault-drift count=\"${drift.length}\">`,\n `⚠️ semantic-memory: ${drift.length} drift issue${drift.length === 1 ? \"\" : \"s\"} detected`,\n ];\n for (const f of drift.slice(0, 5)) {\n const sev = f.severity === \"error\" ? \"✗\" : \"⚠\";\n lines.push(` ${sev} ${f.check}: ${f.summary}`);\n }\n if (drift.length > 5) lines.push(` …and ${drift.length - 5} more`);\n lines.push(`Run /healthcheck for details, or /healthcheck --fix to auto-fix safe items.`);\n lines.push(`</vault-drift>`);\n return lines.join(\"\\n\");\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,96 @@
1
+ // src/core/decay.ts
2
+ import { existsSync, readFileSync } from "fs";
3
+ import { join } from "path";
4
+ import { parse as parseYaml } from "yaml";
5
+ function normalizeVerifiedDate(v) {
6
+ if (typeof v === "string") return v;
7
+ if (v instanceof Date && !Number.isNaN(v.getTime())) return v.toISOString();
8
+ return void 0;
9
+ }
10
+ var DEFAULT_DECAY_CONFIG = {
11
+ enabled: true,
12
+ default_half_life_days: 365,
13
+ per_type: {
14
+ decision: 365,
15
+ note: 365,
16
+ gotcha: 180,
17
+ source: null,
18
+ // never decays
19
+ proposal: 14
20
+ },
21
+ floor: 0.1,
22
+ hotness_boost: { enabled: false, cap: 2 }
23
+ };
24
+ var MS_PER_DAY = 24 * 60 * 60 * 1e3;
25
+ var EVERGREEN_MAX_AGE_DAYS = 365;
26
+ function fresh(reason, ageDays = 0, halfLife = 0, evergreen) {
27
+ return { multiplier: 1, age_days: ageDays, effective_half_life: halfLife, reason, evergreen };
28
+ }
29
+ function computeDecay(input) {
30
+ const { config } = input;
31
+ if (!config.enabled) return fresh("decay disabled");
32
+ let halfLife = config.default_half_life_days;
33
+ if (input.type && Object.prototype.hasOwnProperty.call(config.per_type, input.type)) {
34
+ const perType = config.per_type[input.type];
35
+ if (perType === null) {
36
+ return fresh(`type=${input.type} never decays`, ageDaysOf(input), Infinity);
37
+ }
38
+ if (typeof perType === "number" && perType > 0) halfLife = perType;
39
+ }
40
+ if (!input.last_verified) return fresh("no last_verified (fail open)", 0, halfLife);
41
+ const lastMs = Date.parse(input.last_verified);
42
+ if (Number.isNaN(lastMs)) return fresh("unparseable last_verified (fail open)", 0, halfLife);
43
+ const now = input.nowMs ?? Date.now();
44
+ const ageDays = (now - lastMs) / MS_PER_DAY;
45
+ if (ageDays <= 0) return fresh("last_verified in the future (fail open)", ageDays, halfLife);
46
+ if (input.evergreen && ageDays < EVERGREEN_MAX_AGE_DAYS) {
47
+ return fresh("evergreen (verified within 365d)", ageDays, halfLife, true);
48
+ }
49
+ let effHalfLife = halfLife;
50
+ if (config.hotness_boost.enabled && input.inbound_link_count && input.inbound_link_count > 0) {
51
+ const boost = Math.min(input.inbound_link_count / 5, config.hotness_boost.cap - 1);
52
+ effHalfLife = halfLife * (1 + Math.max(boost, 0));
53
+ }
54
+ const raw = Math.pow(0.5, ageDays / effHalfLife);
55
+ const multiplier = Math.max(raw, config.floor);
56
+ return {
57
+ multiplier,
58
+ age_days: ageDays,
59
+ effective_half_life: effHalfLife,
60
+ reason: `type=${input.type ?? "default"}`,
61
+ evergreen: input.evergreen
62
+ };
63
+ }
64
+ function ageDaysOf(input) {
65
+ if (!input.last_verified) return 0;
66
+ const lastMs = Date.parse(input.last_verified);
67
+ if (Number.isNaN(lastMs)) return 0;
68
+ const now = input.nowMs ?? Date.now();
69
+ return Math.max((now - lastMs) / MS_PER_DAY, 0);
70
+ }
71
+ function loadDecayConfig(vaultPath) {
72
+ try {
73
+ const schemaPath = join(vaultPath, "vault.schema.yml");
74
+ if (!existsSync(schemaPath)) return DEFAULT_DECAY_CONFIG;
75
+ const parsed = parseYaml(readFileSync(schemaPath, "utf-8"));
76
+ const d = parsed?.decay;
77
+ if (!d || typeof d !== "object") return DEFAULT_DECAY_CONFIG;
78
+ return {
79
+ enabled: typeof d.enabled === "boolean" ? d.enabled : DEFAULT_DECAY_CONFIG.enabled,
80
+ default_half_life_days: typeof d.default_half_life_days === "number" ? d.default_half_life_days : DEFAULT_DECAY_CONFIG.default_half_life_days,
81
+ per_type: { ...DEFAULT_DECAY_CONFIG.per_type, ...d.per_type ?? {} },
82
+ floor: typeof d.floor === "number" ? d.floor : DEFAULT_DECAY_CONFIG.floor,
83
+ hotness_boost: { ...DEFAULT_DECAY_CONFIG.hotness_boost, ...d.hotness_boost ?? {} }
84
+ };
85
+ } catch {
86
+ return DEFAULT_DECAY_CONFIG;
87
+ }
88
+ }
89
+
90
+ export {
91
+ normalizeVerifiedDate,
92
+ DEFAULT_DECAY_CONFIG,
93
+ computeDecay,
94
+ loadDecayConfig
95
+ };
96
+ //# sourceMappingURL=chunk-5ZKNO6ZD.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/decay.ts"],"sourcesContent":["import { existsSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { parse as parseYaml } from \"yaml\";\n\n/**\n * Confidence decay (v1.3).\n *\n * Pure, side-effect-free ranking signal: a note's relevance decays smoothly with\n * the time since it was last *verified* (not merely edited). Load-bearing notes\n * resist decay (longer half-life), source notes never decay, evergreen notes are\n * pinned until their evergreen claim itself expires, and an explicit re-verify\n * resets the clock.\n *\n * The multiplier composes MULTIPLICATIVELY with the existing priority/confidence\n * signals at search time — it never replaces them. Floored so notes down-weight\n * but never disappear.\n */\n\nexport interface DecayConfig {\n enabled: boolean;\n default_half_life_days: number;\n /** Per-type half-life in days. `null` means \"never decays\" (e.g. source notes). */\n per_type: Partial<Record<string, number | null>>;\n floor: number;\n hotness_boost: { enabled: boolean; cap: number };\n}\n\nexport interface DecayInput {\n type?: string;\n last_verified?: string;\n evergreen?: boolean;\n inbound_link_count?: number;\n config: DecayConfig;\n /** Override \"now\" — used by tests. */\n nowMs?: number;\n}\n\nexport interface DecayResult {\n multiplier: number;\n age_days: number;\n effective_half_life: number;\n reason: string;\n evergreen?: boolean;\n}\n\n/**\n * Normalize a frontmatter `last_verified` value to an ISO string. YAML parses an\n * unquoted `last_verified: 2019-01-01` into a Date, not a string, so a bare\n * `typeof === \"string\"` check would silently drop it and disable decay. Accepts\n * string or Date; anything else → undefined (fail open).\n */\nexport function normalizeVerifiedDate(v: unknown): string | undefined {\n if (typeof v === \"string\") return v;\n if (v instanceof Date && !Number.isNaN(v.getTime())) return v.toISOString();\n return undefined;\n}\n\n/** The default decay config, mirrored in the default vault schema YAML. */\nexport const DEFAULT_DECAY_CONFIG: DecayConfig = {\n enabled: true,\n default_half_life_days: 365,\n per_type: {\n decision: 365,\n note: 365,\n gotcha: 180,\n source: null, // never decays\n proposal: 14,\n },\n floor: 0.1,\n hotness_boost: { enabled: false, cap: 2.0 },\n};\n\nconst MS_PER_DAY = 24 * 60 * 60 * 1000;\nconst EVERGREEN_MAX_AGE_DAYS = 365;\n\nfunction fresh(reason: string, ageDays = 0, halfLife = 0, evergreen?: boolean): DecayResult {\n return { multiplier: 1.0, age_days: ageDays, effective_half_life: halfLife, reason, evergreen };\n}\n\n/**\n * Compute the decay multiplier for one note. Fails open (multiplier 1.0) on any\n * uncertainty — disabled config, missing/invalid/future last_verified — so decay\n * can only ever DOWN-rank a note we're confident is old, never penalize ambiguity.\n */\nexport function computeDecay(input: DecayInput): DecayResult {\n const { config } = input;\n if (!config.enabled) return fresh(\"decay disabled\");\n\n // Resolve half-life. A `null` per-type entry means this type never decays.\n let halfLife = config.default_half_life_days;\n if (input.type && Object.prototype.hasOwnProperty.call(config.per_type, input.type)) {\n const perType = config.per_type[input.type];\n if (perType === null) {\n return fresh(`type=${input.type} never decays`, ageDaysOf(input), Infinity);\n }\n if (typeof perType === \"number\" && perType > 0) halfLife = perType;\n }\n\n // Age since last verification. Missing/invalid/future → fail open.\n if (!input.last_verified) return fresh(\"no last_verified (fail open)\", 0, halfLife);\n const lastMs = Date.parse(input.last_verified);\n if (Number.isNaN(lastMs)) return fresh(\"unparseable last_verified (fail open)\", 0, halfLife);\n const now = input.nowMs ?? Date.now();\n const ageDays = (now - lastMs) / MS_PER_DAY;\n if (ageDays <= 0) return fresh(\"last_verified in the future (fail open)\", ageDays, halfLife);\n\n // Evergreen: pinned at 1.0 while its verification is fresh; falls through to\n // normal decay once the evergreen claim is older than a year (forces re-affirmation).\n if (input.evergreen && ageDays < EVERGREEN_MAX_AGE_DAYS) {\n return fresh(\"evergreen (verified within 365d)\", ageDays, halfLife, true);\n }\n\n // Backlink hotness (Phase 7 — feature-flagged off by default): a heavily-linked\n // note is likely load-bearing, so extend its effective half-life.\n let effHalfLife = halfLife;\n if (config.hotness_boost.enabled && input.inbound_link_count && input.inbound_link_count > 0) {\n const boost = Math.min(input.inbound_link_count / 5, config.hotness_boost.cap - 1);\n effHalfLife = halfLife * (1 + Math.max(boost, 0));\n }\n\n const raw = Math.pow(0.5, ageDays / effHalfLife);\n const multiplier = Math.max(raw, config.floor);\n return {\n multiplier,\n age_days: ageDays,\n effective_half_life: effHalfLife,\n reason: `type=${input.type ?? \"default\"}`,\n evergreen: input.evergreen,\n };\n}\n\nfunction ageDaysOf(input: DecayInput): number {\n if (!input.last_verified) return 0;\n const lastMs = Date.parse(input.last_verified);\n if (Number.isNaN(lastMs)) return 0;\n const now = input.nowMs ?? Date.now();\n return Math.max((now - lastMs) / MS_PER_DAY, 0);\n}\n\n/**\n * Load the decay config from a vault's `vault.schema.yml` `decay:` block, merged\n * over the shipped defaults. Missing file / block / parse error → defaults.\n * Synchronous + memoization-friendly (callers cache the result).\n */\nexport function loadDecayConfig(vaultPath: string): DecayConfig {\n try {\n const schemaPath = join(vaultPath, \"vault.schema.yml\");\n if (!existsSync(schemaPath)) return DEFAULT_DECAY_CONFIG;\n const parsed = parseYaml(readFileSync(schemaPath, \"utf-8\")) as { decay?: Partial<DecayConfig> } | null;\n const d = parsed?.decay;\n if (!d || typeof d !== \"object\") return DEFAULT_DECAY_CONFIG;\n return {\n enabled: typeof d.enabled === \"boolean\" ? d.enabled : DEFAULT_DECAY_CONFIG.enabled,\n default_half_life_days:\n typeof d.default_half_life_days === \"number\" ? d.default_half_life_days : DEFAULT_DECAY_CONFIG.default_half_life_days,\n per_type: { ...DEFAULT_DECAY_CONFIG.per_type, ...(d.per_type ?? {}) },\n floor: typeof d.floor === \"number\" ? d.floor : DEFAULT_DECAY_CONFIG.floor,\n hotness_boost: { ...DEFAULT_DECAY_CONFIG.hotness_boost, ...(d.hotness_boost ?? {}) },\n };\n } catch {\n return DEFAULT_DECAY_CONFIG;\n }\n}\n"],"mappings":"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","names":[]}