blun-king-cli 9.1.567 → 9.1.570

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 (263) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/agent-spine-plugin/.claude-plugin/marketplace.json +1 -1
  3. package/agent-spine-plugin/.claude-plugin/plugin.json +1 -1
  4. package/agent-spine-plugin/.codex-plugin/plugin.json +2 -1
  5. package/agent-spine-plugin/CHANGELOG.md +1581 -0
  6. package/agent-spine-plugin/README.md +30 -4
  7. package/agent-spine-plugin/blun.plugin.json +3 -3
  8. package/agent-spine-plugin/docs/acceptance.md +61 -0
  9. package/agent-spine-plugin/docs/assignment-continuation.md +48 -0
  10. package/agent-spine-plugin/docs/host-integration.md +178 -0
  11. package/agent-spine-plugin/docs/preflight-recall.md +69 -0
  12. package/agent-spine-plugin/docs/preservation-contract.md +53 -0
  13. package/agent-spine-plugin/docs/quality-gates.md +50 -0
  14. package/agent-spine-plugin/docs/releasing.md +85 -0
  15. package/agent-spine-plugin/docs/session-timeline.md +251 -0
  16. package/agent-spine-plugin/docs/source-roots.md +113 -0
  17. package/agent-spine-plugin/docs/structured-completion.md +67 -0
  18. package/agent-spine-plugin/docs/world-model.md +94 -0
  19. package/agent-spine-plugin/hooks/codex.json +2 -2
  20. package/agent-spine-plugin/hooks/hooks.json +1 -1
  21. package/agent-spine-plugin/hooks/version.json +1 -1
  22. package/agent-spine-plugin/package.json +13 -3
  23. package/agent-spine-plugin/scripts/check-codex-install.js +226 -0
  24. package/agent-spine-plugin/scripts/check-hosts.js +14 -5
  25. package/agent-spine-plugin/scripts/check-install-hook.js +226 -0
  26. package/agent-spine-plugin/scripts/check-install-selfstarter.js +154 -0
  27. package/agent-spine-plugin/scripts/check-install.js +478 -0
  28. package/agent-spine-plugin/scripts/check-line-budget.js +58 -0
  29. package/agent-spine-plugin/scripts/check-syntax.js +29 -0
  30. package/agent-spine-plugin/scripts/github-actions.js +11 -0
  31. package/agent-spine-plugin/scripts/hermetic-process.js +183 -0
  32. package/agent-spine-plugin/scripts/release-check.js +145 -0
  33. package/agent-spine-plugin/scripts/run-acceptance.js +19 -0
  34. package/agent-spine-plugin/scripts/run-checks.js +47 -0
  35. package/agent-spine-plugin/scripts/run-tests-hermetic.js +89 -0
  36. package/agent-spine-plugin/skills/agent-spine/SKILL.md +16 -2
  37. package/agent-spine-plugin/spine-example/1-identity.md +12 -0
  38. package/agent-spine-plugin/spine-example/2-voice.md +6 -0
  39. package/agent-spine-plugin/spine-example/3-conduct.md +8 -0
  40. package/agent-spine-plugin/spine-example/4-history.md +4 -0
  41. package/agent-spine-plugin/src/cli-agent.js +296 -0
  42. package/agent-spine-plugin/src/cli-attention.js +95 -0
  43. package/agent-spine-plugin/src/cli-autonomy.js +36 -0
  44. package/agent-spine-plugin/src/cli-common.js +71 -0
  45. package/agent-spine-plugin/src/cli-continuity.js +116 -0
  46. package/agent-spine-plugin/src/cli-core.js +128 -0
  47. package/agent-spine-plugin/src/cli-diagnostics.js +291 -0
  48. package/agent-spine-plugin/src/cli-host.js +21 -0
  49. package/agent-spine-plugin/src/cli-learning.js +305 -0
  50. package/agent-spine-plugin/src/cli-premortem.js +16 -0
  51. package/agent-spine-plugin/src/cli-sharing.js +230 -0
  52. package/agent-spine-plugin/src/cli.js +40 -1350
  53. package/agent-spine-plugin/src/codex-reader-launcher.js +182 -0
  54. package/agent-spine-plugin/src/hook.js +185 -567
  55. package/agent-spine-plugin/src/index.js +16 -2
  56. package/agent-spine-plugin/src/lib/acceptance.js +113 -11
  57. package/agent-spine-plugin/src/lib/action-lesson-recall.js +53 -0
  58. package/agent-spine-plugin/src/lib/attention-context.js +167 -0
  59. package/agent-spine-plugin/src/lib/attention-events.js +113 -0
  60. package/agent-spine-plugin/src/lib/attention-privacy.js +72 -0
  61. package/agent-spine-plugin/src/lib/attention-schema.js +164 -0
  62. package/agent-spine-plugin/src/lib/attention-storage.js +93 -0
  63. package/agent-spine-plugin/src/lib/attention.js +9 -600
  64. package/agent-spine-plugin/src/lib/audit-premortem.js +223 -0
  65. package/agent-spine-plugin/src/lib/audit.js +56 -6
  66. package/agent-spine-plugin/src/lib/autonomy-policy.js +110 -0
  67. package/agent-spine-plugin/src/lib/autonomy-store.js +202 -0
  68. package/agent-spine-plugin/src/lib/autonomy.js +8 -0
  69. package/agent-spine-plugin/src/lib/briefing.js +67 -4
  70. package/agent-spine-plugin/src/lib/catalog-document-read.js +51 -0
  71. package/agent-spine-plugin/src/lib/catalog.js +1 -1
  72. package/agent-spine-plugin/src/lib/codex-installation.js +231 -0
  73. package/agent-spine-plugin/src/lib/codex-skill-installation.js +211 -0
  74. package/agent-spine-plugin/src/lib/context.js +3 -1
  75. package/agent-spine-plugin/src/lib/delivery-agent-usage.js +224 -0
  76. package/agent-spine-plugin/src/lib/delivery-assignment.js +220 -0
  77. package/agent-spine-plugin/src/lib/delivery-command-actions.js +453 -0
  78. package/agent-spine-plugin/src/lib/delivery-knowledge.js +78 -0
  79. package/agent-spine-plugin/src/lib/delivery-premortem-binding.js +107 -0
  80. package/agent-spine-plugin/src/lib/delivery-premortem-closure.js +171 -0
  81. package/agent-spine-plugin/src/lib/delivery-premortem-codec.js +45 -0
  82. package/agent-spine-plugin/src/lib/delivery-premortem-correction.js +101 -0
  83. package/agent-spine-plugin/src/lib/delivery-premortem-file.js +21 -0
  84. package/agent-spine-plugin/src/lib/delivery-premortem-index.js +493 -0
  85. package/agent-spine-plugin/src/lib/delivery-premortem-inspection.js +37 -0
  86. package/agent-spine-plugin/src/lib/delivery-premortem-recovery.js +120 -0
  87. package/agent-spine-plugin/src/lib/delivery-premortem-rejection.js +65 -0
  88. package/agent-spine-plugin/src/lib/delivery-premortem-results.js +28 -0
  89. package/agent-spine-plugin/src/lib/delivery-premortem-session-guard.js +46 -0
  90. package/agent-spine-plugin/src/lib/delivery-premortem-write-ledger.js +285 -0
  91. package/agent-spine-plugin/src/lib/delivery-premortem.js +499 -0
  92. package/agent-spine-plugin/src/lib/delivery-shell-heredoc.js +115 -0
  93. package/agent-spine-plugin/src/lib/delivery-shell-substitutions.js +111 -0
  94. package/agent-spine-plugin/src/lib/delivery-shell-wrapper.js +64 -0
  95. package/agent-spine-plugin/src/lib/delivery-target.js +73 -0
  96. package/agent-spine-plugin/src/lib/delivery-verification.js +445 -0
  97. package/agent-spine-plugin/src/lib/documents.js +27 -5
  98. package/agent-spine-plugin/src/lib/filesystem-retry.js +2 -0
  99. package/agent-spine-plugin/src/lib/gateway-common.js +68 -0
  100. package/agent-spine-plugin/src/lib/gateway-control.js +302 -0
  101. package/agent-spine-plugin/src/lib/gateway-delivery.js +103 -0
  102. package/agent-spine-plugin/src/lib/gateway-execution.js +350 -0
  103. package/agent-spine-plugin/src/lib/gateway-host-lifecycle.js +185 -0
  104. package/agent-spine-plugin/src/lib/gateway-inspection.js +85 -0
  105. package/agent-spine-plugin/src/lib/gateway-knowledge.js +129 -0
  106. package/agent-spine-plugin/src/lib/gateway-policy-provenance.js +197 -0
  107. package/agent-spine-plugin/src/lib/gateway-premortem-disposition.js +82 -0
  108. package/agent-spine-plugin/src/lib/gateway-premortem.js +363 -0
  109. package/agent-spine-plugin/src/lib/gateway-runs.js +300 -0
  110. package/agent-spine-plugin/src/lib/gateway-runtime-identity.js +31 -0
  111. package/agent-spine-plugin/src/lib/gateway-runtime-records.js +21 -0
  112. package/agent-spine-plugin/src/lib/gateway-runtime.js +18 -1623
  113. package/agent-spine-plugin/src/lib/gateway-state-transaction.js +356 -0
  114. package/agent-spine-plugin/src/lib/gateway-state.js +342 -0
  115. package/agent-spine-plugin/src/lib/hook-artifact-guards.js +356 -0
  116. package/agent-spine-plugin/src/lib/hook-audit.js +16 -2
  117. package/agent-spine-plugin/src/lib/hook-briefing-use.js +68 -0
  118. package/agent-spine-plugin/src/lib/hook-context.js +424 -0
  119. package/agent-spine-plugin/src/lib/hook-final-message.js +26 -0
  120. package/agent-spine-plugin/src/lib/hook-input.js +27 -0
  121. package/agent-spine-plugin/src/lib/hook-output.js +140 -0
  122. package/agent-spine-plugin/src/lib/hook-premortem.js +257 -0
  123. package/agent-spine-plugin/src/lib/hook-process-advisory.js +37 -0
  124. package/agent-spine-plugin/src/lib/hook-protection.js +95 -0
  125. package/agent-spine-plugin/src/lib/hook-stop-verification.js +84 -0
  126. package/agent-spine-plugin/src/lib/hook-timeline.js +91 -0
  127. package/agent-spine-plugin/src/lib/identifier-analysis.js +446 -0
  128. package/agent-spine-plugin/src/lib/indexed-memory.js +23 -6
  129. package/agent-spine-plugin/src/lib/knowledge-evidence.js +431 -0
  130. package/agent-spine-plugin/src/lib/learning-applications.js +441 -0
  131. package/agent-spine-plugin/src/lib/learning-candidates.js +274 -0
  132. package/agent-spine-plugin/src/lib/learning-context.js +130 -0
  133. package/agent-spine-plugin/src/lib/learning-delivery-contracts.js +310 -0
  134. package/agent-spine-plugin/src/lib/learning-evaluation-contracts.js +350 -0
  135. package/agent-spine-plugin/src/lib/learning-evaluation-registration.js +372 -0
  136. package/agent-spine-plugin/src/lib/learning-evaluation-revocation.js +292 -0
  137. package/agent-spine-plugin/src/lib/learning-evidence-contracts.js +321 -0
  138. package/agent-spine-plugin/src/lib/learning-findings.js +458 -0
  139. package/agent-spine-plugin/src/lib/learning-measurement-contracts.js +285 -0
  140. package/agent-spine-plugin/src/lib/learning-measurements.js +265 -0
  141. package/agent-spine-plugin/src/lib/learning-outcome-contracts.js +165 -0
  142. package/agent-spine-plugin/src/lib/learning-outcomes.js +343 -0
  143. package/agent-spine-plugin/src/lib/learning-reconciliation.js +290 -0
  144. package/agent-spine-plugin/src/lib/learning-retry-contracts.js +188 -0
  145. package/agent-spine-plugin/src/lib/learning-schema.js +229 -0
  146. package/agent-spine-plugin/src/lib/learning-scope-targets.js +424 -0
  147. package/agent-spine-plugin/src/lib/learning-state-upgrade.js +477 -0
  148. package/agent-spine-plugin/src/lib/learning-status-configuration.js +476 -0
  149. package/agent-spine-plugin/src/lib/learning-storage.js +203 -0
  150. package/agent-spine-plugin/src/lib/learning-trial-recovery.js +218 -0
  151. package/agent-spine-plugin/src/lib/learning-validation-contracts.js +375 -0
  152. package/agent-spine-plugin/src/lib/learning-validation-renewal.js +298 -0
  153. package/agent-spine-plugin/src/lib/learning-validation-runtime.js +234 -0
  154. package/agent-spine-plugin/src/lib/learning.js +36 -6923
  155. package/agent-spine-plugin/src/lib/lesson-recall-session.js +172 -0
  156. package/agent-spine-plugin/src/lib/mcp-autonomy-tools.js +37 -0
  157. package/agent-spine-plugin/src/lib/mcp-delivery-completion.js +124 -0
  158. package/agent-spine-plugin/src/lib/mcp-delivery-tools.js +45 -0
  159. package/agent-spine-plugin/src/lib/mcp-premortem.js +68 -0
  160. package/agent-spine-plugin/src/lib/mcp-runtime.js +269 -0
  161. package/agent-spine-plugin/src/lib/mcp-source-context.js +51 -0
  162. package/agent-spine-plugin/src/lib/mcp-timeline-tools.js +164 -0
  163. package/agent-spine-plugin/src/lib/mcp-world-tools.js +52 -0
  164. package/agent-spine-plugin/src/lib/owned-file-lock.js +30 -10
  165. package/agent-spine-plugin/src/lib/project-portfolio.js +176 -0
  166. package/agent-spine-plugin/src/lib/selfstarter-core.js +382 -0
  167. package/agent-spine-plugin/src/lib/selfstarter-jobs.js +149 -0
  168. package/agent-spine-plugin/src/lib/selfstarter-lease.js +204 -0
  169. package/agent-spine-plugin/src/lib/selfstarter-policy.js +90 -0
  170. package/agent-spine-plugin/src/lib/selfstarter-workspace.js +111 -0
  171. package/agent-spine-plugin/src/lib/selfstarter.js +11 -911
  172. package/agent-spine-plugin/src/lib/session-timeline-auth.js +316 -0
  173. package/agent-spine-plugin/src/lib/session-timeline-codex.js +58 -0
  174. package/agent-spine-plugin/src/lib/session-timeline-contract.js +48 -0
  175. package/agent-spine-plugin/src/lib/session-timeline-enrollment-source.js +41 -0
  176. package/agent-spine-plugin/src/lib/session-timeline-enrollment-storage.js +132 -0
  177. package/agent-spine-plugin/src/lib/session-timeline-enrollment-transport.js +17 -0
  178. package/agent-spine-plugin/src/lib/session-timeline-enrollment.js +500 -0
  179. package/agent-spine-plugin/src/lib/session-timeline-event-extract.js +157 -0
  180. package/agent-spine-plugin/src/lib/session-timeline-host-origin.js +74 -0
  181. package/agent-spine-plugin/src/lib/session-timeline-host-receipt.js +117 -0
  182. package/agent-spine-plugin/src/lib/session-timeline-invocation.js +201 -0
  183. package/agent-spine-plugin/src/lib/session-timeline-king.js +79 -0
  184. package/agent-spine-plugin/src/lib/session-timeline-prior.js +59 -0
  185. package/agent-spine-plugin/src/lib/session-timeline-provider.js +34 -0
  186. package/agent-spine-plugin/src/lib/session-timeline-query.js +55 -0
  187. package/agent-spine-plugin/src/lib/session-timeline-results.js +31 -0
  188. package/agent-spine-plugin/src/lib/session-timeline-root.js +11 -0
  189. package/agent-spine-plugin/src/lib/session-timeline-search.js +82 -0
  190. package/agent-spine-plugin/src/lib/session-timeline-sid-acl.js +217 -0
  191. package/agent-spine-plugin/src/lib/session-timeline-source.js +83 -0
  192. package/agent-spine-plugin/src/lib/session-timeline-state.js +45 -0
  193. package/agent-spine-plugin/src/lib/session-timeline-transport.js +50 -0
  194. package/agent-spine-plugin/src/lib/session-timeline-windows-acl.js +148 -0
  195. package/agent-spine-plugin/src/lib/session-timeline.js +453 -0
  196. package/agent-spine-plugin/src/lib/source-roots.js +69 -136
  197. package/agent-spine-plugin/src/lib/source-tree-scan.js +178 -0
  198. package/agent-spine-plugin/src/lib/task-knowledge-context.js +78 -0
  199. package/agent-spine-plugin/src/lib/timeline-tool-guard.js +202 -0
  200. package/agent-spine-plugin/src/lib/world-knowledge.js +249 -0
  201. package/agent-spine-plugin/src/lib/world-model.js +278 -0
  202. package/agent-spine-plugin/src/mcp.js +20 -161
  203. package/agent-spine-plugin/src/version.js +1 -1
  204. package/agent-spine-plugin/src/worker.js +22 -5
  205. package/bin/agent-resume-snapshot.cjs +2 -2
  206. package/bin/agentspine-king-goal-inbox.mjs +111 -0
  207. package/bin/agentspine-king-goal-intake.mjs +106 -0
  208. package/bin/baseline-skill-performance-policy.cjs +1 -16
  209. package/bin/core-bootstrap.js +3 -0
  210. package/bin/curiosity-scout-policy.cjs +5 -1
  211. package/bin/input-draft-persistence.cjs +2 -2
  212. package/bin/king-tui-function-contract.json +33 -0
  213. package/bin/launcher-restart-policy.cjs +150 -0
  214. package/bin/launcher-runtime.js +56 -44
  215. package/bin/managed-context-startup-policy.cjs +27 -0
  216. package/bin/managed-plugin-selection.cjs +116 -0
  217. package/bin/mistake-relevance-policy.cjs +1 -1
  218. package/bin/observer-hooks.cjs +14 -0
  219. package/bin/oversized-context-offload-policy.cjs +86 -0
  220. package/bin/plugin-bootstrap.js +7 -40
  221. package/bin/proactive-compaction-policy.cjs +1 -1
  222. package/bin/provider-model-refresh-deadline.cjs +53 -0
  223. package/bin/provider-model-refresh-policy.cjs +107 -0
  224. package/bin/release-artifact-freeze-policy.cjs +30 -0
  225. package/bin/repeated-user-message-projection.cjs +3 -126
  226. package/bin/research-page-result.cjs +74 -0
  227. package/bin/runtime-exit-ledger.cjs +1 -0
  228. package/bin/session-compaction-policy.cjs +84 -0
  229. package/bin/skill-listing-performance-policy.cjs +2 -2
  230. package/bin/standard-tools-bootstrap.js +0 -37
  231. package/bin/subagent-skill-policy.cjs +3 -1
  232. package/bin/telegram-approval-relay.cjs +12 -7
  233. package/bin/telegram-private-conversation-policy.cjs +3 -2
  234. package/bin/telegram-queue-handoff-policy.cjs +24 -0
  235. package/bin/thinking-activity-status-policy.cjs +1 -1
  236. package/bin/thinking-only-guard.cjs +16 -12
  237. package/bin/tool-call-loop-policy.cjs +0 -2
  238. package/bin/tool-result-offload-policy.cjs +11 -2
  239. package/bin/tui-functional-contract.cjs +55 -0
  240. package/bin/update-notice.js +18 -14
  241. package/bin/user-message-offload-policy.cjs +1 -1
  242. package/bin/user-prompt-hook-origin-policy.cjs +34 -0
  243. package/bin/windows-node-crash-dump.cjs +110 -0
  244. package/blun.mjs +2861 -1173
  245. package/codebase-index/codebase_index.py +4 -3
  246. package/package.json +5 -18
  247. package/standard-skills/research-evidence/SKILL.md +39 -0
  248. package/standard-skills/research-evidence/references/evidence-format.md +82 -0
  249. package/standard-skills/research-evidence/scripts/evidence-collection.cjs +254 -0
  250. package/standard-skills/research-evidence/scripts/score-report.cjs +112 -0
  251. package/standard-skills/web-lesen/SKILL.md +37 -22
  252. package/standard-skills/web-lesen/scripts/crawl_public.py +376 -0
  253. package/telegram-plugin/DELIVERY.md +36 -0
  254. package/telegram-plugin/bin/telegram-approval-relay.cjs +13 -7
  255. package/telegram-plugin/bin/telegram-launcher-status-queue.cjs +122 -0
  256. package/telegram-plugin/bin/telegram-private-conversation-policy.cjs +3 -2
  257. package/telegram-plugin/bin/telegram-reply-parts.cjs +149 -0
  258. package/telegram-plugin/dist/bridge.mjs +7 -56
  259. package/telegram-plugin/dist/mcp-server.mjs +33 -4
  260. package/agent-spine-plugin/skill/SKILL.md +0 -76
  261. package/bin/mnemo-connect-heartbeat.cjs +0 -204
  262. package/bin/mnemo-tool-agent-policy.cjs +0 -22
  263. package/telegram-plugin/bin/telegram-mnemo-capture.cjs +0 -297
@@ -0,0 +1,285 @@
1
+ import {
2
+ ID_RE, DIGEST_RE, STALENESS_BOUND_EVALUATIONS, EVIDENCE_SOURCE_ATTESTED_EVALUATIONS, EVIDENCE_SOURCE_ATTESTATION_REVOCATION_REASONS
3
+ } from "./learning-schema.js";
4
+ import {
5
+ digest, learningTargetForCandidate
6
+ } from "./learning-scope-targets.js";
7
+
8
+ export function measurementLineagePayload({ measurementReceiptId, learningId, evaluationId, sourceDigest, runDigest,
9
+ evaluatorRootDigest, rootRunDigest, registeredAt,
10
+ schema = "agentspine.learning-measurement-lineage/v1" }) {
11
+ return {
12
+ schema, measurementReceiptId, learningId, evaluationId,
13
+ sourceDigest, runDigest,
14
+ ...(schema === "agentspine.learning-measurement-lineage/v2" ? { evaluatorRootDigest, rootRunDigest } : {}),
15
+ registeredAt, authority: "context-only"
16
+ };
17
+ }
18
+
19
+ export function storedMeasurementLineageStructure(receipt) {
20
+ if (!receipt || typeof receipt !== "object" || Array.isArray(receipt)) return false;
21
+ const payload = measurementLineagePayload(receipt);
22
+ return ["agentspine.learning-measurement-lineage/v1", "agentspine.learning-measurement-lineage/v2"].includes(receipt.schema)
23
+ && ID_RE.test(receipt.measurementReceiptId || "") && ID_RE.test(receipt.learningId || "")
24
+ && ID_RE.test(receipt.evaluationId || "") && DIGEST_RE.test(receipt.sourceDigest || "")
25
+ && DIGEST_RE.test(receipt.runDigest || "") && Number.isFinite(new Date(receipt.registeredAt).getTime())
26
+ && (receipt.schema !== "agentspine.learning-measurement-lineage/v2"
27
+ || (DIGEST_RE.test(receipt.evaluatorRootDigest || "") && DIGEST_RE.test(receipt.rootRunDigest || "")))
28
+ && receipt.authority === "context-only" && receipt.digest === digest(payload);
29
+ }
30
+
31
+ export function initialTrialPayload({ evaluationId, learningId, scope, metric, benchmark, phase, slot,
32
+ evaluatorId, evaluatorRootDigest, runId, caseCount }) {
33
+ return {
34
+ schema: "agentspine.learning-initial-trial/v1",
35
+ evaluationId,
36
+ learningId,
37
+ scopeDigest: digest(scope),
38
+ metricDigest: digest(metric),
39
+ benchmarkDigest: digest(benchmark),
40
+ phase,
41
+ slot,
42
+ evaluatorId,
43
+ evaluatorRootDigest,
44
+ runId,
45
+ caseCount,
46
+ authority: "context-only"
47
+ };
48
+ }
49
+
50
+ export function initialTrialDigest(input) {
51
+ return digest(initialTrialPayload(input));
52
+ }
53
+
54
+ export function storedInitialTrialPlanStructure(plan, contract) {
55
+ if (!plan || typeof plan !== "object" || Array.isArray(plan)
56
+ || plan.schema !== "agentspine.learning-initial-trials/v1"
57
+ || plan.mode !== "first-admitted-trials"
58
+ || !Number.isInteger(plan.requiredTrials) || plan.requiredTrials < 2 || plan.requiredTrials > 10
59
+ || plan.benchmarkDigest !== digest(contract.benchmark)
60
+ || plan.authority !== "context-only") return false;
61
+ const validPhase = (phase) => Array.isArray(plan[phase]) && plan[phase].length === plan.requiredTrials
62
+ && plan[phase].every((entry, index) => entry?.slot === index + 1
63
+ && ID_RE.test(entry?.evaluatorId || "") && DIGEST_RE.test(entry?.evaluatorRootDigest || "")
64
+ && ID_RE.test(entry?.runId || "") && Number.isInteger(entry?.caseCount)
65
+ && entry.caseCount === contract.benchmark.minCases && DIGEST_RE.test(entry?.trialDigest || "")
66
+ && entry.authority === "context-only"
67
+ && entry.trialDigest === initialTrialDigest({
68
+ evaluationId: contract.id, learningId: contract.learningId, scope: contract.scope,
69
+ metric: contract.metric, benchmark: contract.benchmark, phase, slot: entry.slot,
70
+ evaluatorId: entry.evaluatorId, evaluatorRootDigest: entry.evaluatorRootDigest,
71
+ runId: entry.runId, caseCount: entry.caseCount
72
+ }))
73
+ && new Set(plan[phase].map((entry) => entry.evaluatorId)).size === plan[phase].length
74
+ && new Set(plan[phase].map((entry) => entry.evaluatorRootDigest)).size === plan[phase].length
75
+ && new Set(plan[phase].map((entry) => entry.runId)).size === plan[phase].length;
76
+ return validPhase("before") && validPhase("after")
77
+ && plan.before.every((entry, index) => entry.evaluatorId === plan.after[index].evaluatorId
78
+ && entry.evaluatorRootDigest === plan.after[index].evaluatorRootDigest
79
+ && entry.runId !== plan.after[index].runId)
80
+ && plan.before.every((entry) => contract.evaluatorRoots.some((root) => root.evaluatorId === entry.evaluatorId
81
+ && root.principalDigest === entry.evaluatorRootDigest));
82
+ }
83
+
84
+ export function completionPolicyPayload({ deliveryTimeoutMs, outcomeTimeoutMs }) {
85
+ return {
86
+ schema: "agentspine.learning-completion-policy/v1",
87
+ deliveryTimeoutMs,
88
+ outcomeTimeoutMs,
89
+ missingDelivery: "blocking-defect",
90
+ missingOutcome: "blocking-defect",
91
+ authority: "context-only"
92
+ };
93
+ }
94
+
95
+ export function storedCompletionPolicyStructure(policy) {
96
+ if (!policy || typeof policy !== "object" || Array.isArray(policy)) return false;
97
+ const payload = completionPolicyPayload(policy);
98
+ return policy.schema === "agentspine.learning-completion-policy/v1"
99
+ && Number.isInteger(policy.deliveryTimeoutMs) && policy.deliveryTimeoutMs >= 60_000
100
+ && policy.deliveryTimeoutMs <= 15 * 60_000
101
+ && Number.isInteger(policy.outcomeTimeoutMs) && policy.outcomeTimeoutMs >= policy.deliveryTimeoutMs
102
+ && policy.outcomeTimeoutMs <= 7 * 86400000
103
+ && policy.missingDelivery === "blocking-defect" && policy.missingOutcome === "blocking-defect"
104
+ && policy.authority === "context-only" && policy.digest === digest(payload);
105
+ }
106
+
107
+ export function stalenessPolicyPayload({ outcomeMaxAgeDays, canaryTtlDays }) {
108
+ return {
109
+ schema: "agentspine.learning-staleness-policy/v1",
110
+ outcomeMaxAgeDays,
111
+ canaryTtlDays,
112
+ staleOutcome: "ineligible",
113
+ expiredCanary: "automatic-rollback",
114
+ authority: "context-only"
115
+ };
116
+ }
117
+
118
+ export function storedStalenessPolicyStructure(policy) {
119
+ if (!policy || typeof policy !== "object" || Array.isArray(policy)) return false;
120
+ const payload = stalenessPolicyPayload(policy);
121
+ return policy.schema === "agentspine.learning-staleness-policy/v1"
122
+ && Number.isInteger(policy.outcomeMaxAgeDays) && policy.outcomeMaxAgeDays >= 1
123
+ && policy.outcomeMaxAgeDays <= 365
124
+ && Number.isInteger(policy.canaryTtlDays) && policy.canaryTtlDays >= 1
125
+ && policy.canaryTtlDays <= 90
126
+ && policy.staleOutcome === "ineligible" && policy.expiredCanary === "automatic-rollback"
127
+ && policy.authority === "context-only" && policy.digest === digest(payload);
128
+ }
129
+
130
+ export function evaluationStalenessPolicy(contract, config) {
131
+ return STALENESS_BOUND_EVALUATIONS.has(contract?.schema)
132
+ ? contract.stalenessPolicy
133
+ : { outcomeMaxAgeDays: config.outcomeMaxAgeDays, canaryTtlDays: config.canaryTtlDays };
134
+ }
135
+
136
+ export function blockingDefectPolicyPayload() {
137
+ return {
138
+ schema: "agentspine.learning-blocking-defect-policy/v1",
139
+ phases: ["before", "after", "revalidation"],
140
+ aggregation: "any-defect-overrides-average",
141
+ beforeAction: "block-canary-admission",
142
+ afterAction: "automatic-rollback",
143
+ authority: "context-only"
144
+ };
145
+ }
146
+
147
+ export function storedBlockingDefectPolicyStructure(policy) {
148
+ if (!policy || typeof policy !== "object" || Array.isArray(policy)) return false;
149
+ const payload = blockingDefectPolicyPayload();
150
+ return Object.keys(policy).length === 7
151
+ && JSON.stringify(policy.phases) === JSON.stringify(payload.phases)
152
+ && policy.schema === payload.schema && policy.aggregation === payload.aggregation
153
+ && policy.beforeAction === payload.beforeAction && policy.afterAction === payload.afterAction
154
+ && policy.authority === "context-only" && policy.digest === digest(payload);
155
+ }
156
+
157
+ export function evidenceSourcePolicyPayload(schema = "agentspine.learning-evidence-source-policy/v1") {
158
+ return {
159
+ schema,
160
+ qualifyingTypes: ["user-statement", "test"],
161
+ minimumQualifyingEvidence: 1,
162
+ documentOnly: "insufficient",
163
+ interactionOnly: "insufficient",
164
+ ...(schema === "agentspine.learning-evidence-source-policy/v2"
165
+ ? { qualifyingEvidence: "explicit-local-attestation-required" } : {}),
166
+ insufficientCohort: "reject-before-evaluation",
167
+ authority: "context-only"
168
+ };
169
+ }
170
+
171
+ export function storedEvidenceSourcePolicyStructure(policy) {
172
+ if (!policy || typeof policy !== "object" || Array.isArray(policy)) return false;
173
+ const payload = evidenceSourcePolicyPayload(policy.schema);
174
+ const attested = policy.schema === "agentspine.learning-evidence-source-policy/v2";
175
+ return ["agentspine.learning-evidence-source-policy/v1",
176
+ "agentspine.learning-evidence-source-policy/v2"].includes(policy.schema)
177
+ && Object.keys(policy).length === (attested ? 9 : 8)
178
+ && JSON.stringify(policy.qualifyingTypes) === JSON.stringify(payload.qualifyingTypes)
179
+ && policy.minimumQualifyingEvidence === payload.minimumQualifyingEvidence
180
+ && policy.documentOnly === payload.documentOnly && policy.interactionOnly === payload.interactionOnly
181
+ && (!attested || policy.qualifyingEvidence === payload.qualifyingEvidence)
182
+ && policy.insufficientCohort === payload.insufficientCohort
183
+ && policy.authority === "context-only" && policy.digest === digest(payload);
184
+ }
185
+
186
+ export function qualifyingEvidenceCount(cohort, policy, attestations = []) {
187
+ if (policy.schema === "agentspine.learning-evidence-source-policy/v2") {
188
+ return new Set(attestations.map((entry) => entry.independenceDigest)).size;
189
+ }
190
+ return new Set(cohort.filter((entry) => policy.qualifyingTypes.includes(entry.type))
191
+ .map((entry) => entry.independenceDigest)).size;
192
+ }
193
+
194
+ export function evidenceSourceAttestations(cohort, confirmedAt) {
195
+ return cohort.filter((entry) => ["user-statement", "test"].includes(entry.type)).map((entry) => ({
196
+ schema: "agentspine.learning-evidence-source-attestation/v1",
197
+ evidenceDigest: entry.evidenceDigest,
198
+ independenceDigest: entry.independenceDigest,
199
+ sourceClass: entry.type === "test" ? "objective-test" : "explicit-user-feedback",
200
+ confirmedAt,
201
+ authority: "context-only"
202
+ })).sort((left, right) => left.evidenceDigest.localeCompare(right.evidenceDigest));
203
+ }
204
+
205
+ export function storedEvidenceSourceAttestationsStructure(attestations, cohort, confirmedAt) {
206
+ const expected = evidenceSourceAttestations(cohort, confirmedAt);
207
+ return Array.isArray(attestations) && attestations.length >= 1
208
+ && attestations.every((entry) => entry && typeof entry === "object" && !Array.isArray(entry)
209
+ && Object.keys(entry).length === 6
210
+ && entry.schema === "agentspine.learning-evidence-source-attestation/v1"
211
+ && DIGEST_RE.test(entry.evidenceDigest || "") && DIGEST_RE.test(entry.independenceDigest || "")
212
+ && ["explicit-user-feedback", "objective-test"].includes(entry.sourceClass)
213
+ && entry.confirmedAt === confirmedAt && entry.authority === "context-only")
214
+ && digest(attestations) === digest(expected);
215
+ }
216
+
217
+ export function evidenceSourceAttestationRevocationPayload({ id, learningId, evaluationId, evaluationDigest,
218
+ candidateAdmissionDigest, evidenceDigest, independenceDigest, sourceClass, targetDigest, scopeDigest,
219
+ reasonCode, reasonDigest, revokedAt }) {
220
+ return {
221
+ schema: "agentspine.learning-evidence-source-attestation-revocation/v1",
222
+ id,
223
+ learningId,
224
+ evaluationId,
225
+ evaluationDigest,
226
+ candidateAdmissionDigest,
227
+ evidenceDigest,
228
+ independenceDigest,
229
+ sourceClass,
230
+ targetDigest,
231
+ scopeDigest,
232
+ reasonCode,
233
+ reasonDigest,
234
+ revokedAt,
235
+ authority: "context-only"
236
+ };
237
+ }
238
+
239
+ export function storedEvidenceSourceAttestationRevocationStructure(receipt) {
240
+ if (!receipt || typeof receipt !== "object" || Array.isArray(receipt)) return false;
241
+ const payload = evidenceSourceAttestationRevocationPayload(receipt);
242
+ return receipt.schema === "agentspine.learning-evidence-source-attestation-revocation/v1"
243
+ && Object.keys(receipt).length === 16
244
+ && Object.keys(receipt).every((field) => ["schema", "id", "learningId", "evaluationId",
245
+ "evaluationDigest", "candidateAdmissionDigest", "evidenceDigest", "independenceDigest",
246
+ "sourceClass", "targetDigest", "scopeDigest", "reasonCode", "reasonDigest", "revokedAt",
247
+ "authority", "digest"].includes(field))
248
+ && ID_RE.test(receipt.id || "") && ID_RE.test(receipt.learningId || "")
249
+ && ID_RE.test(receipt.evaluationId || "") && DIGEST_RE.test(receipt.evaluationDigest || "")
250
+ && DIGEST_RE.test(receipt.candidateAdmissionDigest || "") && DIGEST_RE.test(receipt.evidenceDigest || "")
251
+ && DIGEST_RE.test(receipt.independenceDigest || "")
252
+ && ["explicit-user-feedback", "objective-test"].includes(receipt.sourceClass)
253
+ && DIGEST_RE.test(receipt.targetDigest || "") && DIGEST_RE.test(receipt.scopeDigest || "")
254
+ && EVIDENCE_SOURCE_ATTESTATION_REVOCATION_REASONS.has(receipt.reasonCode)
255
+ && DIGEST_RE.test(receipt.reasonDigest || "") && Number.isFinite(new Date(receipt.revokedAt).getTime())
256
+ && receipt.authority === "context-only" && receipt.digest === digest(payload);
257
+ }
258
+
259
+ export function evidenceSourceAttestationRevocationMatchesState(state, receipt) {
260
+ const candidate = state.candidates.find((entry) => entry.id === receipt.learningId);
261
+ const evaluation = state.evaluations.find((entry) => entry.id === receipt.evaluationId
262
+ && entry.learningId === receipt.learningId && entry.digest === receipt.evaluationDigest);
263
+ const attestation = evaluation?.candidateAdmission?.evidenceSourceAttestations?.find((entry) =>
264
+ entry.evidenceDigest === receipt.evidenceDigest && entry.independenceDigest === receipt.independenceDigest
265
+ && entry.sourceClass === receipt.sourceClass);
266
+ return Boolean(candidate && evaluation && EVIDENCE_SOURCE_ATTESTED_EVALUATIONS.has(evaluation.schema)
267
+ && evaluation.candidateAdmission.digest === receipt.candidateAdmissionDigest
268
+ && attestation && learningTargetForCandidate(candidate).digest === receipt.targetDigest
269
+ && digest(evaluation.scope) === receipt.scopeDigest);
270
+ }
271
+
272
+ export function revokedEvidenceSourceAttestation(state, evaluationId, evidenceDigest = null) {
273
+ return state.evidenceSourceAttestationRevocations.find((receipt) => receipt.evaluationId === evaluationId
274
+ && (evidenceDigest === null || receipt.evidenceDigest === evidenceDigest)) || null;
275
+ }
276
+
277
+ export function revokedEvidenceSourceAttestationForCandidate(state, candidate) {
278
+ const canary = candidate?.promotion?.mode === "outcome-canary" ? candidate.promotion.canary : null;
279
+ if (canary?.evaluationId) {
280
+ return state.evidenceSourceAttestationRevocations.find((receipt) => receipt.learningId === candidate.id
281
+ && receipt.evaluationId === canary.evaluationId && receipt.evaluationDigest === canary.evaluationDigest) || null;
282
+ }
283
+ return state.evidenceSourceAttestationRevocations.find((receipt) =>
284
+ receipt.learningId === candidate?.id) || null;
285
+ }
@@ -0,0 +1,265 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import {
3
+ OUTCOME_PHASES, MEASUREMENT_KINDS, ID_RE, DIGEST_RE, LINEAGE_EVALUATIONS, PAIRED_EVALUATIONS,
4
+ ROOT_BOUND_EVALUATIONS, REGISTRY_BOUND_EVALUATIONS, INITIAL_TRIAL_EVALUATIONS, TARGET_BOUND_EVALUATIONS, INITIAL_TRIAL_APPLICATIONS, TARGET_BOUND_APPLICATIONS,
5
+ DEADLINE_BOUND_APPLICATIONS, MEASUREMENT_REVOCATION_REASONS
6
+ } from "./learning-schema.js";
7
+ import {
8
+ normalizeScope, scopeContains, exactScope, digest, learningTargetForCandidate, measurementPayload,
9
+ measurementRevocationPayload, revokedMeasurement, measurementRunDigest, evaluatorRootRunDigest, activeEvaluationBinding, revokedEvaluation
10
+ } from "./learning-scope-targets.js";
11
+ import {
12
+ revokedValidationForCandidate
13
+ } from "./learning-validation-runtime.js";
14
+ import {
15
+ measurementLineagePayload, revokedEvidenceSourceAttestation
16
+ } from "./learning-measurement-contracts.js";
17
+ import {
18
+ revokedApplication, revokedDelivery
19
+ } from "./learning-delivery-contracts.js";
20
+ import {
21
+ date, number, integer, safeText, mutation
22
+ } from "./learning-storage.js";
23
+
24
+ export async function recordLearningMeasurement({
25
+ root = process.cwd(), id = `measurement:${randomUUID()}`, learningId, evaluationId, phase, scope, metric,
26
+ measurement, coverage, measuredAt, confirmLocalMeasurement = false, now = new Date()
27
+ }) {
28
+ if (!confirmLocalMeasurement) throw new Error("measurement registration requires explicit local confirmation");
29
+ if (!ID_RE.test(id || "") || !ID_RE.test(learningId || "") || !ID_RE.test(evaluationId || "")) {
30
+ throw new Error("measurement id, learningId and evaluationId must be stable identifiers");
31
+ }
32
+ const timestamp = date(now, "now");
33
+ return mutation(root, (state, _catalog, learningPath) => {
34
+ const candidate = state.candidates.find((entry) => entry.id === learningId);
35
+ const evaluation = state.evaluations.find((entry) => entry.id === evaluationId);
36
+ const existing = state.measurements.find((entry) => entry.id === id);
37
+ if (!candidate || !evaluation || !LINEAGE_EVALUATIONS.has(evaluation.schema)
38
+ || evaluation.learningId !== learningId) {
39
+ throw new Error("measurements require a matching lineage evaluation contract");
40
+ }
41
+ if (revokedEvaluation(state, evaluation.id)) {
42
+ throw new Error("learning evaluation contract was explicitly revoked and cannot accept measurements");
43
+ }
44
+ if (revokedEvidenceSourceAttestation(state, evaluation.id)) {
45
+ throw new Error("learning evidence source attestation was explicitly revoked and cannot support measurements");
46
+ }
47
+ if (REGISTRY_BOUND_EVALUATIONS.has(evaluation.schema)
48
+ && !activeEvaluationBinding(state, evaluation)) {
49
+ throw new Error("measurement evaluator registry binding is missing, changed, or revoked");
50
+ }
51
+ const normalizedScope = normalizeScope(scope);
52
+ if (!scopeContains(candidate.scope, normalizedScope) || !exactScope(evaluation.scope, normalizedScope)) {
53
+ throw new Error("measurement scope does not match the evaluation contract");
54
+ }
55
+ if (!OUTCOME_PHASES.has(phase)) throw new Error("measurement phase must be before or after");
56
+ if (phase === "before" && candidate.status !== "candidate") {
57
+ throw new Error("before measurements require an unreviewed candidate");
58
+ }
59
+ const activeRevalidation = candidate?.promotion?.canary?.status === "validated"
60
+ && candidate.promotion.canary.revalidation?.status === "active"
61
+ && new Date(candidate.promotion.canary.revalidation.expiresAt).getTime() >= new Date(timestamp).getTime();
62
+ if (activeRevalidation && revokedValidationForCandidate(state, candidate)) {
63
+ throw new Error("learning validation decision was explicitly revoked and cannot accept renewal measurements");
64
+ }
65
+ if (phase === "after" && (candidate.status !== "accepted" || candidate.promotion?.mode !== "outcome-canary"
66
+ || (!activeRevalidation && candidate.promotion?.canary?.status !== "active")
67
+ || candidate.promotion.canary.evaluationId !== evaluationId)) {
68
+ throw new Error("after measurements require the matching active outcome canary");
69
+ }
70
+ const name = safeText(metric?.name, "measurement.metric.name", 120);
71
+ const direction = metric?.direction;
72
+ if (name !== evaluation.metric.name || direction !== evaluation.metric.direction) {
73
+ throw new Error("measurement metric does not match the evaluation contract");
74
+ }
75
+ const normalizedMetric = {
76
+ name, direction,
77
+ value: number(metric?.value, "measurement.metric.value", 0, 1),
78
+ blockingDefects: integer(metric?.blockingDefects ?? 0, "measurement.metric.blockingDefects", 0, 1000)
79
+ };
80
+ const kind = measurement?.kind;
81
+ const evaluatorId = measurement?.evaluatorId;
82
+ const runId = measurement?.runId;
83
+ const sourceDigest = measurement?.sourceDigest;
84
+ if (!MEASUREMENT_KINDS.has(kind)) throw new Error("measurement kind is unsupported");
85
+ if (!ID_RE.test(evaluatorId || "") || !evaluation.evaluatorIds.includes(evaluatorId)) {
86
+ throw new Error("measurement evaluator is not allowed by the evaluation contract");
87
+ }
88
+ if (!ID_RE.test(runId || "")) throw new Error("measurement runId must be a stable identifier");
89
+ if (!DIGEST_RE.test(sourceDigest || "")) throw new Error("measurement sourceDigest must be a SHA-256 digest");
90
+ const evaluatorRoot = ROOT_BOUND_EVALUATIONS.has(evaluation.schema)
91
+ ? evaluation.evaluatorRoots.find((root) => root.evaluatorId === evaluatorId) : null;
92
+ if (ROOT_BOUND_EVALUATIONS.has(evaluation.schema) && !evaluatorRoot) {
93
+ throw new Error("measurement evaluator root is not frozen by the evaluation contract");
94
+ }
95
+ const normalizedMeasurement = {
96
+ kind, evaluatorId, runId, sourceDigest,
97
+ ...(evaluatorRoot ? { evaluatorRootDigest: evaluatorRoot.principalDigest } : {}),
98
+ authority: "context-only"
99
+ };
100
+ const normalizedCoverage = {
101
+ datasetDigest: coverage?.datasetDigest,
102
+ caseCount: integer(coverage?.caseCount, "measurement.coverage.caseCount", 1, 1000000),
103
+ authority: "context-only"
104
+ };
105
+ if (normalizedCoverage.datasetDigest !== evaluation.benchmark.datasetDigest) {
106
+ throw new Error("measurement coverage dataset does not match the evaluation contract");
107
+ }
108
+ if (normalizedCoverage.caseCount < evaluation.benchmark.minCases) {
109
+ throw new Error(`measurement coverage requires at least ${evaluation.benchmark.minCases} cases`);
110
+ }
111
+ const initialTrial = INITIAL_TRIAL_EVALUATIONS.has(evaluation.schema) && !activeRevalidation
112
+ ? evaluation.initialTrials?.[phase]?.find((entry) => entry.evaluatorId === evaluatorId) : null;
113
+ if (INITIAL_TRIAL_EVALUATIONS.has(evaluation.schema) && !activeRevalidation
114
+ && (!initialTrial || initialTrial.evaluatorRootDigest !== evaluatorRoot?.principalDigest
115
+ || initialTrial.runId !== runId || initialTrial.caseCount !== normalizedCoverage.caseCount)) {
116
+ throw new Error("measurement does not match the precommitted initial trial");
117
+ }
118
+ const observedAt = date(measuredAt || existing?.measuredAt || timestamp, "measurement.measuredAt");
119
+ if (new Date(observedAt).getTime() < new Date(evaluation.registeredAt).getTime()
120
+ || new Date(observedAt).getTime() > new Date(evaluation.expiresAt).getTime()
121
+ || (!existing && new Date(observedAt).getTime() > new Date(timestamp).getTime())) {
122
+ throw new Error("measurement is outside its evaluation contract window");
123
+ }
124
+ if (INITIAL_TRIAL_EVALUATIONS.has(evaluation.schema) && phase === "after" && !activeRevalidation) {
125
+ const application = state.applications.find((entry) => INITIAL_TRIAL_APPLICATIONS.has(entry.schema)
126
+ && entry.learningId === learningId && entry.initialAdmission.evaluationId === evaluation.id
127
+ && entry.initialAdmission.evaluationDigest === evaluation.digest
128
+ && entry.initialAdmission.slot === initialTrial.slot
129
+ && entry.initialAdmission.trialDigest === initialTrial.trialDigest
130
+ && (!TARGET_BOUND_EVALUATIONS.has(evaluation.schema)
131
+ || (TARGET_BOUND_APPLICATIONS.has(entry.schema)
132
+ && entry.initialAdmission.targetDigest === evaluation.target.digest)));
133
+ const delivery = application && state.deliveries.find((entry) => entry.applicationId === application.id);
134
+ if (!application || revokedApplication(state, application.id) || !delivery || revokedDelivery(state, delivery.id)
135
+ || new Date(observedAt).getTime() < new Date(delivery.completedAt).getTime()) {
136
+ throw new Error("after measurement requires the precommitted first-admitted trial and completed delivery");
137
+ }
138
+ if (DEADLINE_BOUND_APPLICATIONS.has(application.schema)
139
+ && (new Date(observedAt).getTime() > new Date(application.outcomeExpiresAt).getTime()
140
+ || (!existing && new Date(timestamp).getTime() > new Date(application.outcomeExpiresAt).getTime()))) {
141
+ throw new Error("after measurement missed its immutable initial trial outcome deadline");
142
+ }
143
+ }
144
+ if (phase === "after" && activeRevalidation) {
145
+ const application = state.applications.find((entry) => entry.schema === "agentspine.learning-application/v4"
146
+ && entry.learningId === learningId
147
+ && entry.revalidationAdmission.evaluatorId === evaluatorId
148
+ && entry.revalidationAdmission.evaluatorRootDigest === evaluatorRoot?.principalDigest
149
+ && entry.revalidationAdmission.runId === runId);
150
+ const delivery = application && state.deliveries.find((entry) => entry.applicationId === application.id);
151
+ if (!application || revokedApplication(state, application.id) || !delivery || revokedDelivery(state, delivery.id)
152
+ || new Date(observedAt).getTime() < new Date(delivery.completedAt).getTime()) {
153
+ throw new Error("revalidation measurement requires its precommitted admission slot and completed delivery");
154
+ }
155
+ }
156
+ const payload = measurementPayload({
157
+ schema: ROOT_BOUND_EVALUATIONS.has(evaluation.schema)
158
+ ? "agentspine.learning-measurement/v2" : "agentspine.learning-measurement/v1",
159
+ id, learningId, evaluationId, phase, scope: normalizedScope,
160
+ metric: normalizedMetric, measurement: normalizedMeasurement, coverage: normalizedCoverage, measuredAt: observedAt });
161
+ const receipt = { ...payload, digest: digest(payload) };
162
+ const lineagePayload = measurementLineagePayload({
163
+ schema: evaluatorRoot ? "agentspine.learning-measurement-lineage/v2" : "agentspine.learning-measurement-lineage/v1",
164
+ measurementReceiptId: id, learningId, evaluationId, sourceDigest,
165
+ runDigest: measurementRunDigest(evaluatorId, runId),
166
+ evaluatorRootDigest: evaluatorRoot?.principalDigest,
167
+ rootRunDigest: evaluatorRoot ? evaluatorRootRunDigest(evaluatorRoot.principalDigest, runId) : undefined,
168
+ registeredAt: observedAt
169
+ });
170
+ const lineage = { ...lineagePayload, digest: digest(lineagePayload) };
171
+ if (existing) {
172
+ const existingLineage = state.measurementLineage.find((entry) => entry.measurementReceiptId === id);
173
+ if (existing.digest === receipt.digest && existingLineage?.digest === lineage.digest) {
174
+ return { receipt: existing, lineage: existingLineage, learningPath, unchanged: true };
175
+ }
176
+ throw new Error("measurement receipt IDs are immutable");
177
+ }
178
+ if (PAIRED_EVALUATIONS.has(evaluation.schema) && phase === "after") {
179
+ const beforeReceiptIds = new Set(candidate.promotion?.canary?.beforeReceipts || []);
180
+ const pairedBefore = state.outcomes.find((entry) => beforeReceiptIds.has(entry.id)
181
+ && ["agentspine.learning-outcome/v8", "agentspine.learning-outcome/v9"].includes(entry.schema)
182
+ && entry.measurement.evaluatorId === evaluatorId
183
+ && (!evaluatorRoot || entry.measurement.evaluatorRootDigest === evaluatorRoot.principalDigest));
184
+ if (!pairedBefore) throw new Error("after measurement requires the same evaluator as a frozen before measurement");
185
+ if (pairedBefore.measurement.kind !== kind || pairedBefore.coverage.caseCount !== normalizedCoverage.caseCount) {
186
+ throw new Error("after measurement must match the frozen evaluator's measurement kind and case count");
187
+ }
188
+ }
189
+ if (state.measurementLineage.some((entry) => entry.measurementReceiptId === id)) {
190
+ throw new Error("measurement receipt IDs are retained as immutable replay tombstones");
191
+ }
192
+ if (state.measurementLineage.some((entry) => entry.sourceDigest === sourceDigest)) {
193
+ throw new Error("measurement source provenance cannot be reused across evaluation contracts");
194
+ }
195
+ if (state.measurementLineage.some((entry) => entry.runDigest === lineage.runDigest)) {
196
+ throw new Error("measurement evaluator run cannot be replayed");
197
+ }
198
+ if (lineage.schema === "agentspine.learning-measurement-lineage/v2"
199
+ && state.measurementLineage.some((entry) => entry.schema === "agentspine.learning-measurement-lineage/v2"
200
+ && entry.rootRunDigest === lineage.rootRunDigest)) {
201
+ throw new Error("measurement evaluator-root run cannot be replayed through an alias ID");
202
+ }
203
+ state.measurements.push(receipt);
204
+ state.measurements.sort((a, b) => a.id.localeCompare(b.id));
205
+ state.measurementLineage.push(lineage);
206
+ state.measurementLineage.sort((a, b) => a.measurementReceiptId.localeCompare(b.measurementReceiptId));
207
+ return { receipt, lineage, learningPath, unchanged: false };
208
+ });
209
+ }
210
+
211
+ export async function revokeLearningMeasurement({
212
+ root = process.cwd(), measurementId, reasonCode, reason, confirmation, now = new Date()
213
+ }) {
214
+ if (confirmation !== "local-measurement-revocation-confirmed") {
215
+ throw new Error("measurement revocation requires explicit local confirmation");
216
+ }
217
+ if (!ID_RE.test(measurementId || "")) throw new Error("measurementId must be a stable identifier");
218
+ if (!MEASUREMENT_REVOCATION_REASONS.has(reasonCode)) {
219
+ throw new Error("reasonCode must be source-invalid, evaluator-invalid, protocol-invalid, duplicate, or other");
220
+ }
221
+ const revokeReason = safeText(reason, "reason", 500);
222
+ const timestamp = date(now, "now");
223
+ return mutation(root, (state, _catalog, learningPath) => {
224
+ const measurement = state.measurements.find((entry) => entry.id === measurementId);
225
+ if (!measurement) throw new Error(`unknown learning measurement: ${measurementId}`);
226
+ const candidate = state.candidates.find((entry) => entry.id === measurement.learningId);
227
+ const evaluation = state.evaluations.find((entry) => entry.id === measurement.evaluationId);
228
+ if (!candidate || !evaluation || evaluation.learningId !== candidate.id) {
229
+ throw new Error("measurement revocation requires its bound candidate and evaluation contract");
230
+ }
231
+ const outcome = state.outcomes.find((entry) => entry.measurementReceiptId === measurement.id) || null;
232
+ const targetDigest = learningTargetForCandidate(candidate).digest;
233
+ const reasonDigest = digest(revokeReason);
234
+ const existing = revokedMeasurement(state, measurementId);
235
+ if (existing) {
236
+ if (existing.reasonCode !== reasonCode || existing.reasonDigest !== reasonDigest
237
+ || existing.measurementDigest !== measurement.digest || existing.evaluationDigest !== evaluation.digest
238
+ || existing.targetDigest !== targetDigest || existing.outcomeId !== (outcome?.id || null)
239
+ || existing.outcomeDigest !== (outcome?.digest || null)) {
240
+ throw new Error("learning measurement revocations are immutable");
241
+ }
242
+ return { receipt: existing, learningPath, unchanged: true, authority: "context-only" };
243
+ }
244
+ const id = `measurement-revocation:${createHash("sha256")
245
+ .update(`${measurement.id}\0${measurement.digest}\0${targetDigest}`).digest("hex").slice(0, 32)}`;
246
+ const payload = measurementRevocationPayload({
247
+ id,
248
+ learningId: candidate.id,
249
+ evaluationId: evaluation.id,
250
+ evaluationDigest: evaluation.digest,
251
+ measurementId: measurement.id,
252
+ measurementDigest: measurement.digest,
253
+ outcomeId: outcome?.id || null,
254
+ outcomeDigest: outcome?.digest || null,
255
+ targetDigest,
256
+ reasonCode,
257
+ reasonDigest,
258
+ revokedAt: timestamp
259
+ });
260
+ const receipt = { ...payload, digest: digest(payload) };
261
+ state.measurementRevocations.push(receipt);
262
+ state.measurementRevocations.sort((a, b) => a.id.localeCompare(b.id));
263
+ return { receipt, learningPath, unchanged: false, authority: "context-only" };
264
+ });
265
+ }