akm-cli 0.9.0-beta.9 → 0.9.0-rc.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 (325) hide show
  1. package/CHANGELOG.md +592 -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/help/help-improve.md +9 -6
  6. package/dist/assets/hints/cli-hints-full.md +6 -5
  7. package/dist/assets/profiles/default.json +9 -4
  8. package/dist/assets/profiles/frequent.json +1 -1
  9. package/dist/assets/profiles/memory-focus.json +1 -1
  10. package/dist/assets/profiles/proactive-maintenance.json +25 -0
  11. package/dist/assets/profiles/quick.json +1 -1
  12. package/dist/assets/profiles/recombine-only.json +21 -0
  13. package/dist/assets/profiles/reflect-distill.json +30 -0
  14. package/dist/assets/profiles/synthesize.json +15 -0
  15. package/dist/assets/profiles/thorough.json +1 -1
  16. package/dist/assets/prompts/consolidate-system.md +23 -0
  17. package/dist/assets/prompts/contradiction-judge.md +33 -0
  18. package/dist/assets/prompts/distill-knowledge-system.md +22 -0
  19. package/dist/assets/prompts/distill-lesson-system.md +36 -0
  20. package/dist/assets/prompts/extract-session.md +11 -3
  21. package/dist/assets/prompts/graph-extract-system.md +1 -0
  22. package/dist/assets/prompts/graph-extract-user-prompt.md +1 -1
  23. package/dist/assets/prompts/memory-infer-system.md +1 -0
  24. package/dist/assets/prompts/memory-infer-user.md +5 -0
  25. package/dist/assets/prompts/metadata-enhance-system.md +1 -0
  26. package/dist/assets/prompts/procedural-system.md +44 -0
  27. package/dist/assets/prompts/recombine-system.md +40 -0
  28. package/dist/assets/prompts/staleness-detect-system.md +6 -0
  29. package/dist/assets/prompts/validate-summary-judge.md +1 -0
  30. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +38 -0
  31. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +38 -0
  32. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +39 -0
  33. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +40 -0
  34. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +43 -0
  35. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +38 -0
  36. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +43 -0
  37. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +40 -0
  38. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +43 -0
  39. package/dist/assets/templates/html/health.html +281 -111
  40. package/dist/assets/wiki/ingest-workflow-template.md +45 -16
  41. package/dist/assets/wiki/schema-template.md +4 -4
  42. package/dist/cli/clack.js +56 -0
  43. package/dist/cli/config-migrate.js +7 -1
  44. package/dist/cli/confirm.js +1 -1
  45. package/dist/cli/parse-args.js +46 -1
  46. package/dist/cli/shared.js +28 -0
  47. package/dist/cli.js +25 -21
  48. package/dist/commands/agent/agent-dispatch.js +3 -2
  49. package/dist/commands/agent/agent-support.js +0 -7
  50. package/dist/commands/agent/contribute-cli.js +26 -7
  51. package/dist/commands/config-cli.js +26 -13
  52. package/dist/commands/env/child-env.js +47 -0
  53. package/dist/commands/env/env-cli.js +220 -227
  54. package/dist/commands/env/env.js +14 -67
  55. package/dist/commands/env/secret-cli.js +140 -138
  56. package/dist/commands/feedback-cli.js +156 -155
  57. package/dist/commands/graph/graph-cli.js +5 -13
  58. package/dist/commands/graph/graph.js +3 -3
  59. package/dist/commands/health/advisories.js +151 -0
  60. package/dist/commands/health/checks.js +103 -16
  61. package/dist/commands/health/html-report.js +447 -81
  62. package/dist/commands/health/improve-metrics.js +771 -0
  63. package/dist/commands/health/llm-usage.js +65 -0
  64. package/dist/commands/health/md-report.js +103 -0
  65. package/dist/commands/health/metrics.js +278 -0
  66. package/dist/commands/health/stash-exposure.js +46 -0
  67. package/dist/commands/health/surfaces.js +216 -0
  68. package/dist/commands/health/task-runs.js +135 -0
  69. package/dist/commands/health/types.js +26 -0
  70. package/dist/commands/health/windows.js +195 -0
  71. package/dist/commands/health.js +91 -1091
  72. package/dist/commands/improve/anti-collapse.js +170 -0
  73. package/dist/commands/improve/calibration.js +161 -0
  74. package/dist/commands/improve/collapse-detector.js +421 -0
  75. package/dist/commands/improve/consolidate/chunking.js +141 -0
  76. package/dist/commands/improve/consolidate/eligibility.js +64 -0
  77. package/dist/commands/improve/consolidate/merge.js +145 -0
  78. package/dist/commands/improve/consolidate/sanitize.js +231 -0
  79. package/dist/commands/{lint.js → improve/consolidate/types.js} +1 -1
  80. package/dist/commands/improve/consolidate.js +1295 -1277
  81. package/dist/commands/improve/dedup.js +482 -0
  82. package/dist/commands/improve/distill/content-repair.js +202 -0
  83. package/dist/commands/improve/distill/promote-memory.js +229 -0
  84. package/dist/commands/improve/distill/quality-gate.js +236 -0
  85. package/dist/commands/improve/distill-guards.js +127 -0
  86. package/dist/commands/improve/distill-promotion-policy.js +826 -167
  87. package/dist/commands/improve/distill.js +228 -605
  88. package/dist/commands/improve/eligibility.js +434 -0
  89. package/dist/commands/improve/encoding-salience.js +205 -0
  90. package/dist/commands/improve/extract-cli.js +179 -59
  91. package/dist/commands/improve/extract-prompt.js +54 -3
  92. package/dist/commands/improve/extract-watch.js +140 -0
  93. package/dist/commands/improve/extract.js +409 -43
  94. package/dist/commands/improve/feedback-valence.js +54 -0
  95. package/dist/commands/improve/hot-probation.js +45 -0
  96. package/dist/commands/improve/improve-auto-accept.js +157 -10
  97. package/dist/commands/improve/improve-cli.js +115 -73
  98. package/dist/commands/improve/improve-profiles.js +28 -8
  99. package/dist/commands/improve/improve-result-file.js +15 -25
  100. package/dist/commands/improve/improve-session.js +58 -0
  101. package/dist/commands/improve/improve.js +485 -2764
  102. package/dist/commands/improve/locks.js +154 -0
  103. package/dist/commands/improve/loop-stages.js +1100 -0
  104. package/dist/commands/improve/memory/memory-belief.js +14 -15
  105. package/dist/commands/improve/memory/memory-contradiction-detect.js +83 -60
  106. package/dist/commands/improve/memory/memory-improve.js +27 -27
  107. package/dist/commands/improve/outcome-loop.js +270 -0
  108. package/dist/commands/improve/preparation.js +2002 -0
  109. package/dist/commands/improve/proactive-maintenance.js +37 -35
  110. package/dist/commands/improve/procedural.js +398 -0
  111. package/dist/commands/improve/recombine.js +818 -0
  112. package/dist/commands/improve/reflect-noise.js +0 -0
  113. package/dist/commands/improve/reflect.js +206 -45
  114. package/dist/commands/improve/salience.js +455 -0
  115. package/dist/commands/improve/schema-similarity-gate.js +168 -0
  116. package/dist/commands/improve/shared.js +51 -0
  117. package/dist/commands/improve/triage.js +93 -0
  118. package/dist/commands/lint/agent-linter.js +19 -24
  119. package/dist/commands/lint/base-linter.js +173 -60
  120. package/dist/commands/lint/command-linter.js +19 -24
  121. package/dist/commands/lint/env-key-rules.js +38 -1
  122. package/dist/commands/lint/fact-linter.js +39 -0
  123. package/dist/commands/lint/index.js +31 -13
  124. package/dist/commands/lint/memory-linter.js +1 -1
  125. package/dist/commands/lint/registry.js +7 -2
  126. package/dist/commands/lint/task-linter.js +3 -3
  127. package/dist/commands/lint/workflow-linter.js +26 -1
  128. package/dist/commands/observability-cli.js +4 -4
  129. package/dist/commands/proposal/drain-policies.js +13 -4
  130. package/dist/commands/proposal/drain.js +45 -51
  131. package/dist/commands/proposal/legacy-import.js +115 -0
  132. package/dist/commands/proposal/proposal-cli.js +24 -34
  133. package/dist/commands/proposal/proposal.js +2 -1
  134. package/dist/commands/proposal/propose.js +8 -3
  135. package/dist/commands/proposal/repository.js +829 -0
  136. package/dist/commands/proposal/validators/proposal-quality-validators.js +9 -8
  137. package/dist/commands/proposal/validators/proposals.js +93 -895
  138. package/dist/commands/read/curate.js +410 -111
  139. package/dist/commands/read/knowledge.js +10 -3
  140. package/dist/commands/read/remember-cli.js +133 -138
  141. package/dist/commands/read/search-cli.js +15 -8
  142. package/dist/commands/read/search.js +22 -11
  143. package/dist/commands/read/show.js +106 -14
  144. package/dist/commands/registry-cli.js +76 -87
  145. package/dist/commands/remember.js +11 -12
  146. package/dist/commands/sources/add-cli.js +91 -95
  147. package/dist/commands/sources/history.js +1 -1
  148. package/dist/commands/sources/init.js +66 -18
  149. package/dist/commands/sources/installed-stashes.js +11 -3
  150. package/dist/commands/sources/schema-repair.js +44 -46
  151. package/dist/commands/sources/self-update.js +2 -2
  152. package/dist/commands/sources/source-add.js +7 -3
  153. package/dist/commands/sources/sources-cli.js +3 -3
  154. package/dist/commands/sources/stash-cli.js +19 -39
  155. package/dist/commands/sources/stash-skeleton.js +57 -8
  156. package/dist/commands/tasks/default-tasks.js +15 -2
  157. package/dist/commands/tasks/tasks-cli.js +20 -29
  158. package/dist/commands/tasks/tasks.js +39 -11
  159. package/dist/commands/wiki-cli.js +23 -38
  160. package/dist/commands/workflow-cli.js +15 -1
  161. package/dist/core/asset/asset-registry.js +3 -1
  162. package/dist/core/asset/asset-spec.js +21 -4
  163. package/dist/core/asset/frontmatter.js +188 -167
  164. package/dist/core/asset/markdown.js +8 -0
  165. package/dist/core/authoring-rules.js +92 -0
  166. package/dist/core/common.js +4 -23
  167. package/dist/core/concurrent.js +10 -1
  168. package/dist/core/config/config-io.js +10 -1
  169. package/dist/core/config/config-migration.js +18 -40
  170. package/dist/core/config/config-schema.js +382 -62
  171. package/dist/core/config/config-types.js +3 -3
  172. package/dist/core/config/config.js +67 -22
  173. package/dist/core/deep-merge.js +38 -0
  174. package/dist/core/errors.js +1 -0
  175. package/dist/core/eval/rank-metrics.js +113 -0
  176. package/dist/core/events.js +4 -7
  177. package/dist/core/improve-types.js +47 -8
  178. package/dist/core/logs-db.js +14 -75
  179. package/dist/core/parse.js +36 -16
  180. package/dist/core/paths.js +18 -18
  181. package/dist/core/standards/resolve-standards-context.js +87 -0
  182. package/dist/core/standards/resolve-stash-standards.js +99 -0
  183. package/dist/core/standards/resolve-type-conventions.js +66 -0
  184. package/dist/core/state/migrations.js +770 -0
  185. package/dist/core/state-db.js +132 -1126
  186. package/dist/core/structured.js +69 -0
  187. package/dist/core/time.js +53 -0
  188. package/dist/core/warn.js +21 -0
  189. package/dist/core/write-source.js +37 -0
  190. package/dist/indexer/db/db.js +259 -769
  191. package/dist/indexer/db/entry-mapper.js +41 -0
  192. package/dist/indexer/db/graph-db.js +129 -86
  193. package/dist/indexer/db/llm-cache.js +2 -2
  194. package/dist/indexer/db/schema.js +516 -0
  195. package/dist/indexer/ensure-index.js +36 -92
  196. package/dist/indexer/feedback/utility-policy.js +75 -0
  197. package/dist/indexer/graph/graph-boost.js +51 -41
  198. package/dist/indexer/graph/graph-extraction.js +207 -4
  199. package/dist/indexer/index-writer-lock.js +18 -11
  200. package/dist/indexer/index-written-assets.js +105 -0
  201. package/dist/indexer/indexer.js +182 -204
  202. package/dist/indexer/passes/dir-staleness.js +114 -0
  203. package/dist/indexer/passes/memory-inference.js +13 -5
  204. package/dist/indexer/passes/metadata.js +20 -0
  205. package/dist/indexer/read-preflight.js +23 -0
  206. package/dist/indexer/search/db-search.js +89 -13
  207. package/dist/indexer/search/fts-query.js +51 -0
  208. package/dist/indexer/search/ranking-contributors.js +95 -9
  209. package/dist/indexer/search/ranking.js +79 -3
  210. package/dist/indexer/search/search-fields.js +6 -0
  211. package/dist/indexer/search/search-source.js +32 -21
  212. package/dist/indexer/search/semantic-status.js +4 -0
  213. package/dist/indexer/walk/matchers.js +9 -0
  214. package/dist/indexer/walk/walker.js +21 -13
  215. package/dist/integrations/agent/builders.js +39 -13
  216. package/dist/integrations/agent/config.js +20 -59
  217. package/dist/integrations/agent/detect.js +9 -0
  218. package/dist/integrations/agent/index.js +3 -19
  219. package/dist/integrations/agent/model-aliases.js +7 -2
  220. package/dist/integrations/agent/profiles.js +7 -1
  221. package/dist/integrations/agent/prompts.js +75 -9
  222. package/dist/integrations/agent/runner-dispatch.js +59 -0
  223. package/dist/integrations/agent/runner.js +13 -9
  224. package/dist/integrations/agent/spawn.js +69 -67
  225. package/dist/integrations/harnesses/claude/agent-builder.js +1 -1
  226. package/dist/integrations/harnesses/claude/index.js +2 -0
  227. package/dist/integrations/harnesses/claude/session-log.js +10 -0
  228. package/dist/integrations/harnesses/index.js +2 -3
  229. package/dist/integrations/harnesses/opencode/agent-builder.js +1 -1
  230. package/dist/integrations/harnesses/opencode/index.js +2 -0
  231. package/dist/integrations/harnesses/opencode/session-log.js +173 -3
  232. package/dist/integrations/harnesses/opencode-sdk/index.js +2 -2
  233. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +98 -17
  234. package/dist/integrations/harnesses/types.js +1 -0
  235. package/dist/integrations/session-logs/index.js +16 -0
  236. package/dist/llm/call-ai.js +2 -2
  237. package/dist/llm/client.js +34 -11
  238. package/dist/llm/embedder.js +67 -4
  239. package/dist/llm/embedders/cache.js +3 -1
  240. package/dist/llm/embedders/deterministic.js +66 -0
  241. package/dist/llm/embedders/local.js +73 -3
  242. package/dist/llm/feature-gate.js +16 -15
  243. package/dist/llm/graph-extract.js +67 -44
  244. package/dist/llm/memory-infer-impl.js +138 -0
  245. package/dist/llm/memory-infer.js +1 -127
  246. package/dist/llm/metadata-enhance.js +44 -31
  247. package/dist/llm/structured-call.js +49 -0
  248. package/dist/migrate-storage-node.mjs +8 -0
  249. package/dist/output/context.js +5 -5
  250. package/dist/output/renderers.js +85 -14
  251. package/dist/output/shapes/curate.js +14 -2
  252. package/dist/output/shapes/helpers.js +0 -3
  253. package/dist/output/shapes/passthrough.js +2 -1
  254. package/dist/output/text/helpers.js +29 -1
  255. package/dist/output/text/workflow.js +1 -0
  256. package/dist/registry/providers/skills-sh.js +21 -147
  257. package/dist/registry/providers/static-index.js +15 -157
  258. package/dist/registry/resolve.js +27 -9
  259. package/dist/runtime.js +25 -1
  260. package/dist/scripts/migrate-storage.js +2661 -2369
  261. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +883 -596
  262. package/dist/setup/detect.js +9 -0
  263. package/dist/setup/legacy-config.js +106 -0
  264. package/dist/setup/prompt.js +57 -0
  265. package/dist/setup/providers.js +14 -0
  266. package/dist/setup/registry-stash-loader.js +12 -0
  267. package/dist/setup/semantic-assets.js +124 -0
  268. package/dist/setup/setup.js +52 -1614
  269. package/dist/setup/steps/connection.js +734 -0
  270. package/dist/setup/steps/output.js +31 -0
  271. package/dist/setup/steps/platforms.js +124 -0
  272. package/dist/setup/steps/semantic.js +27 -0
  273. package/dist/setup/steps/sources.js +222 -0
  274. package/dist/setup/steps/stashdir.js +42 -0
  275. package/dist/setup/steps/tasks.js +152 -0
  276. package/dist/sources/include.js +6 -2
  277. package/dist/sources/providers/filesystem.js +0 -1
  278. package/dist/sources/providers/git-install.js +210 -0
  279. package/dist/sources/providers/git-provider.js +234 -0
  280. package/dist/sources/providers/git-stash.js +248 -0
  281. package/dist/sources/providers/git.js +10 -661
  282. package/dist/sources/providers/npm.js +2 -6
  283. package/dist/sources/providers/provider-utils.js +13 -7
  284. package/dist/sources/providers/sync-from-ref.js +9 -1
  285. package/dist/sources/providers/website.js +9 -5
  286. package/dist/sources/website-ingest.js +187 -29
  287. package/dist/sources/wiki-fetchers/registry.js +53 -0
  288. package/dist/sources/wiki-fetchers/youtube.js +239 -0
  289. package/dist/storage/database.js +45 -10
  290. package/dist/storage/managed-db.js +82 -0
  291. package/dist/storage/repositories/canaries-repository.js +107 -0
  292. package/dist/storage/repositories/consolidation-repository.js +38 -0
  293. package/dist/storage/repositories/embeddings-repository.js +72 -0
  294. package/dist/storage/repositories/events-repository.js +187 -0
  295. package/dist/storage/repositories/extract-sessions-repository.js +96 -0
  296. package/dist/storage/repositories/improve-runs-repository.js +146 -0
  297. package/dist/storage/repositories/index-db.js +14 -8
  298. package/dist/storage/repositories/proposals-repository.js +220 -0
  299. package/dist/storage/repositories/recombine-repository.js +213 -0
  300. package/dist/storage/repositories/registry-cache.js +93 -0
  301. package/dist/storage/repositories/registry-index-cache-repository.js +46 -0
  302. package/dist/storage/repositories/task-history-repository.js +93 -0
  303. package/dist/storage/sqlite-pragmas.js +146 -0
  304. package/dist/tasks/backends/cron.js +1 -1
  305. package/dist/tasks/backends/index.js +9 -0
  306. package/dist/tasks/backends/launchd.js +1 -1
  307. package/dist/tasks/backends/schtasks.js +1 -1
  308. package/dist/tasks/{resolveAkmBin.js → resolve-akm-bin.js} +2 -2
  309. package/dist/tasks/runner.js +15 -13
  310. package/dist/text-import-hook.mjs +0 -0
  311. package/dist/wiki/wiki.js +52 -11
  312. package/dist/workflows/cli.js +1 -0
  313. package/dist/workflows/db.js +3 -4
  314. package/dist/workflows/runtime/runs.js +43 -118
  315. package/dist/workflows/runtime/workflow-asset-loader.js +125 -0
  316. package/dist/workflows/validate-summary.js +2 -7
  317. package/docs/README.md +69 -18
  318. package/docs/data-and-telemetry.md +5 -4
  319. package/docs/migration/release-notes/0.7.0.md +1 -1
  320. package/docs/migration/release-notes/0.9.0.md +39 -0
  321. package/package.json +10 -10
  322. package/dist/assets/tasks/core/update-stashes.yml +0 -4
  323. package/dist/commands/db-cli.js +0 -23
  324. package/dist/indexer/db/db-backup.js +0 -376
  325. package/dist/indexer/passes/staleness-detect.js +0 -488
@@ -0,0 +1,229 @@
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
+ * Memory→knowledge promotion branch for `akm distill`.
6
+ *
7
+ * This is an entire second command that used to be inlined inside `akmDistill`:
8
+ * when a `memory:*` ref is reinforced enough (per the deterministic stability
9
+ * heuristic in `distill-promotion-policy`), distill graduates it into a
10
+ * `knowledge:*` proposal instead of a lesson. The branch owns its own LLM
11
+ * contradiction-merge (mem0 ADD/UPDATE/NOOP), quality gate, proposal creation,
12
+ * and `distill_invoked` event emit.
13
+ *
14
+ * {@link promoteMemoryToKnowledge} returns the finished {@link AkmDistillResult}
15
+ * when the branch fired, or `null` when the ref is not a promotion candidate —
16
+ * in which case the caller falls through to the ordinary lesson/knowledge LLM
17
+ * path. Logic is byte-identical to the pre-extraction inline code.
18
+ */
19
+ import fs from "node:fs";
20
+ import { parseFrontmatter } from "../../../core/asset/frontmatter.js";
21
+ import { getDefaultLlmConfig } from "../../../core/config/config.js";
22
+ import { ConfigError } from "../../../core/errors.js";
23
+ import { appendEvent } from "../../../core/events.js";
24
+ import { parseEmbeddedJsonResponse } from "../../../llm/client.js";
25
+ import { isLlmFeatureEnabled } from "../../../llm/feature-gate.js";
26
+ import { createProposal, isProposalSkipped } from "../../proposal/repository.js";
27
+ import { assessMemoryKnowledgePromotionCandidate } from "../distill-promotion-policy.js";
28
+ import { persistOutputEncodingSalience, runLessonQualityJudge, writeQualityRejection } from "./quality-gate.js";
29
+ /**
30
+ * Run the memory→knowledge promotion branch. Returns the finished distill
31
+ * result when promotion fired (all paths terminal), or `null` when the ref is
32
+ * not a promotion candidate and the caller should continue to the ordinary
33
+ * lesson/knowledge distillation path.
34
+ */
35
+ export async function promoteMemoryToKnowledge(ctx) {
36
+ const { targetKind, inputRef, assetContent, config, chat, stash, lookup, fetchSimilarLessonsFn, existingRefVocabulary, outcomeWeightEnabled, eligMeta, exclusionSetSize, filteredFeedbackCount, feedbackFullyFiltered, } = ctx;
37
+ const promotion = targetKind === "lesson"
38
+ ? null
39
+ : assessMemoryKnowledgePromotionCandidate({
40
+ inputRef,
41
+ assetContent,
42
+ feedbackEvents: ctx.filteredEvents.map((event) => ({
43
+ ...(event.metadata !== undefined ? { metadata: event.metadata } : {}),
44
+ })),
45
+ });
46
+ if (!(promotion?.promote && promotion.content && (targetKind === "knowledge" || targetKind === "auto"))) {
47
+ return null;
48
+ }
49
+ // D-1 / #369: When the destination knowledge file already exists, route
50
+ // through the LLM for contradiction resolution instead of silently
51
+ // overwriting. Follows mem0 ADD/UPDATE/DELETE/NOOP pattern (arXiv:2504.19413 §3.2)
52
+ // and A-MEM dynamic linking (arXiv:2502.12110).
53
+ let resolvedPromotionContent = promotion.content;
54
+ const existingKnowledgePath = await lookup(promotion.knowledgeRef);
55
+ const existingKnowledgeContent = existingKnowledgePath && fs.existsSync(existingKnowledgePath)
56
+ ? (() => {
57
+ try {
58
+ return fs.readFileSync(existingKnowledgePath, "utf8");
59
+ }
60
+ catch {
61
+ return null;
62
+ }
63
+ })()
64
+ : null;
65
+ if (existingKnowledgeContent && config && getDefaultLlmConfig(config)) {
66
+ // Existing content found: call LLM for contradiction-resolution merge.
67
+ const mergePrompt = [
68
+ "You are merging two versions of a knowledge document.",
69
+ "Existing content is already committed; new content comes from a memory distillation run.",
70
+ "Choose one of: ADD (combine both), UPDATE (replace existing with new), NOOP (keep existing unchanged).",
71
+ 'Return ONLY valid JSON: {"action": "ADD"|"UPDATE"|"NOOP", "content": "<merged markdown if ADD/UPDATE, empty string if NOOP>"}',
72
+ "",
73
+ "## Existing knowledge content",
74
+ "```",
75
+ existingKnowledgeContent.slice(0, 3000),
76
+ "```",
77
+ "",
78
+ "## New content from distillation",
79
+ "```",
80
+ promotion.content.slice(0, 3000),
81
+ "```",
82
+ ].join("\n");
83
+ try {
84
+ const mergeLlm = getDefaultLlmConfig(config);
85
+ if (!mergeLlm) {
86
+ throw new ConfigError("LLM is not configured for distillation merge.", "LLM_NOT_CONFIGURED");
87
+ }
88
+ const mergeResponse = await chat(mergeLlm, [
89
+ { role: "system", content: "Return only valid JSON. No prose." },
90
+ { role: "user", content: mergePrompt },
91
+ ]);
92
+ const mergeResult = parseEmbeddedJsonResponse(mergeResponse);
93
+ if (mergeResult?.action === "NOOP") {
94
+ // Existing content is authoritative — no update needed.
95
+ appendEvent({
96
+ eventType: "distill_invoked",
97
+ ref: inputRef,
98
+ metadata: {
99
+ outcome: "skipped",
100
+ lessonRef: promotion.knowledgeRef,
101
+ message: "D-1: LLM resolved destination conflict as NOOP — existing content kept",
102
+ ...eligMeta,
103
+ },
104
+ });
105
+ return {
106
+ schemaVersion: 1,
107
+ ok: true,
108
+ outcome: "skipped",
109
+ inputRef,
110
+ lessonRef: promotion.knowledgeRef,
111
+ message: "Existing knowledge content unchanged (contradiction resolution: NOOP)",
112
+ };
113
+ }
114
+ if (mergeResult?.action && (mergeResult.action === "ADD" || mergeResult.action === "UPDATE")) {
115
+ if (mergeResult.content?.trim()) {
116
+ resolvedPromotionContent = mergeResult.content;
117
+ }
118
+ }
119
+ }
120
+ catch {
121
+ // LLM merge failed — fall through with the original promotion content.
122
+ // The reviewer will see both versions in the proposal diff.
123
+ }
124
+ }
125
+ else if (existingKnowledgeContent && config && !getDefaultLlmConfig(config)) {
126
+ // No LLM configured: include existing content as context in the proposal
127
+ // so the reviewer can do the contradiction resolution manually.
128
+ resolvedPromotionContent = [
129
+ promotion.content,
130
+ "",
131
+ "---",
132
+ "<!-- D-1 / #369: Existing knowledge content is shown below for reviewer reference. -->",
133
+ "<!-- Review: decide whether to ADD (merge), UPDATE (replace), or NOOP (keep existing). -->",
134
+ "",
135
+ "## Existing content (for reviewer reference)",
136
+ "",
137
+ existingKnowledgeContent,
138
+ ].join("\n");
139
+ }
140
+ // Apply quality gate to fast-path knowledge promotion (Risk 4 fix).
141
+ // D-5 / #388: Three-band system — review_needed band queues to proposal
142
+ // queue with review_needed outcome rather than auto-rejecting.
143
+ let knowledgeJudgeConfidence;
144
+ if (isLlmFeatureEnabled(config, "lesson_quality_gate")) {
145
+ // D-4 / #390: retrieve top-3 similar lessons for dedup check in judge.
146
+ const similarLessons = await fetchSimilarLessonsFn(resolvedPromotionContent.slice(0, 500), 3);
147
+ const judgeResult = await runLessonQualityJudge(config, resolvedPromotionContent, assetContent ?? "", chat, similarLessons.length > 0 ? similarLessons : undefined);
148
+ if (!judgeResult.pass) {
149
+ if (judgeResult.reviewNeeded) {
150
+ // Uncertainty band (2.5–3.5): queue as review_needed instead of rejecting.
151
+ return writeQualityRejection(stash, inputRef, promotion.knowledgeRef, resolvedPromotionContent, judgeResult.score, judgeResult.reason, { reviewNeeded: true }, ctx.eligibilitySource);
152
+ }
153
+ return writeQualityRejection(stash, inputRef, promotion.knowledgeRef, resolvedPromotionContent, judgeResult.score, judgeResult.reason, {}, ctx.eligibilitySource);
154
+ }
155
+ // Normalize 1-5 judge score to [0, 1]. Only a real passing verdict reaches
156
+ // here (07 P0-2: the judge now fails CLOSED on no-LLM / timeout / parse
157
+ // failure, so those return pass:false and early-return above). The score>0
158
+ // guard defensively leaves confidence undefined for any non-positive score.
159
+ if (judgeResult.score > 0)
160
+ knowledgeJudgeConfidence = judgeResult.score / 5;
161
+ }
162
+ const knowledgeParsed = parseFrontmatter(resolvedPromotionContent);
163
+ const proposalResult = createProposal(stash, {
164
+ ref: promotion.knowledgeRef,
165
+ source: "distill",
166
+ ...(ctx.sourceRun !== undefined ? { sourceRun: ctx.sourceRun } : {}),
167
+ payload: {
168
+ content: resolvedPromotionContent,
169
+ ...(Object.keys(knowledgeParsed.data).length > 0 ? { frontmatter: knowledgeParsed.data } : {}),
170
+ },
171
+ ...(knowledgeJudgeConfidence !== undefined ? { confidence: knowledgeJudgeConfidence } : {}),
172
+ // Attribution tagging: persist the eligibility lane on the proposal.
173
+ ...(ctx.eligibilitySource ? { eligibilitySource: ctx.eligibilitySource } : {}),
174
+ }, ctx.proposalsCtx);
175
+ if (isProposalSkipped(proposalResult)) {
176
+ appendEvent({
177
+ eventType: "distill_invoked",
178
+ ref: inputRef,
179
+ metadata: {
180
+ outcome: "skipped",
181
+ lessonRef: promotion.knowledgeRef,
182
+ message: proposalResult.message,
183
+ skipReason: proposalResult.reason,
184
+ ...eligMeta,
185
+ },
186
+ });
187
+ return {
188
+ schemaVersion: 1,
189
+ ok: true,
190
+ outcome: "skipped",
191
+ inputRef,
192
+ lessonRef: promotion.knowledgeRef,
193
+ message: proposalResult.message,
194
+ };
195
+ }
196
+ const proposal = proposalResult;
197
+ // G4: content-score the distilled OUTPUT so it carries a real encoding
198
+ // salience (encoding_source='content') from creation.
199
+ persistOutputEncodingSalience(promotion.knowledgeRef, resolvedPromotionContent, existingRefVocabulary, outcomeWeightEnabled);
200
+ appendEvent({
201
+ eventType: "distill_invoked",
202
+ ref: inputRef,
203
+ metadata: {
204
+ outcome: "queued",
205
+ lessonRef: promotion.knowledgeRef,
206
+ proposalRef: promotion.knowledgeRef,
207
+ proposalKind: "knowledge",
208
+ proposalId: proposal.id,
209
+ // R3: judge verdicts are longitudinally queryable, not just a one-shot
210
+ // proposal.confidence write (normalized 1–5 score / 5).
211
+ ...(knowledgeJudgeConfidence !== undefined ? { judgeConfidence: knowledgeJudgeConfidence } : {}),
212
+ ...(ctx.sourceRun !== undefined ? { sourceRun: ctx.sourceRun } : {}),
213
+ ...(exclusionSetSize > 0 ? { filteredFeedbackCount } : {}),
214
+ ...eligMeta,
215
+ },
216
+ });
217
+ return {
218
+ schemaVersion: 1,
219
+ ok: true,
220
+ outcome: "queued",
221
+ inputRef,
222
+ lessonRef: promotion.knowledgeRef,
223
+ proposalRef: promotion.knowledgeRef,
224
+ proposalKind: "knowledge",
225
+ proposalId: proposal.id,
226
+ proposal,
227
+ ...(exclusionSetSize > 0 ? { filteredFeedbackCount, feedbackFullyFiltered } : {}),
228
+ };
229
+ }
@@ -0,0 +1,236 @@
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
+ * Distill quality-gate cluster — LLM-as-judge, quality-rejection envelope
6
+ * writer, and output-salience persistence. Extracted verbatim from
7
+ * `distill.ts` so the main `akmDistill` orchestrator and the memory→knowledge
8
+ * promotion branch (`promote-memory.ts`) can share the same helpers without a
9
+ * circular import. Logic is byte-identical to the pre-extraction inline code.
10
+ */
11
+ import fs from "node:fs";
12
+ import path from "node:path";
13
+ import { parseAssetRef } from "../../../core/asset/asset-ref.js";
14
+ import { timestampForFilename } from "../../../core/common.js";
15
+ import { getDefaultLlmConfig } from "../../../core/config/config.js";
16
+ import { appendEvent } from "../../../core/events.js";
17
+ import { withStateDb } from "../../../core/state-db.js";
18
+ import { parseEmbeddedJsonResponse } from "../../../llm/client.js";
19
+ import { akmSearch } from "../../read/search.js";
20
+ import { scoreEncodingSalience } from "../encoding-salience.js";
21
+ import { computeSalience, upsertAssetSalience } from "../salience.js";
22
+ // ── D-4 / #390: Top-3 similar lessons retrieval ──────────────────────────────
23
+ /**
24
+ * Default implementation: use akmSearch to find top-N similar lesson assets.
25
+ * Returns empty array when search fails or returns no results.
26
+ * Requires embedding configured for semantic similarity; degrades gracefully.
27
+ */
28
+ export async function fetchTopSimilarLessons(query, n, _stashDir) {
29
+ try {
30
+ const result = await akmSearch({
31
+ query,
32
+ type: "lesson",
33
+ limit: n,
34
+ skipLogging: true,
35
+ eventSource: "improve",
36
+ });
37
+ const hits = result?.hits ?? [];
38
+ return hits
39
+ .filter((h) => "path" in h && typeof h.path === "string")
40
+ .slice(0, n)
41
+ .map((h) => {
42
+ let content = "";
43
+ try {
44
+ if (h.path && fs.existsSync(h.path)) {
45
+ content = fs.readFileSync(h.path, "utf8");
46
+ }
47
+ }
48
+ catch {
49
+ /* best-effort */
50
+ }
51
+ return { ref: h.ref, content };
52
+ });
53
+ }
54
+ catch {
55
+ return [];
56
+ }
57
+ }
58
+ // ── LLM-as-judge quality gate (P2-B) ────────────────────────────────────────
59
+ /**
60
+ * D-4 / #390: Build the LLM-as-judge prompt.
61
+ *
62
+ * When similarLessons are provided (top-3 by embedding similarity), they are
63
+ * included in the context so the judge can lower the score for near-duplicates.
64
+ * Voyager arXiv:2305.16291 — skill library admission requires similarity check
65
+ * against the existing library. A-MEM arXiv:2502.12110 — new notes are checked
66
+ * against existing notes before linking.
67
+ */
68
+ export function buildJudgePrompt(lessonContent, sourceContent, similarLessons) {
69
+ const lines = [
70
+ "You are evaluating a proposed lesson asset for an akm knowledge base.",
71
+ "",
72
+ "Score this lesson on each criterion from 1 (poor) to 5 (excellent):",
73
+ "1. NOVELTY: Does the lesson add information not already present in the source asset?",
74
+ "2. ACTIONABILITY: Can an agent follow this lesson without additional context?",
75
+ "3. NON-REDUNDANCY: Is this lesson meaningfully different from what the source already says?",
76
+ "",
77
+ "Source asset content:",
78
+ "```",
79
+ sourceContent.slice(0, 2000),
80
+ "```",
81
+ ];
82
+ if (similarLessons && similarLessons.length > 0) {
83
+ lines.push("");
84
+ lines.push("Existing similar lessons (top-3 by similarity). Rate lower if the proposed lesson is substantially similar to any of these:");
85
+ for (const sl of similarLessons) {
86
+ lines.push(`\nExisting lesson ref: ${sl.ref}`);
87
+ lines.push("```");
88
+ lines.push(sl.content.slice(0, 500));
89
+ lines.push("```");
90
+ }
91
+ }
92
+ lines.push("");
93
+ lines.push("Proposed lesson content:");
94
+ lines.push("```");
95
+ lines.push(lessonContent.slice(0, 1000));
96
+ lines.push("```");
97
+ lines.push("");
98
+ lines.push('Return ONLY valid JSON, no prose: {"score": <average score 1-5 as float>, "reason": "<one sentence>"}');
99
+ return lines.join("\n");
100
+ }
101
+ /**
102
+ * Run the LLM-as-judge quality gate on a proposal's content.
103
+ *
104
+ * Exported so reflect.ts can apply the same gate to reflect proposals (R-5 / #374).
105
+ * Gated by the flag name `lesson_quality_gate` (or its alias
106
+ * `proposal_quality_gate`) via {@link isLlmFeatureEnabled} — which reads
107
+ * `profiles.improve.default.processes.distill.qualityGate.enabled` (and the
108
+ * corresponding `.reflect.qualityGate.enabled` for proposals).
109
+ *
110
+ * Fail-CLOSED (07 P0-2): returns `pass: false` (score -1) on timeout, parse
111
+ * failure, or missing LLM. Minted content that cannot be judged is rejected,
112
+ * not passed through — an unverifiable judge must never wave content into the
113
+ * stash. The rejection is `quality_rejected`, not `review_needed`.
114
+ */
115
+ export async function runLessonQualityJudge(config, lessonContent, sourceContent, chat,
116
+ /** D-4 / #390: top-3 similar existing lessons for dedup check. */
117
+ similarLessons) {
118
+ const llmConfig = getDefaultLlmConfig(config);
119
+ if (!llmConfig) {
120
+ return { pass: false, score: -1, reason: "no LLM configured — cannot judge, failing closed" };
121
+ }
122
+ const judgeLlmConfig = llmConfig.judgeModel ? { ...llmConfig, model: llmConfig.judgeModel } : llmConfig;
123
+ const JUDGE_TIMEOUT_MS = 8_000;
124
+ try {
125
+ const raw = await Promise.race([
126
+ chat(judgeLlmConfig, [
127
+ { role: "system", content: "Return only valid JSON. No prose." },
128
+ { role: "user", content: buildJudgePrompt(lessonContent, sourceContent, similarLessons) },
129
+ ]),
130
+ new Promise((_, reject) => setTimeout(() => reject(new Error("judge timeout")), JUDGE_TIMEOUT_MS)),
131
+ ]);
132
+ const parsed = parseEmbeddedJsonResponse(raw);
133
+ if (!parsed || typeof parsed.score !== "number") {
134
+ return { pass: false, score: -1, reason: "judge parse failed — cannot judge, failing closed" };
135
+ }
136
+ // D-5 / #388: Three-band system (MT-Bench arXiv:2306.05685 — ~±0.5 judge variance).
137
+ // >= 3.5: auto-queue as pending (pass: true)
138
+ // 2.5–3.5: review-needed band — uncertain, escalate to human (reviewNeeded: true)
139
+ // < 2.5: auto-reject (pass: false)
140
+ const score = parsed.score;
141
+ const reason = parsed.reason ?? "";
142
+ if (score >= 3.5) {
143
+ return { pass: true, score, reason };
144
+ }
145
+ if (score >= 2.5) {
146
+ // Uncertainty band: treat as failed for auto-queuing but flag for review.
147
+ return { pass: false, score, reason, reviewNeeded: true };
148
+ }
149
+ return { pass: false, score, reason };
150
+ }
151
+ catch {
152
+ return { pass: false, score: -1, reason: "judge timeout/error — cannot judge, failing closed" };
153
+ }
154
+ }
155
+ // ── Quality-rejection helper ─────────────────────────────────────────────────
156
+ /**
157
+ * Write a rejected lesson to `.akm/distill-rejected/`, append a `distill_invoked`
158
+ * quality-rejected event, and return the `quality_rejected` envelope.
159
+ *
160
+ * @param stash - Root stash directory.
161
+ * @param inputRef - The original input ref (for the event).
162
+ * @param lessonRef - The proposed lesson/knowledge ref.
163
+ * @param content - The raw content that failed the quality gate.
164
+ * @param score - Quality score from the judge.
165
+ * @param reason - Human-readable rejection reason.
166
+ * @param extraMeta - Optional additional metadata for the event.
167
+ */
168
+ export function writeQualityRejection(stash, inputRef, lessonRef, content, score, reason, extraMeta = {}, eligibilitySource) {
169
+ // D-5 / #388: reviewNeeded flag selects "review_needed" vs "quality_rejected" outcome.
170
+ const outcome = extraMeta.reviewNeeded ? "review_needed" : "quality_rejected";
171
+ const rejectDir = path.join(stash, ".akm", "distill-rejected");
172
+ fs.mkdirSync(rejectDir, { recursive: true });
173
+ const ts = timestampForFilename();
174
+ fs.writeFileSync(path.join(rejectDir, `${ts}-${lessonRef}.md`), `---\nscore: ${score}\nreason: ${reason}\noutcome: ${outcome}\n---\n\n${content}`, "utf8");
175
+ appendEvent({
176
+ eventType: "distill_invoked",
177
+ ref: inputRef,
178
+ metadata: {
179
+ outcome,
180
+ lessonRef,
181
+ score,
182
+ reason,
183
+ ...extraMeta,
184
+ // Attribution tagging: stamp the eligibility lane so distill_invoked can be
185
+ // sliced by lane downstream. See EligibilitySource.
186
+ ...(eligibilitySource ? { eligibilitySource } : {}),
187
+ },
188
+ });
189
+ return {
190
+ schemaVersion: 1,
191
+ ok: true,
192
+ outcome,
193
+ inputRef,
194
+ lessonRef,
195
+ score,
196
+ reason,
197
+ ...extraMeta,
198
+ };
199
+ }
200
+ /**
201
+ * G4 — content-score a distilled OUTPUT (lesson/knowledge proposal body) and
202
+ * persist it to state.db :: asset_salience with `encoding_source: "content"`.
203
+ *
204
+ * Lessons are refused as distill INPUTS (`DISTILL_REFUSED_INPUT_TYPES`), so
205
+ * this creation-time write is their only chance to earn a real content-derived
206
+ * encoding score instead of sitting on the type-weight stub forever. Best-effort:
207
+ * never blocks or fails the proposal flow.
208
+ */
209
+ export function persistOutputEncodingSalience(ref, body, existingRefVocabulary,
210
+ // Operator opt-out (improve.salience.outcomeWeightEnabled: false) must apply
211
+ // here too, or distill-written rank_score rows would use WS-2 weights while
212
+ // preparation uses parity weights — inconsistent salience semantics.
213
+ outcomeWeightEnabled) {
214
+ try {
215
+ const parsedRef = parseAssetRef(ref);
216
+ const salienceResult = scoreEncodingSalience({
217
+ body,
218
+ type: parsedRef.type,
219
+ existingRefVocabulary,
220
+ revisionCount: 0, // a freshly distilled output IS a first encounter
221
+ });
222
+ withStateDb((stateDb) => {
223
+ const vector = computeSalience({
224
+ ref,
225
+ type: parsedRef.type,
226
+ retrievalFreq: 0,
227
+ encodingSalience: salienceResult.score,
228
+ outcomeWeightEnabled,
229
+ });
230
+ upsertAssetSalience(stateDb, ref, vector);
231
+ });
232
+ }
233
+ catch {
234
+ // Best-effort — scoring must never block proposal creation.
235
+ }
236
+ }
@@ -0,0 +1,127 @@
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-3b distill-stage guards.
6
+ *
7
+ * **CLS interleaving (step 9)**
8
+ * distill/memoryInference prompts include embedding-retrieved adjacent
9
+ * lessons/knowledge so the pipeline doesn't overwrite prior generalizations.
10
+ *
11
+ * **Distill→source fidelity (step 10)**
12
+ * After a distill proposal, check it against cited source memories; a
13
+ * contradiction flag routes to human review.
14
+ *
15
+ * @module distill-guards
16
+ */
17
+ // ── CLS adjacent lesson context (step 9) ─────────────────────────────────────
18
+ /** Default number of adjacent lessons/knowledge for CLS interleaving. */
19
+ export const DEFAULT_CLS_ADJACENT_COUNT = 3;
20
+ /**
21
+ * Build a CLS (Complementary Learning System) context snippet for injection
22
+ * into distill/memoryInference prompts.
23
+ *
24
+ * Given a list of embedding-retrieved adjacent lessons/knowledge, formats them
25
+ * as a markdown section to append to the prompt so the LLM avoids overwriting
26
+ * prior generalizations.
27
+ *
28
+ * Returns an empty string when CLS is disabled or no adjacent items are found.
29
+ *
30
+ * @param adjacentItems - Top-N adjacent lessons/knowledge retrieved by embedding.
31
+ * @param config - CLS config.
32
+ */
33
+ export function buildClsContext(adjacentItems, config) {
34
+ if (!config.enabled || adjacentItems.length === 0)
35
+ return "";
36
+ const lines = [
37
+ "",
38
+ "## Existing adjacent lessons / knowledge (CLS context)",
39
+ "The following are semantically related entries already in the stash.",
40
+ "Your proposal MUST NOT contradict or silently overwrite these — if you",
41
+ "disagree with one, flag it as contradicted (do not ignore it).",
42
+ "",
43
+ ];
44
+ for (const item of adjacentItems) {
45
+ lines.push(`### ${item.ref}`);
46
+ // Truncate to 400 chars to keep the prompt size reasonable.
47
+ lines.push(item.content.trim().slice(0, 400));
48
+ lines.push("");
49
+ }
50
+ return lines.join("\n");
51
+ }
52
+ /**
53
+ * Check a distill proposal against its cited source memories for contradictions.
54
+ *
55
+ * Uses a simple heuristic: looks for explicit negation of key claims in the
56
+ * proposal body that appear in the source bodies. A full LLM-based
57
+ * contradiction check is expensive (one LLM call per proposal); this cheap
58
+ * heuristic catches the most obvious cases and flags them for human review.
59
+ *
60
+ * When `fidelityCheck.enabled` is false, returns `{ contradictionDetected: false }`
61
+ * immediately (no work done).
62
+ *
63
+ * @param proposalBody - The stripped body of the distill proposal.
64
+ * @param sourceBodies - The stripped bodies of the cited source memories.
65
+ * @param config - Fidelity check config.
66
+ */
67
+ export function checkDistillFidelity(proposalBody, sourceBodies, config) {
68
+ if (!config.enabled || sourceBodies.length === 0) {
69
+ return { contradictionDetected: false };
70
+ }
71
+ // Heuristic: detect explicit negation of "never" / "always" / "must" claims.
72
+ // A proposal that says "always X" while the source says "never X" (or vice
73
+ // versa) is a clear contradiction worth flagging.
74
+ //
75
+ // This is intentionally conservative: it only flags when both the proposal
76
+ // AND the source contain the opposing polarity of the same key term. False
77
+ // negatives (missed contradictions) are preferred over false positives
78
+ // (blocking valid proposals) since the consequence of a false positive is
79
+ // a human review request, while the cost of a false negative is a slightly
80
+ // degraded stash.
81
+ const proposalLow = proposalBody.toLowerCase();
82
+ // Extract "always/never/must/must not" claims from the proposal.
83
+ const strongClaims = extractStrongClaims(proposalLow);
84
+ if (strongClaims.length === 0)
85
+ return { contradictionDetected: false };
86
+ for (const sourceBody of sourceBodies) {
87
+ const sourceLow = sourceBody.toLowerCase();
88
+ for (const { polarity, term } of strongClaims) {
89
+ const oppositePolarity = polarity === "positive" ? "negative" : "positive";
90
+ const sourceHasOpposite = hasStrongClaim(sourceLow, term, oppositePolarity);
91
+ if (sourceHasOpposite) {
92
+ return {
93
+ contradictionDetected: true,
94
+ reason: `Proposal makes a ${polarity} strong claim about "${term}" that conflicts with an opposing claim in a cited source. Route to human review.`,
95
+ };
96
+ }
97
+ }
98
+ }
99
+ // Also flag proposals whose source_refs are empty (broken provenance).
100
+ // This is a degradation signal, not a contradiction, but worth surfacing.
101
+ return { contradictionDetected: false };
102
+ }
103
+ function extractStrongClaims(text) {
104
+ const claims = [];
105
+ // Match "always <term>", "never <term>", "must <term>", "must not <term>".
106
+ const patterns = [
107
+ { polarity: "positive", re: /\b(?:always|must)\s+(\w+)/g },
108
+ { polarity: "negative", re: /\b(?:never|must\s+not|should\s+not)\s+(\w+)/g },
109
+ ];
110
+ for (const { polarity, re } of patterns) {
111
+ re.lastIndex = 0;
112
+ let m = re.exec(text);
113
+ while (m !== null) {
114
+ const term = m[1];
115
+ if (term && term.length > 2)
116
+ claims.push({ polarity, term });
117
+ m = re.exec(text);
118
+ }
119
+ }
120
+ return claims;
121
+ }
122
+ function hasStrongClaim(text, term, polarity) {
123
+ if (polarity === "positive") {
124
+ return /\b(?:always|must)\s/.test(text) && text.includes(term);
125
+ }
126
+ return /\b(?:never|must\s+not|should\s+not)\s/.test(text) && text.includes(term);
127
+ }