akm-cli 0.9.0-beta.5 → 0.9.0-beta.51

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 (221) hide show
  1. package/CHANGELOG.md +711 -0
  2. package/README.md +12 -4
  3. package/dist/akm +38 -0
  4. package/dist/akm-migrate-storage +38 -0
  5. package/dist/assets/profiles/default.json +9 -4
  6. package/dist/assets/profiles/frequent.json +1 -1
  7. package/dist/assets/profiles/memory-focus.json +1 -1
  8. package/dist/assets/profiles/quick.json +1 -1
  9. package/dist/assets/profiles/synthesize.json +15 -0
  10. package/dist/assets/profiles/thorough.json +1 -1
  11. package/dist/assets/prompts/consolidate-system.md +23 -0
  12. package/dist/assets/prompts/contradiction-judge.md +33 -0
  13. package/dist/assets/prompts/distill-knowledge-system.md +22 -0
  14. package/dist/assets/prompts/distill-lesson-system.md +36 -0
  15. package/dist/assets/prompts/extract-session.md +6 -2
  16. package/dist/assets/prompts/graph-extract-system.md +1 -0
  17. package/dist/assets/prompts/graph-extract-user-prompt.md +1 -1
  18. package/dist/assets/prompts/memory-infer-system.md +1 -0
  19. package/dist/assets/prompts/memory-infer-user.md +5 -0
  20. package/dist/assets/prompts/metadata-enhance-system.md +1 -0
  21. package/dist/assets/prompts/procedural-system.md +44 -0
  22. package/dist/assets/prompts/recombine-system.md +40 -0
  23. package/dist/assets/prompts/staleness-detect-system.md +6 -0
  24. package/dist/assets/prompts/validate-summary-judge.md +1 -0
  25. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +38 -0
  26. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +38 -0
  27. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +39 -0
  28. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +40 -0
  29. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +43 -0
  30. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +38 -0
  31. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +43 -0
  32. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +40 -0
  33. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +43 -0
  34. package/dist/assets/templates/html/health.html +281 -111
  35. package/dist/assets/wiki/ingest-workflow-template.md +38 -10
  36. package/dist/cli/parse-args.js +46 -1
  37. package/dist/cli/shared.js +28 -0
  38. package/dist/cli.js +27 -11
  39. package/dist/commands/agent/agent-dispatch.js +2 -2
  40. package/dist/commands/agent/agent-support.js +0 -7
  41. package/dist/commands/agent/contribute-cli.js +17 -4
  42. package/dist/commands/config-cli.js +18 -2
  43. package/dist/commands/env/child-env.js +47 -0
  44. package/dist/commands/env/env-cli.js +33 -26
  45. package/dist/commands/env/secret-cli.js +36 -22
  46. package/dist/commands/feedback-cli.js +15 -6
  47. package/dist/commands/graph/graph-cli.js +5 -13
  48. package/dist/commands/graph/graph.js +76 -72
  49. package/dist/commands/health/checks.js +49 -1
  50. package/dist/commands/health/html-report.js +422 -80
  51. package/dist/commands/health.js +386 -9
  52. package/dist/commands/improve/calibration.js +161 -0
  53. package/dist/commands/improve/consolidate/chunking.js +141 -0
  54. package/dist/commands/improve/consolidate/eligibility.js +81 -0
  55. package/dist/commands/improve/consolidate/merge.js +145 -0
  56. package/dist/commands/improve/consolidate/sanitize.js +231 -0
  57. package/dist/commands/{lint.js → improve/consolidate/types.js} +1 -1
  58. package/dist/commands/improve/consolidate.js +635 -660
  59. package/dist/commands/improve/dedup.js +482 -0
  60. package/dist/commands/improve/distill.js +159 -69
  61. package/dist/commands/improve/eligibility.js +434 -0
  62. package/dist/commands/improve/encoding-salience.js +205 -0
  63. package/dist/commands/improve/extract-cli.js +124 -2
  64. package/dist/commands/improve/extract-prompt.js +39 -2
  65. package/dist/commands/improve/extract-watch.js +140 -0
  66. package/dist/commands/improve/extract.js +389 -40
  67. package/dist/commands/improve/feedback-valence.js +54 -0
  68. package/dist/commands/improve/homeostatic.js +467 -0
  69. package/dist/commands/improve/improve-auto-accept.js +138 -7
  70. package/dist/commands/improve/improve-cli.js +36 -61
  71. package/dist/commands/improve/improve-profiles.js +14 -0
  72. package/dist/commands/improve/improve-result-file.js +14 -25
  73. package/dist/commands/improve/improve-session.js +58 -0
  74. package/dist/commands/improve/improve.js +485 -2498
  75. package/dist/commands/improve/locks.js +154 -0
  76. package/dist/commands/improve/loop-stages.js +1083 -0
  77. package/dist/commands/improve/memory/memory-contradiction-detect.js +23 -28
  78. package/dist/commands/improve/outcome-loop.js +256 -0
  79. package/dist/commands/improve/preparation.js +1966 -0
  80. package/dist/commands/improve/proactive-maintenance.js +115 -0
  81. package/dist/commands/improve/procedural.js +418 -0
  82. package/dist/commands/improve/recombine.js +850 -0
  83. package/dist/commands/improve/reflect-noise.js +0 -0
  84. package/dist/commands/improve/reflect.js +183 -40
  85. package/dist/commands/improve/salience.js +438 -0
  86. package/dist/commands/improve/triage.js +93 -0
  87. package/dist/commands/lint/agent-linter.js +19 -24
  88. package/dist/commands/lint/base-linter.js +173 -60
  89. package/dist/commands/lint/command-linter.js +19 -24
  90. package/dist/commands/lint/env-key-rules.js +38 -1
  91. package/dist/commands/lint/fact-linter.js +39 -0
  92. package/dist/commands/lint/index.js +31 -13
  93. package/dist/commands/lint/memory-linter.js +1 -1
  94. package/dist/commands/lint/registry.js +7 -2
  95. package/dist/commands/lint/task-linter.js +3 -3
  96. package/dist/commands/lint/workflow-linter.js +26 -1
  97. package/dist/commands/proposal/drain-policies.js +5 -0
  98. package/dist/commands/proposal/drain.js +43 -50
  99. package/dist/commands/proposal/proposal-cli.js +21 -31
  100. package/dist/commands/proposal/proposal.js +5 -0
  101. package/dist/commands/proposal/propose.js +7 -2
  102. package/dist/commands/proposal/validators/proposal-quality-validators.js +9 -8
  103. package/dist/commands/proposal/validators/proposals.js +189 -63
  104. package/dist/commands/read/curate.js +414 -94
  105. package/dist/commands/read/knowledge.js +6 -3
  106. package/dist/commands/read/search-cli.js +9 -4
  107. package/dist/commands/read/search.js +10 -6
  108. package/dist/commands/read/show.js +86 -7
  109. package/dist/commands/sources/init.js +49 -17
  110. package/dist/commands/sources/installed-stashes.js +11 -3
  111. package/dist/commands/sources/schema-repair.js +43 -45
  112. package/dist/commands/sources/self-update.js +2 -2
  113. package/dist/commands/sources/source-add.js +7 -3
  114. package/dist/commands/sources/stash-cli.js +28 -40
  115. package/dist/commands/sources/stash-skeleton.js +23 -8
  116. package/dist/commands/tasks/tasks-cli.js +19 -27
  117. package/dist/commands/tasks/tasks.js +39 -11
  118. package/dist/commands/wiki-cli.js +21 -35
  119. package/dist/core/asset/asset-registry.js +3 -1
  120. package/dist/core/asset/asset-spec.js +18 -2
  121. package/dist/core/asset/frontmatter.js +166 -167
  122. package/dist/core/asset/markdown.js +8 -0
  123. package/dist/core/authoring-rules.js +92 -0
  124. package/dist/core/common.js +0 -5
  125. package/dist/core/config/config-migration.js +12 -11
  126. package/dist/core/config/config-schema.js +340 -56
  127. package/dist/core/config/config-types.js +3 -3
  128. package/dist/core/config/config.js +28 -7
  129. package/dist/core/events.js +3 -7
  130. package/dist/core/improve-types.js +11 -8
  131. package/dist/core/logs-db.js +10 -66
  132. package/dist/core/parse.js +36 -16
  133. package/dist/core/paths.js +3 -0
  134. package/dist/core/standards/resolve-standards-context.js +87 -0
  135. package/dist/core/standards/resolve-stash-standards.js +99 -0
  136. package/dist/core/standards/resolve-type-conventions.js +66 -0
  137. package/dist/core/state/migrations.js +714 -0
  138. package/dist/core/state-db.js +525 -474
  139. package/dist/indexer/db/db.js +439 -247
  140. package/dist/indexer/db/graph-db.js +129 -86
  141. package/dist/indexer/ensure-index.js +152 -17
  142. package/dist/indexer/graph/graph-boost.js +51 -41
  143. package/dist/indexer/graph/graph-extraction.js +218 -4
  144. package/dist/indexer/index-writer-lock.js +99 -0
  145. package/dist/indexer/indexer.js +123 -221
  146. package/dist/indexer/passes/dir-staleness.js +114 -0
  147. package/dist/indexer/passes/memory-inference.js +13 -5
  148. package/dist/indexer/passes/staleness-detect.js +2 -5
  149. package/dist/indexer/search/db-search.js +19 -6
  150. package/dist/indexer/search/ranking-contributors.js +22 -0
  151. package/dist/indexer/search/ranking.js +4 -0
  152. package/dist/indexer/search/search-source.js +17 -18
  153. package/dist/indexer/search/semantic-status.js +4 -0
  154. package/dist/indexer/walk/matchers.js +9 -0
  155. package/dist/integrations/agent/config.js +6 -53
  156. package/dist/integrations/agent/index.js +2 -18
  157. package/dist/integrations/agent/prompts.js +75 -9
  158. package/dist/integrations/agent/runner-dispatch.js +59 -0
  159. package/dist/integrations/harnesses/claude/session-log.js +11 -1
  160. package/dist/integrations/harnesses/index.js +2 -3
  161. package/dist/integrations/harnesses/opencode/session-log.js +173 -3
  162. package/dist/integrations/harnesses/opencode-sdk/index.js +2 -2
  163. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +0 -2
  164. package/dist/integrations/session-logs/index.js +16 -0
  165. package/dist/llm/client.js +45 -15
  166. package/dist/llm/embedder.js +42 -3
  167. package/dist/llm/embedders/deterministic.js +66 -0
  168. package/dist/llm/embedders/local.js +66 -2
  169. package/dist/llm/feature-gate.js +8 -4
  170. package/dist/llm/graph-extract.js +67 -44
  171. package/dist/llm/memory-infer-impl.js +138 -0
  172. package/dist/llm/memory-infer.js +1 -127
  173. package/dist/llm/metadata-enhance.js +44 -31
  174. package/dist/llm/structured-call.js +49 -0
  175. package/dist/migrate-storage-node.mjs +8 -0
  176. package/dist/output/context.js +5 -5
  177. package/dist/output/renderers.js +74 -2
  178. package/dist/output/shapes/curate.js +14 -2
  179. package/dist/output/shapes/passthrough.js +0 -1
  180. package/dist/output/text/helpers.js +16 -1
  181. package/dist/registry/providers/skills-sh.js +21 -147
  182. package/dist/registry/providers/static-index.js +15 -157
  183. package/dist/registry/resolve.js +22 -9
  184. package/dist/runtime.js +25 -1
  185. package/dist/scripts/migrate-storage.js +2617 -1961
  186. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +759 -510
  187. package/dist/setup/setup.js +29 -8
  188. package/dist/sources/include.js +6 -2
  189. package/dist/sources/providers/filesystem.js +0 -1
  190. package/dist/sources/providers/git-install.js +210 -0
  191. package/dist/sources/providers/git-provider.js +234 -0
  192. package/dist/sources/providers/git-stash.js +248 -0
  193. package/dist/sources/providers/git.js +10 -661
  194. package/dist/sources/providers/npm.js +2 -6
  195. package/dist/sources/providers/provider-utils.js +13 -7
  196. package/dist/sources/providers/sync-from-ref.js +9 -1
  197. package/dist/sources/providers/tar-utils.js +16 -8
  198. package/dist/sources/providers/website.js +9 -5
  199. package/dist/sources/website-ingest.js +187 -29
  200. package/dist/sources/wiki-fetchers/registry.js +53 -0
  201. package/dist/sources/wiki-fetchers/youtube.js +239 -0
  202. package/dist/storage/database.js +45 -10
  203. package/dist/storage/managed-db.js +82 -0
  204. package/dist/storage/repositories/registry-cache.js +92 -0
  205. package/dist/storage/sqlite-pragmas.js +146 -0
  206. package/dist/tasks/backends/cron.js +1 -1
  207. package/dist/tasks/backends/launchd.js +1 -1
  208. package/dist/tasks/backends/schtasks.js +1 -1
  209. package/dist/tasks/{resolveAkmBin.js → resolve-akm-bin.js} +2 -2
  210. package/dist/tasks/runner.js +5 -13
  211. package/dist/text-import-hook.mjs +0 -0
  212. package/dist/wiki/wiki.js +37 -0
  213. package/dist/workflows/db.js +3 -4
  214. package/dist/workflows/runtime/runs.js +1 -117
  215. package/dist/workflows/runtime/workflow-asset-loader.js +125 -0
  216. package/dist/workflows/validate-summary.js +2 -7
  217. package/docs/data-and-telemetry.md +3 -2
  218. package/docs/migration/release-notes/0.9.0.md +39 -0
  219. package/package.json +13 -11
  220. package/dist/commands/db-cli.js +0 -23
  221. package/dist/indexer/db/db-backup.js +0 -376
@@ -0,0 +1,438 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ /**
5
+ * WS-1 — Unified SALIENCE model (S1 seam).
6
+ *
7
+ * Replaces the three competing attention formulas that existed across:
8
+ * - proactive-maintenance.ts:186 — `importance × log(1+freq) × recencyDecay / log10(size)`
9
+ * - feedback-valence.ts:111 — `combinedEligibilityScore = utility·0.7 + valence·0.3`
10
+ * - getUtilityScoresByIds — MemRL utility EMA (#386)
11
+ *
12
+ * ## Salience vector (three independently-stored, independently-decayable sub-scores)
13
+ *
14
+ * | Sub-score | Brain analogy | Source |
15
+ * |-------------------|------------------------|---------------------------------|
16
+ * | `encodingSalience`| Amygdala tagging (Gap 1)| Set at extract; v1 = type weight|
17
+ * | `outcomeSalience` | Dopaminergic outcome | WS-2 (0 until that lands) |
18
+ * | `retrievalSalience`| Hippocampal frequency+recency | usage_events + utility_scores |
19
+ *
20
+ * ## Projection
21
+ *
22
+ * `rankScore = (w_e·encoding + w_o·outcome + w_r·retrieval) × sizePenalty`, normalized [0,1].
23
+ *
24
+ * **WS-2 default-off (Part-V gate):**
25
+ * `w_o = 0.15` is the target but is applied only when `outcomeWeightEnabled=true`
26
+ * (set via `improve.salience.outcomeWeightEnabled: true` in config after running
27
+ * Part-V T0 baseline). Default: WS-1 parity weights `w_e=0.30, w_r=0.70, w_o=0`.
28
+ * `outcomeSalience` is populated from `asset_outcome.outcome_score` (WS-2) for
29
+ * observability regardless of the flag.
30
+ *
31
+ * ## Plasticity
32
+ *
33
+ * `consecutive_no_ops` (INTEGER column in `asset_salience`) dampens CONSOLIDATION-
34
+ * SELECTION only — it is intentionally NOT applied to `rankScore`, so a stable
35
+ * asset stays fully retrievable while no longer consuming repeated LLM merge
36
+ * attempts. See `getConsecutiveNoOps` / `recordNoOp` / `resetConsecutiveNoOps`.
37
+ *
38
+ * ## Canonical store
39
+ *
40
+ * The three sub-scores live in `state.db :: asset_salience` (canonical).
41
+ * An optional frontmatter mirror of the stable `encodingSalience` is allowed for
42
+ * portability (issue #608 may write it there), but state.db is the source of truth
43
+ * for ranking. This prevents the frontmatter-vs-state.db split that issue #608
44
+ * would otherwise create (#608 pull-forward decision from Part VI).
45
+ *
46
+ * @module salience
47
+ */
48
+ import { makeAssetRef } from "../../core/asset/asset-ref.js";
49
+ import { getAllEntries, getUtilityScoresByIds } from "../../indexer/db/db.js";
50
+ import { WARM_START_CAP } from "./outcome-loop.js";
51
+ // ── One day in ms ─────────────────────────────────────────────────────────────
52
+ const DAY_MS = 86_400_000;
53
+ // ── Recency decay half-life (mirrors the proactive-maintenance prototype) ─────
54
+ const RECENCY_HALFLIFE_DAYS = 21;
55
+ // ── Size proxy floor (avoids log10(0)) ────────────────────────────────────────
56
+ const SIZE_FLOOR_BYTES = 200;
57
+ // ── Projection weights ────────────────────────────────────────────────────────
58
+ //
59
+ // These constants reflect the WS-2 TARGET values (used when outcomeWeightEnabled=true).
60
+ // Default ranking uses WS-1 parity weights (w_e=0.30, w_r=0.70, w_o=0) until the
61
+ // maintainer opts in via `improve.salience.outcomeWeightEnabled: true` after running
62
+ // the Part-V T0 baseline (scripts/akm-eval + health report).
63
+ //
64
+ // WS-2 opt-in split (w_e=0.25, w_o=0.15, w_r=0.60, sum = 1.0):
65
+ // [exp] Expert recommendation: encoding should be moderate so a type-importance
66
+ // stub does not completely dominate; retrieval should be strong since it directly
67
+ // measures use; outcome provides a quality signal proportional to usefulness.
68
+ //
69
+ // Re-tune via the Part-V measurement protocol if the throughput/quality gate
70
+ // shows regression after enabling the outcome weight.
71
+ export const W_ENCODING = 0.25; // WS-2 target encoding weight (w_e)
72
+ export const W_OUTCOME = 0.15; // WS-2 target outcome weight (w_o)
73
+ export const W_RETRIEVAL = 0.6; // WS-2 target retrieval weight (w_r)
74
+ // Compile-time guard: weights must sum to 1.0 (±ε). The TS initializer runs
75
+ // at module load, not build time, so this acts as a startup assertion.
76
+ if (Math.abs(W_ENCODING + W_OUTCOME + W_RETRIEVAL - 1.0) > 1e-9) {
77
+ throw new Error(`salience.ts: W_ENCODING + W_OUTCOME + W_RETRIEVAL must equal 1.0 (got ${W_ENCODING + W_OUTCOME + W_RETRIEVAL})`);
78
+ }
79
+ // ── WS-1 parity weights ───────────────────────────────────────────────────────
80
+ //
81
+ // These constants reflect the default WS-1 parity weights used when
82
+ // `outcomeWeightEnabled` is false/absent (the default). They preserve the
83
+ // WS-1 two-way split (w_e=0.30, w_r=0.70) with w_o=0 so outcome does not
84
+ // affect rankScore until the operator opts in after the Part-V baseline run.
85
+ //
86
+ // Named here (rather than inline literals in the else branch) so a future
87
+ // re-tune has a single source of truth and the sum-to-1 guard below catches
88
+ // any accidental mis-edit.
89
+ export const W_ENCODING_PARITY = 0.3; // WS-1 parity encoding weight
90
+ export const W_OUTCOME_PARITY = 0; // WS-1 parity outcome weight (0 = disabled)
91
+ export const W_RETRIEVAL_PARITY = 0.7; // WS-1 parity retrieval weight
92
+ // Startup guard: parity triple must also sum to 1.0 (±ε).
93
+ if (Math.abs(W_ENCODING_PARITY + W_OUTCOME_PARITY + W_RETRIEVAL_PARITY - 1.0) > 1e-9) {
94
+ throw new Error(`salience.ts: W_ENCODING_PARITY + W_OUTCOME_PARITY + W_RETRIEVAL_PARITY must equal 1.0 (got ${W_ENCODING_PARITY + W_OUTCOME_PARITY + W_RETRIEVAL_PARITY})`);
95
+ }
96
+ // ── Type-importance fallback weights (#608 landed) ────────────────────────────
97
+ //
98
+ // The real encoding salience estimator is `scoreEncodingSalience` in
99
+ // `encoding-salience.ts` (#608). These weights are the fallback used when
100
+ // `SalienceInputs.encodingSalience` is absent (pre-#608 assets without a
101
+ // frontmatter `salience:` field or a state.db row seeded by distill).
102
+ export const DEFAULT_TYPE_ENCODING_WEIGHTS = Object.freeze({
103
+ skill: 0.9,
104
+ agent: 0.9,
105
+ command: 0.8,
106
+ workflow: 0.8,
107
+ lesson: 0.75,
108
+ knowledge: 0.7,
109
+ script: 0.6,
110
+ memory: 0.5,
111
+ });
112
+ /** Default encoding salience for types not in the table above. */
113
+ export const DEFAULT_ENCODING_SALIENCE = 0.5;
114
+ // ── Core computation ─────────────────────────────────────────────────────────
115
+ /**
116
+ * Compute the salience vector for one asset.
117
+ *
118
+ * Pure function — no I/O. All inputs are pre-fetched by the caller.
119
+ */
120
+ export function computeSalience(inputs) {
121
+ const now = inputs.now ?? Date.now();
122
+ // ── Encoding salience ────────────────────────────────────────────────────────
123
+ //
124
+ // When `inputs.encodingSalience` is provided (computed by `scoreEncodingSalience`
125
+ // in encoding-salience.ts at extract/distill time, #608), use it directly.
126
+ // Fall back to the type-importance stub only when the caller has not yet
127
+ // computed a content-based score (e.g. on older assets before the first
128
+ // staleness refresh runs).
129
+ const encodingSource = inputs.encodingSalience !== undefined ? "content" : "type-stub";
130
+ const encoding = inputs.encodingSalience !== undefined
131
+ ? Math.min(1, Math.max(0, inputs.encodingSalience))
132
+ : (DEFAULT_TYPE_ENCODING_WEIGHTS[inputs.type] ?? DEFAULT_ENCODING_SALIENCE);
133
+ // ── Outcome salience (WS-2 active) ────────────────────────────────────────
134
+ //
135
+ // When `inputs.outcomeSalience` is provided (WS-2 has populated asset_outcome
136
+ // for this ref), use it directly — it has already been normalised by
137
+ // `outcomeScoreToSalience` in outcome-loop.ts (value in [DIVERSITY_FLOOR, 1]).
138
+ //
139
+ // When absent (new asset, no WS-2 row yet): fall back to the warm-start seed
140
+ // from `utilityScore` clipped to [0, WARM_START_CAP], matching the seed
141
+ // value that `updateAssetOutcome` writes on first row creation. This ensures
142
+ // `outcomeSalience` is non-zero at launch for assets with utility history
143
+ // (avoiding the starvation problem described in the plan §WS-2 warm start).
144
+ let outcome;
145
+ if (inputs.outcomeSalience !== undefined) {
146
+ // Direct pass-through — caller already normalised via outcomeScoreToSalience.
147
+ outcome = Math.min(1, Math.max(0, inputs.outcomeSalience));
148
+ }
149
+ else {
150
+ // Warm-start fallback: clip utility to [0, WARM_START_CAP] so the
151
+ // outcomeSalience term contributes a modest non-zero baseline.
152
+ outcome = Math.min(WARM_START_CAP, Math.max(0, inputs.utilityScore ?? 0));
153
+ }
154
+ // ── Retrieval salience ─────────────────────────────────────────────────────
155
+ //
156
+ // Formula: log(1 + freq) × recencyDecay
157
+ // log(1+freq): sub-linear frequency term (same as proactive-maintenance prototype).
158
+ // recencyDecay: 0.1 + 0.5^(useAgeDays/halflife) — decays to 0.1 floor when stale.
159
+ // lastUseMs=0/undefined → useAgeDays=9999 → recencyDecay≈0.1 (floor).
160
+ //
161
+ // The recency term is MANDATORY (plan requirement §WS-1 step 2). Without it
162
+ // retrievalSalience degenerates to a non-decaying frequency count and the WS-3
163
+ // homeostatic step-0 demotion has nothing to act on.
164
+ const lastUseMs = inputs.lastUseMs ?? 0;
165
+ const useAgeDays = lastUseMs > 0 ? (now - lastUseMs) / DAY_MS : 9999;
166
+ const recencyDecay = 0.1 + 0.5 ** (useAgeDays / RECENCY_HALFLIFE_DAYS);
167
+ const rawRetrieval = Math.log(1 + inputs.retrievalFreq) * recencyDecay;
168
+ // ── Size penalty ─────────────────────────────────────────────────────────────
169
+ // 1/log10(size): larger assets are slightly deprioritized (same as proactive prototype).
170
+ const sizeProxy = Math.max(SIZE_FLOOR_BYTES, inputs.sizeBytes ?? 0);
171
+ const sizePenalty = 1 / Math.log10(sizeProxy);
172
+ // ── Projection → rankScore ────────────────────────────────────────────────
173
+ //
174
+ // Raw projection may be > 1 (log retrieval terms can exceed 1 for high freq + fresh use).
175
+ // Normalize by the theoretical maximum of the retrieval component:
176
+ // max retrievalRaw = log(1 + Infinity) × (0.1 + 1.0) = Infinity, so we
177
+ // cap instead — rankScore is clamped to [0,1] after applying the size penalty.
178
+ //
179
+ // Normalization approach: we scale the combined linear sum to [0,1] by clamping,
180
+ // after applying the size penalty. The encoding term is already in [0,1]; the
181
+ // retrieval term is open-ended but bounded in practice by log(1+N)×1.1 where N
182
+ // is the retrieval count. We normalize `retrieval` to [0,1] using a soft cap:
183
+ // retrieval_normalized = rawRetrieval / (rawRetrieval + 1)
184
+ // which asymptotes to 1 and equals 0.5 at rawRetrieval=1. This is the same
185
+ // formula used for MemRL utility updates.
186
+ const retrieval = rawRetrieval / (rawRetrieval + 1);
187
+ // ── Weight selection (Part-V gate) ────────────────────────────────────────
188
+ //
189
+ // When `outcomeWeightEnabled` is false/absent (default): use WS-1 parity
190
+ // weights (w_e=0.30, w_r=0.70, w_o=0) so ranking is unchanged from the WS-1
191
+ // baseline. The `outcome` sub-score is still computed and stored in the
192
+ // salience vector for observability, but it does not affect rankScore.
193
+ //
194
+ // When `outcomeWeightEnabled` is true (operator opt-in after Part-V run):
195
+ // use WS-2 weights (w_e=0.25, w_o=0.15, w_r=0.60).
196
+ //
197
+ // The constants W_ENCODING, W_OUTCOME, W_RETRIEVAL always reflect the
198
+ // WS-2 target values for documentation and re-tune reference.
199
+ let we;
200
+ let wo;
201
+ let wr;
202
+ if (inputs.outcomeWeightEnabled === true) {
203
+ // WS-2 active: three-way split from Part-V operator opt-in.
204
+ we = W_ENCODING; // 0.25
205
+ wo = W_OUTCOME; // 0.15
206
+ wr = W_RETRIEVAL; // 0.60
207
+ }
208
+ else {
209
+ // WS-1 parity (default): w_o=0, redistribute to WS-1 proportions.
210
+ // Original WS-1 split was w_e=0.30, w_r=0.70.
211
+ we = W_ENCODING_PARITY;
212
+ wo = W_OUTCOME_PARITY;
213
+ wr = W_RETRIEVAL_PARITY;
214
+ }
215
+ const rawRankScore = (we * encoding + wo * outcome + wr * retrieval) * sizePenalty;
216
+ const rankScore = Math.min(1, Math.max(0, rawRankScore));
217
+ return { encoding, outcome, retrieval, rankScore, encodingSource };
218
+ }
219
+ /**
220
+ * Does this row carry a genuine content-derived `encoding_salience` (#644)?
221
+ *
222
+ * Returns true when the provenance flag is `"content"`. For legacy rows
223
+ * (`encoding_source === null`, written before migration 015) we apply a
224
+ * conservative heuristic: treat the stored value as content-derived only when it
225
+ * does NOT equal the pure type-weight stub for the asset's type — because before
226
+ * the #644 fix every run overwrote real scores with the stub, a value that still
227
+ * differs from the stub must have been content-written and never re-clobbered.
228
+ * When the type cannot be determined (no `type` given) a null-provenance row is
229
+ * treated as a stub (the safe default).
230
+ */
231
+ export function isContentEncodingRow(row, type) {
232
+ if (row.encoding_source === "content")
233
+ return true;
234
+ if (row.encoding_source === "type-stub")
235
+ return false;
236
+ // Legacy NULL provenance: differ-from-stub heuristic.
237
+ if (!type)
238
+ return false;
239
+ const stub = DEFAULT_TYPE_ENCODING_WEIGHTS[type] ?? DEFAULT_ENCODING_SALIENCE;
240
+ return Math.abs(row.encoding_salience - stub) > 1e-9;
241
+ }
242
+ /**
243
+ * Upsert salience scores for one asset into state.db.
244
+ *
245
+ * Idempotent: safe to call every run; updates the outcome / retrieval / rank
246
+ * columns on conflict.
247
+ *
248
+ * #644 — encoding provenance guard: the `encoding_salience` + `encoding_source`
249
+ * columns are NOT lowered from a real content-derived score to a type-weight
250
+ * stub. When the stored row is `encoding_source = 'content'` and the incoming
251
+ * vector is a `type-stub` fallback, the stored encoding score and its provenance
252
+ * are preserved (only the other sub-scores and `rank_score` advance). A `content`
253
+ * write always wins; a `type-stub` write only seeds a row that has no content
254
+ * score yet. This stops the improve loop's type-weight fallback re-asserting the
255
+ * stub over a distill-written score on every run.
256
+ *
257
+ * NOTE: when the guard preserves the stored encoding score, the incoming
258
+ * `vector.rankScore` (computed from the stub encoding) is still written. Callers
259
+ * that want the rank_score to reflect the preserved content score should pass the
260
+ * stored content score back in as `inputs.encodingSalience` to `computeSalience`
261
+ * — which the improve loop does. The guard here is the defensive backstop.
262
+ */
263
+ export function upsertAssetSalience(db, ref, vector, now) {
264
+ const ts = now ?? Date.now();
265
+ db.prepare(`INSERT INTO asset_salience
266
+ (asset_ref, encoding_salience, outcome_salience, retrieval_salience, rank_score, consecutive_no_ops, updated_at, encoding_source)
267
+ VALUES (?, ?, ?, ?, ?, 0, ?, ?)
268
+ ON CONFLICT(asset_ref) DO UPDATE SET
269
+ -- #644: never lower a real content-derived score to a type-weight stub.
270
+ -- Keep the stored encoding score + provenance when the stored row is
271
+ -- 'content' and the incoming write is a 'type-stub' fallback.
272
+ encoding_salience = CASE
273
+ WHEN asset_salience.encoding_source = 'content' AND excluded.encoding_source = 'type-stub'
274
+ THEN asset_salience.encoding_salience
275
+ ELSE excluded.encoding_salience
276
+ END,
277
+ encoding_source = CASE
278
+ WHEN asset_salience.encoding_source = 'content' AND excluded.encoding_source = 'type-stub'
279
+ THEN asset_salience.encoding_source
280
+ ELSE excluded.encoding_source
281
+ END,
282
+ outcome_salience = excluded.outcome_salience,
283
+ retrieval_salience = excluded.retrieval_salience,
284
+ rank_score = excluded.rank_score,
285
+ updated_at = excluded.updated_at`).run(ref, vector.encoding, vector.outcome, vector.retrieval, vector.rankScore, ts, vector.encodingSource ?? "type-stub");
286
+ }
287
+ /**
288
+ * Load the salience row for one asset, or undefined if not yet computed.
289
+ */
290
+ export function getAssetSalience(db, ref) {
291
+ const row = db
292
+ .prepare(`SELECT asset_ref, encoding_salience, outcome_salience, retrieval_salience,
293
+ rank_score, consecutive_no_ops, updated_at, encoding_source
294
+ FROM asset_salience WHERE asset_ref = ?`)
295
+ .get(ref);
296
+ // Bun SQLite returns null (not undefined) when no row found.
297
+ return row == null ? undefined : row;
298
+ }
299
+ /**
300
+ * Load ALL rank scores from the asset_salience table (full-stash query).
301
+ *
302
+ * Used by the forgetting-safety report (plan §WS-1 step 7) to compute stash-wide
303
+ * rank positions rather than pool-relative positions. Returns an empty Map when the
304
+ * table is empty (first WS-1 run = no pre-existing rows).
305
+ *
306
+ * Order is unspecified; callers must sort before assigning 1-indexed positions.
307
+ */
308
+ export function getAllRankScores(db) {
309
+ const rows = db.prepare("SELECT asset_ref, rank_score FROM asset_salience").all();
310
+ const result = new Map();
311
+ for (const row of rows) {
312
+ result.set(row.asset_ref, row.rank_score);
313
+ }
314
+ return result;
315
+ }
316
+ // ── Plasticity helpers ────────────────────────────────────────────────────────
317
+ /**
318
+ * Increment `consecutive_no_ops` for an asset. Called after a no-op reflect/distill.
319
+ * Has NO effect on `rank_score` — the plasticity counter only dampens consolidation
320
+ * selection, not retrieval ranking. See plan §WS-1 step 8.
321
+ *
322
+ * Invariant: recordNoOp must never originate rank_score semantics. If the asset has
323
+ * no salience row yet (persistence's best-effort try/catch may have swallowed an
324
+ * error), we do nothing — a no-op counter is meaningless without a rank_score row,
325
+ * and a synthetic INSERT would fabricate a rank_score=0 entry that could produce
326
+ * false catastrophic-forgetting signals in buildRankChangeReport.
327
+ */
328
+ export function recordNoOp(db, ref) {
329
+ db.prepare(`UPDATE asset_salience SET consecutive_no_ops = consecutive_no_ops + 1, updated_at = ? WHERE asset_ref = ?`).run(Date.now(), ref);
330
+ // If changes === 0 the asset has no salience row yet — leave the table unchanged.
331
+ }
332
+ /**
333
+ * Reset `consecutive_no_ops` to 0 when an asset produces an accepted change.
334
+ * Call after a successful proposal acceptance or detected mutation.
335
+ */
336
+ export function resetConsecutiveNoOps(db, ref) {
337
+ db.prepare(`UPDATE asset_salience SET consecutive_no_ops = 0, updated_at = ? WHERE asset_ref = ?`).run(Date.now(), ref);
338
+ }
339
+ /**
340
+ * Return the `consecutive_no_ops` count for one asset. 0 when unknown.
341
+ */
342
+ export function getConsecutiveNoOps(db, ref) {
343
+ const row = db.prepare(`SELECT consecutive_no_ops FROM asset_salience WHERE asset_ref = ?`).get(ref);
344
+ return row?.consecutive_no_ops ?? 0;
345
+ }
346
+ // ── Consolidation-selection dampener constants ────────────────────────────────
347
+ //
348
+ // Assets with consecutive_no_ops >= THRESHOLD are deprioritised in the
349
+ // SELECTION ORDER only. The persisted rank_score is intentionally left
350
+ // unchanged so stable assets remain fully retrievable by other callers.
351
+ //
352
+ // Tuning guidance:
353
+ // THRESHOLD — how many consecutive no-op runs before dampening kicks in.
354
+ // 3 means "skipped three times in a row", which signals the
355
+ // LLM consistently has nothing to say about this asset.
356
+ // FACTOR — multiplicative penalty on the effective selection score.
357
+ // 0.5 halves the apparent score so a dampened asset sorts
358
+ // after any peer with >= half its rankScore.
359
+ export const SALIENCE_NO_OP_DAMPEN_THRESHOLD = 3;
360
+ export const SALIENCE_NO_OP_DAMPEN_FACTOR = 0.5;
361
+ /**
362
+ * Emit the forgetting-safety rank-change distribution report.
363
+ *
364
+ * Compares the provided `newRanks` (Map<ref, position (1-indexed)>) against
365
+ * the provided `oldRanks` and flags refs that were in the old top-200 but
366
+ * are now below position 500 as "forgetting candidates".
367
+ *
368
+ * Caller is responsible for computing old/new rank positions before and after
369
+ * the WS-1 formula cutover. Called once at cutover, not every run.
370
+ *
371
+ * @param oldRanks - Map<ref, 1-indexed rank position> under the OLD formula.
372
+ * @param newRanks - Map<ref, 1-indexed rank position> under the NEW formula.
373
+ * @param oldTopN - Assets in old top-N to guard (default: 200).
374
+ * @param forgettingThreshold - New rank position below which a fall is flagged (default: 500).
375
+ */
376
+ export function buildRankChangeReport(oldRanks, newRanks, oldTopN = 200, forgettingThreshold = 500) {
377
+ const allChanges = [];
378
+ const forgettingCandidates = [];
379
+ for (const [ref, oldRank] of oldRanks) {
380
+ const newRank = newRanks.get(ref);
381
+ if (newRank === undefined)
382
+ continue; // ref not in new ranking
383
+ const rankDelta = newRank - oldRank; // positive = fell in rank
384
+ allChanges.push({ ref, oldRank, newRank, rankDelta });
385
+ if (oldRank <= oldTopN && newRank > forgettingThreshold) {
386
+ forgettingCandidates.push({ ref, oldRank, newRank, rankDelta });
387
+ }
388
+ }
389
+ // Sort by magnitude of rank drop (most dramatic first).
390
+ forgettingCandidates.sort((a, b) => b.rankDelta - a.rankDelta);
391
+ return { forgettingCandidates, allChanges };
392
+ }
393
+ // ── Last-use timestamp lookup helper ─────────────────────────────────────────
394
+ //
395
+ // Wraps the index DB query to retrieve the last-retrieval timestamp per ref,
396
+ // so callers do not need to import the raw db helpers directly. Returns a Map
397
+ // keyed by the same ref strings passed in.
398
+ //
399
+ // Source: `utility_scores.last_used_at` (ISO-8601 string) joined to entries
400
+ // via entry_id. WS-2 may later supersede this with `asset_outcome.last_retrieved_at`.
401
+ /**
402
+ * Build a Map<ref, lastUseMs> from the index database's utility_scores table.
403
+ *
404
+ * Returns only refs that have a non-null `last_used_at`. Refs absent from the
405
+ * map should be treated as never retrieved (lastUseMs = 0).
406
+ *
407
+ * @param indexDb - An open read-capable index database connection.
408
+ * @param refs - The set of asset refs to look up.
409
+ */
410
+ export function getLastUseMsByRef(indexDb, refs) {
411
+ const result = new Map();
412
+ if (refs.length === 0)
413
+ return result;
414
+ const refSet = new Set(refs);
415
+ const allEntries = getAllEntries(indexDb);
416
+ const idToRef = new Map();
417
+ for (const indexed of allEntries) {
418
+ const ref = makeAssetRef(indexed.entry.type, indexed.entry.name);
419
+ if (refSet.has(ref))
420
+ idToRef.set(indexed.id, ref);
421
+ }
422
+ const ids = [...idToRef.keys()];
423
+ if (ids.length === 0)
424
+ return result;
425
+ const { global: scores } = getUtilityScoresByIds(indexDb, ids);
426
+ for (const [id, row] of scores) {
427
+ const ref = idToRef.get(id);
428
+ if (!ref)
429
+ continue;
430
+ const lastUsedAt = row.lastUsedAt;
431
+ if (!lastUsedAt)
432
+ continue;
433
+ const ms = typeof lastUsedAt === "number" ? lastUsedAt : Date.parse(lastUsedAt);
434
+ if (ms > 0)
435
+ result.set(ref, ms);
436
+ }
437
+ return result;
438
+ }
@@ -0,0 +1,93 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ /**
5
+ * Default minimum total score a session must reach to PASS the gate. Tuned
6
+ * conservatively: a single real narrative-lesson session, or a procedurally
7
+ * dense session, clears it — but pure read-only Q&A does not. Config-overridable
8
+ * via `processes.extract.triage.minScore`.
9
+ */
10
+ export const DEFAULT_TRIAGE_MIN_SCORE = 2;
11
+ // Decision / outcome / error narrative markers. Case-insensitive, word-bounded.
12
+ const MARKER_RE = /\b(error|failed|fix(?:ed)?|root cause|turns out|because|decided|instead|gotcha|workaround|regress(?:ed)?|broke|TIL)\b/i;
13
+ // Edit / commit / file-write markers in tool event text.
14
+ const EDIT_COMMIT_RE = /\b(Edit|Write|MultiEdit|git commit|diff)\b/i;
15
+ // A substantive turn is an assistant or tool turn whose text is non-trivial.
16
+ const SUBSTANTIVE_MIN_CHARS = 40;
17
+ export function scoreSessionTriage(data, minScore, options) {
18
+ const events = data.events;
19
+ const total = events.length;
20
+ // (a) markers — count word-bounded marker hits across all event text, capped.
21
+ let markerHits = 0;
22
+ for (const e of events) {
23
+ if (MARKER_RE.test(e.text))
24
+ markerHits += 1;
25
+ }
26
+ // Each marker-bearing event contributes 1 point, capped at 2 (a single real
27
+ // narrative session clears the bar on markers alone).
28
+ const markers = Math.min(markerHits, 2);
29
+ // (b) toolDensity — bounded contribution from tool-use events.
30
+ let toolEvents = 0;
31
+ for (const e of events) {
32
+ if (e.role === "tool")
33
+ toolEvents += 1;
34
+ }
35
+ // 0.25 per tool event, capped at 1.5.
36
+ const toolDensity = Math.min(toolEvents * 0.25, 1.5);
37
+ // (c) editCommit — bounded contribution from edit/write/commit markers on
38
+ // events (filePath present, or text matching the edit/commit regex).
39
+ let editCommitEvents = 0;
40
+ for (const e of events) {
41
+ if (e.filePath || EDIT_COMMIT_RE.test(e.text))
42
+ editCommitEvents += 1;
43
+ }
44
+ // 0.25 per edit/commit event, capped at 1.5. Together with toolDensity a
45
+ // procedurally dense session reaches well above the default threshold.
46
+ const editCommit = Math.min(editCommitEvents * 0.25, 1.5);
47
+ // (d) substantiveRatio — scaled fraction of non-trivial assistant+tool turns.
48
+ let substantive = 0;
49
+ for (const e of events) {
50
+ if ((e.role === "assistant" || e.role === "tool") && e.text.length >= SUBSTANTIVE_MIN_CHARS) {
51
+ substantive += 1;
52
+ }
53
+ }
54
+ const ratio = total > 0 ? substantive / total : 0;
55
+ // Scale to a bounded [0,1] contribution.
56
+ const substantiveRatio = Math.min(ratio, 1);
57
+ const subscores = { markers, toolDensity, editCommit, substantiveRatio };
58
+ const score = markers + toolDensity + editCommit + substantiveRatio;
59
+ // Base gate: score must clear the floor.
60
+ let pass = score >= minScore;
61
+ // #641 procedural-aware floor (opt-in, DEFAULT OFF).
62
+ // When enabled, a session that clears the score gate must ALSO have at least
63
+ // one narrative marker (markers >= 1) OR meaningful edit/commit signal
64
+ // (editCommit >= 0.5). Pure read-only Q&A sessions that pass only via
65
+ // toolDensity + substantiveRatio are rejected as low-signal.
66
+ if (pass && options?.proceduralAwareFloor === true) {
67
+ const hasProceduralSignal = markers >= 1 || editCommit >= 0.5;
68
+ if (!hasProceduralSignal) {
69
+ pass = false;
70
+ }
71
+ }
72
+ return {
73
+ pass,
74
+ score,
75
+ subscores,
76
+ ...(pass ? {} : { reason: "low_signal" }),
77
+ };
78
+ }
79
+ /**
80
+ * Resolve the effective triage config from the extract process config. Mirrors
81
+ * the minContentChars / schemaSimilarity resolution style in akmExtract.
82
+ *
83
+ * Default-off: `enabled` is FALSE unless `triage.enabled === true`. `minScore`
84
+ * defaults to {@link DEFAULT_TRIAGE_MIN_SCORE}.
85
+ */
86
+ export function resolveTriageConfig(extractProcess) {
87
+ const triage = extractProcess?.triage;
88
+ const enabled = triage?.enabled === true;
89
+ const minScore = typeof triage?.minScore === "number" ? triage.minScore : DEFAULT_TRIAGE_MIN_SCORE;
90
+ // #641: default-off — only true when explicitly set to true.
91
+ const proceduralAwareFloor = triage?.proceduralAwareFloor === true;
92
+ return { enabled, minScore, proceduralAwareFloor };
93
+ }
@@ -1,22 +1,25 @@
1
1
  // This Source Code Form is subject to the terms of the Mozilla Public
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
- import path from "node:path";
5
4
  import { BaseLinter } from "./base-linter.js";
5
+ /** Valid values for the `type` field in agent frontmatter. */
6
+ const VALID_AGENT_TYPES = ["agent"];
6
7
  /**
7
8
  * Linter for `agents/` assets.
8
9
  *
9
- * Extra check beyond base:
10
+ * Extra checks beyond base:
10
11
  * - `missing-name-or-type`: frontmatter exists but `name` or `type` field is
11
12
  * absent. Not auto-fixable; detail includes a suggested slug.
13
+ * - `missing-name-or-type` (invalid value): `type` is present but not a
14
+ * recognised agent type value.
12
15
  */
13
16
  export class AgentLinter extends BaseLinter {
14
17
  types = ["agents"];
15
18
  lint(ctx) {
16
19
  const issues = this.runBaseChecks(ctx);
17
- const missingFieldDetail = this.#checkMissingNameOrType(ctx.data, ctx.frontmatter);
20
+ const missingFieldDetail = this.checkMissingNameOrType(ctx.data, ctx.frontmatter);
18
21
  if (missingFieldDetail) {
19
- const slug = this.#suggestSlug(ctx.filePath);
22
+ const slug = this.suggestSlug(ctx.filePath);
20
23
  issues.push({
21
24
  file: ctx.relPath,
22
25
  issue: "missing-name-or-type",
@@ -24,26 +27,18 @@ export class AgentLinter extends BaseLinter {
24
27
  fixed: false,
25
28
  });
26
29
  }
30
+ else {
31
+ // Only validate the value when the field is actually present.
32
+ const invalidTypeDetail = this.checkInvalidTypeValue(ctx.data, VALID_AGENT_TYPES);
33
+ if (invalidTypeDetail) {
34
+ issues.push({
35
+ file: ctx.relPath,
36
+ issue: "missing-name-or-type",
37
+ detail: invalidTypeDetail,
38
+ fixed: false,
39
+ });
40
+ }
41
+ }
27
42
  return issues;
28
43
  }
29
- #checkMissingNameOrType(data, frontmatterText) {
30
- if (!frontmatterText)
31
- return null;
32
- const missingFields = [];
33
- if (!("name" in data) || !data.name)
34
- missingFields.push("name");
35
- if (!("type" in data) || !data.type)
36
- missingFields.push("type");
37
- if (missingFields.length === 0)
38
- return null;
39
- return `missing fields: ${missingFields.join(", ")}`;
40
- }
41
- #suggestSlug(filePath) {
42
- return path
43
- .basename(filePath, ".md")
44
- .toLowerCase()
45
- .replace(/[^a-z0-9-]+/g, "-")
46
- .replace(/-+/g, "-")
47
- .replace(/^-|-$/g, "");
48
- }
49
44
  }