blun-king-cli 9.1.566 → 9.1.569

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 (262) hide show
  1. package/agent-spine-plugin/.claude-plugin/marketplace.json +1 -1
  2. package/agent-spine-plugin/.claude-plugin/plugin.json +1 -1
  3. package/agent-spine-plugin/.codex-plugin/plugin.json +2 -1
  4. package/agent-spine-plugin/CHANGELOG.md +1581 -0
  5. package/agent-spine-plugin/README.md +30 -4
  6. package/agent-spine-plugin/blun.plugin.json +3 -3
  7. package/agent-spine-plugin/docs/acceptance.md +61 -0
  8. package/agent-spine-plugin/docs/assignment-continuation.md +48 -0
  9. package/agent-spine-plugin/docs/host-integration.md +178 -0
  10. package/agent-spine-plugin/docs/preflight-recall.md +69 -0
  11. package/agent-spine-plugin/docs/preservation-contract.md +53 -0
  12. package/agent-spine-plugin/docs/quality-gates.md +50 -0
  13. package/agent-spine-plugin/docs/releasing.md +85 -0
  14. package/agent-spine-plugin/docs/session-timeline.md +251 -0
  15. package/agent-spine-plugin/docs/source-roots.md +113 -0
  16. package/agent-spine-plugin/docs/structured-completion.md +67 -0
  17. package/agent-spine-plugin/docs/world-model.md +94 -0
  18. package/agent-spine-plugin/hooks/codex.json +2 -2
  19. package/agent-spine-plugin/hooks/hooks.json +1 -1
  20. package/agent-spine-plugin/hooks/version.json +1 -1
  21. package/agent-spine-plugin/package.json +13 -3
  22. package/agent-spine-plugin/scripts/check-codex-install.js +226 -0
  23. package/agent-spine-plugin/scripts/check-hosts.js +14 -5
  24. package/agent-spine-plugin/scripts/check-install-hook.js +226 -0
  25. package/agent-spine-plugin/scripts/check-install-selfstarter.js +154 -0
  26. package/agent-spine-plugin/scripts/check-install.js +478 -0
  27. package/agent-spine-plugin/scripts/check-line-budget.js +58 -0
  28. package/agent-spine-plugin/scripts/check-syntax.js +29 -0
  29. package/agent-spine-plugin/scripts/github-actions.js +11 -0
  30. package/agent-spine-plugin/scripts/hermetic-process.js +183 -0
  31. package/agent-spine-plugin/scripts/release-check.js +145 -0
  32. package/agent-spine-plugin/scripts/run-acceptance.js +19 -0
  33. package/agent-spine-plugin/scripts/run-checks.js +47 -0
  34. package/agent-spine-plugin/scripts/run-tests-hermetic.js +89 -0
  35. package/agent-spine-plugin/skills/agent-spine/SKILL.md +16 -2
  36. package/agent-spine-plugin/spine-example/1-identity.md +12 -0
  37. package/agent-spine-plugin/spine-example/2-voice.md +6 -0
  38. package/agent-spine-plugin/spine-example/3-conduct.md +8 -0
  39. package/agent-spine-plugin/spine-example/4-history.md +4 -0
  40. package/agent-spine-plugin/src/cli-agent.js +296 -0
  41. package/agent-spine-plugin/src/cli-attention.js +95 -0
  42. package/agent-spine-plugin/src/cli-autonomy.js +36 -0
  43. package/agent-spine-plugin/src/cli-common.js +71 -0
  44. package/agent-spine-plugin/src/cli-continuity.js +116 -0
  45. package/agent-spine-plugin/src/cli-core.js +128 -0
  46. package/agent-spine-plugin/src/cli-diagnostics.js +291 -0
  47. package/agent-spine-plugin/src/cli-host.js +21 -0
  48. package/agent-spine-plugin/src/cli-learning.js +305 -0
  49. package/agent-spine-plugin/src/cli-premortem.js +16 -0
  50. package/agent-spine-plugin/src/cli-sharing.js +230 -0
  51. package/agent-spine-plugin/src/cli.js +40 -1350
  52. package/agent-spine-plugin/src/codex-reader-launcher.js +182 -0
  53. package/agent-spine-plugin/src/hook.js +185 -567
  54. package/agent-spine-plugin/src/index.js +16 -2
  55. package/agent-spine-plugin/src/lib/acceptance.js +113 -11
  56. package/agent-spine-plugin/src/lib/action-lesson-recall.js +53 -0
  57. package/agent-spine-plugin/src/lib/attention-context.js +167 -0
  58. package/agent-spine-plugin/src/lib/attention-events.js +113 -0
  59. package/agent-spine-plugin/src/lib/attention-privacy.js +72 -0
  60. package/agent-spine-plugin/src/lib/attention-schema.js +164 -0
  61. package/agent-spine-plugin/src/lib/attention-storage.js +93 -0
  62. package/agent-spine-plugin/src/lib/attention.js +9 -600
  63. package/agent-spine-plugin/src/lib/audit-premortem.js +223 -0
  64. package/agent-spine-plugin/src/lib/audit.js +56 -6
  65. package/agent-spine-plugin/src/lib/autonomy-policy.js +110 -0
  66. package/agent-spine-plugin/src/lib/autonomy-store.js +202 -0
  67. package/agent-spine-plugin/src/lib/autonomy.js +8 -0
  68. package/agent-spine-plugin/src/lib/briefing.js +67 -4
  69. package/agent-spine-plugin/src/lib/catalog-document-read.js +51 -0
  70. package/agent-spine-plugin/src/lib/catalog.js +1 -1
  71. package/agent-spine-plugin/src/lib/codex-installation.js +231 -0
  72. package/agent-spine-plugin/src/lib/codex-skill-installation.js +211 -0
  73. package/agent-spine-plugin/src/lib/context.js +3 -1
  74. package/agent-spine-plugin/src/lib/delivery-agent-usage.js +224 -0
  75. package/agent-spine-plugin/src/lib/delivery-assignment.js +220 -0
  76. package/agent-spine-plugin/src/lib/delivery-command-actions.js +453 -0
  77. package/agent-spine-plugin/src/lib/delivery-knowledge.js +78 -0
  78. package/agent-spine-plugin/src/lib/delivery-premortem-binding.js +107 -0
  79. package/agent-spine-plugin/src/lib/delivery-premortem-closure.js +171 -0
  80. package/agent-spine-plugin/src/lib/delivery-premortem-codec.js +45 -0
  81. package/agent-spine-plugin/src/lib/delivery-premortem-correction.js +101 -0
  82. package/agent-spine-plugin/src/lib/delivery-premortem-file.js +21 -0
  83. package/agent-spine-plugin/src/lib/delivery-premortem-index.js +493 -0
  84. package/agent-spine-plugin/src/lib/delivery-premortem-inspection.js +37 -0
  85. package/agent-spine-plugin/src/lib/delivery-premortem-recovery.js +120 -0
  86. package/agent-spine-plugin/src/lib/delivery-premortem-rejection.js +65 -0
  87. package/agent-spine-plugin/src/lib/delivery-premortem-results.js +28 -0
  88. package/agent-spine-plugin/src/lib/delivery-premortem-session-guard.js +46 -0
  89. package/agent-spine-plugin/src/lib/delivery-premortem-write-ledger.js +285 -0
  90. package/agent-spine-plugin/src/lib/delivery-premortem.js +499 -0
  91. package/agent-spine-plugin/src/lib/delivery-shell-heredoc.js +115 -0
  92. package/agent-spine-plugin/src/lib/delivery-shell-substitutions.js +111 -0
  93. package/agent-spine-plugin/src/lib/delivery-shell-wrapper.js +64 -0
  94. package/agent-spine-plugin/src/lib/delivery-target.js +73 -0
  95. package/agent-spine-plugin/src/lib/delivery-verification.js +445 -0
  96. package/agent-spine-plugin/src/lib/documents.js +27 -5
  97. package/agent-spine-plugin/src/lib/filesystem-retry.js +2 -0
  98. package/agent-spine-plugin/src/lib/gateway-common.js +68 -0
  99. package/agent-spine-plugin/src/lib/gateway-control.js +302 -0
  100. package/agent-spine-plugin/src/lib/gateway-delivery.js +103 -0
  101. package/agent-spine-plugin/src/lib/gateway-execution.js +350 -0
  102. package/agent-spine-plugin/src/lib/gateway-host-lifecycle.js +185 -0
  103. package/agent-spine-plugin/src/lib/gateway-inspection.js +85 -0
  104. package/agent-spine-plugin/src/lib/gateway-knowledge.js +129 -0
  105. package/agent-spine-plugin/src/lib/gateway-policy-provenance.js +197 -0
  106. package/agent-spine-plugin/src/lib/gateway-premortem-disposition.js +82 -0
  107. package/agent-spine-plugin/src/lib/gateway-premortem.js +363 -0
  108. package/agent-spine-plugin/src/lib/gateway-runs.js +300 -0
  109. package/agent-spine-plugin/src/lib/gateway-runtime-identity.js +31 -0
  110. package/agent-spine-plugin/src/lib/gateway-runtime-records.js +21 -0
  111. package/agent-spine-plugin/src/lib/gateway-runtime.js +18 -1623
  112. package/agent-spine-plugin/src/lib/gateway-state-transaction.js +356 -0
  113. package/agent-spine-plugin/src/lib/gateway-state.js +342 -0
  114. package/agent-spine-plugin/src/lib/hook-artifact-guards.js +356 -0
  115. package/agent-spine-plugin/src/lib/hook-audit.js +16 -2
  116. package/agent-spine-plugin/src/lib/hook-briefing-use.js +68 -0
  117. package/agent-spine-plugin/src/lib/hook-context.js +424 -0
  118. package/agent-spine-plugin/src/lib/hook-final-message.js +26 -0
  119. package/agent-spine-plugin/src/lib/hook-input.js +27 -0
  120. package/agent-spine-plugin/src/lib/hook-output.js +140 -0
  121. package/agent-spine-plugin/src/lib/hook-premortem.js +257 -0
  122. package/agent-spine-plugin/src/lib/hook-process-advisory.js +37 -0
  123. package/agent-spine-plugin/src/lib/hook-protection.js +95 -0
  124. package/agent-spine-plugin/src/lib/hook-stop-verification.js +84 -0
  125. package/agent-spine-plugin/src/lib/hook-timeline.js +91 -0
  126. package/agent-spine-plugin/src/lib/identifier-analysis.js +446 -0
  127. package/agent-spine-plugin/src/lib/indexed-memory.js +23 -6
  128. package/agent-spine-plugin/src/lib/knowledge-evidence.js +431 -0
  129. package/agent-spine-plugin/src/lib/learning-applications.js +441 -0
  130. package/agent-spine-plugin/src/lib/learning-candidates.js +274 -0
  131. package/agent-spine-plugin/src/lib/learning-context.js +130 -0
  132. package/agent-spine-plugin/src/lib/learning-delivery-contracts.js +310 -0
  133. package/agent-spine-plugin/src/lib/learning-evaluation-contracts.js +350 -0
  134. package/agent-spine-plugin/src/lib/learning-evaluation-registration.js +372 -0
  135. package/agent-spine-plugin/src/lib/learning-evaluation-revocation.js +292 -0
  136. package/agent-spine-plugin/src/lib/learning-evidence-contracts.js +321 -0
  137. package/agent-spine-plugin/src/lib/learning-findings.js +458 -0
  138. package/agent-spine-plugin/src/lib/learning-measurement-contracts.js +285 -0
  139. package/agent-spine-plugin/src/lib/learning-measurements.js +265 -0
  140. package/agent-spine-plugin/src/lib/learning-outcome-contracts.js +165 -0
  141. package/agent-spine-plugin/src/lib/learning-outcomes.js +343 -0
  142. package/agent-spine-plugin/src/lib/learning-reconciliation.js +290 -0
  143. package/agent-spine-plugin/src/lib/learning-retry-contracts.js +188 -0
  144. package/agent-spine-plugin/src/lib/learning-schema.js +229 -0
  145. package/agent-spine-plugin/src/lib/learning-scope-targets.js +424 -0
  146. package/agent-spine-plugin/src/lib/learning-state-upgrade.js +477 -0
  147. package/agent-spine-plugin/src/lib/learning-status-configuration.js +476 -0
  148. package/agent-spine-plugin/src/lib/learning-storage.js +203 -0
  149. package/agent-spine-plugin/src/lib/learning-trial-recovery.js +218 -0
  150. package/agent-spine-plugin/src/lib/learning-validation-contracts.js +375 -0
  151. package/agent-spine-plugin/src/lib/learning-validation-renewal.js +298 -0
  152. package/agent-spine-plugin/src/lib/learning-validation-runtime.js +234 -0
  153. package/agent-spine-plugin/src/lib/learning.js +36 -6923
  154. package/agent-spine-plugin/src/lib/lesson-recall-session.js +172 -0
  155. package/agent-spine-plugin/src/lib/mcp-autonomy-tools.js +37 -0
  156. package/agent-spine-plugin/src/lib/mcp-delivery-completion.js +124 -0
  157. package/agent-spine-plugin/src/lib/mcp-delivery-tools.js +45 -0
  158. package/agent-spine-plugin/src/lib/mcp-premortem.js +68 -0
  159. package/agent-spine-plugin/src/lib/mcp-runtime.js +269 -0
  160. package/agent-spine-plugin/src/lib/mcp-source-context.js +51 -0
  161. package/agent-spine-plugin/src/lib/mcp-timeline-tools.js +164 -0
  162. package/agent-spine-plugin/src/lib/mcp-world-tools.js +52 -0
  163. package/agent-spine-plugin/src/lib/owned-file-lock.js +30 -10
  164. package/agent-spine-plugin/src/lib/project-portfolio.js +176 -0
  165. package/agent-spine-plugin/src/lib/selfstarter-core.js +382 -0
  166. package/agent-spine-plugin/src/lib/selfstarter-jobs.js +149 -0
  167. package/agent-spine-plugin/src/lib/selfstarter-lease.js +204 -0
  168. package/agent-spine-plugin/src/lib/selfstarter-policy.js +90 -0
  169. package/agent-spine-plugin/src/lib/selfstarter-workspace.js +111 -0
  170. package/agent-spine-plugin/src/lib/selfstarter.js +11 -911
  171. package/agent-spine-plugin/src/lib/session-timeline-auth.js +316 -0
  172. package/agent-spine-plugin/src/lib/session-timeline-codex.js +58 -0
  173. package/agent-spine-plugin/src/lib/session-timeline-contract.js +48 -0
  174. package/agent-spine-plugin/src/lib/session-timeline-enrollment-source.js +41 -0
  175. package/agent-spine-plugin/src/lib/session-timeline-enrollment-storage.js +132 -0
  176. package/agent-spine-plugin/src/lib/session-timeline-enrollment-transport.js +17 -0
  177. package/agent-spine-plugin/src/lib/session-timeline-enrollment.js +500 -0
  178. package/agent-spine-plugin/src/lib/session-timeline-event-extract.js +157 -0
  179. package/agent-spine-plugin/src/lib/session-timeline-host-origin.js +74 -0
  180. package/agent-spine-plugin/src/lib/session-timeline-host-receipt.js +117 -0
  181. package/agent-spine-plugin/src/lib/session-timeline-invocation.js +201 -0
  182. package/agent-spine-plugin/src/lib/session-timeline-king.js +79 -0
  183. package/agent-spine-plugin/src/lib/session-timeline-prior.js +59 -0
  184. package/agent-spine-plugin/src/lib/session-timeline-provider.js +34 -0
  185. package/agent-spine-plugin/src/lib/session-timeline-query.js +55 -0
  186. package/agent-spine-plugin/src/lib/session-timeline-results.js +31 -0
  187. package/agent-spine-plugin/src/lib/session-timeline-root.js +11 -0
  188. package/agent-spine-plugin/src/lib/session-timeline-search.js +82 -0
  189. package/agent-spine-plugin/src/lib/session-timeline-sid-acl.js +217 -0
  190. package/agent-spine-plugin/src/lib/session-timeline-source.js +83 -0
  191. package/agent-spine-plugin/src/lib/session-timeline-state.js +45 -0
  192. package/agent-spine-plugin/src/lib/session-timeline-transport.js +50 -0
  193. package/agent-spine-plugin/src/lib/session-timeline-windows-acl.js +148 -0
  194. package/agent-spine-plugin/src/lib/session-timeline.js +453 -0
  195. package/agent-spine-plugin/src/lib/source-roots.js +69 -136
  196. package/agent-spine-plugin/src/lib/source-tree-scan.js +178 -0
  197. package/agent-spine-plugin/src/lib/task-knowledge-context.js +78 -0
  198. package/agent-spine-plugin/src/lib/timeline-tool-guard.js +202 -0
  199. package/agent-spine-plugin/src/lib/world-knowledge.js +249 -0
  200. package/agent-spine-plugin/src/lib/world-model.js +278 -0
  201. package/agent-spine-plugin/src/mcp.js +20 -161
  202. package/agent-spine-plugin/src/version.js +1 -1
  203. package/agent-spine-plugin/src/worker.js +22 -5
  204. package/bin/agent-resume-snapshot.cjs +2 -2
  205. package/bin/agentspine-king-goal-inbox.mjs +111 -0
  206. package/bin/agentspine-king-goal-intake.mjs +106 -0
  207. package/bin/baseline-skill-performance-policy.cjs +1 -16
  208. package/bin/core-bootstrap.js +2 -0
  209. package/bin/curiosity-scout-policy.cjs +5 -1
  210. package/bin/input-draft-persistence.cjs +2 -2
  211. package/bin/king-tui-function-contract.json +33 -0
  212. package/bin/launcher-restart-policy.cjs +150 -0
  213. package/bin/launcher-runtime.js +56 -44
  214. package/bin/managed-context-startup-policy.cjs +27 -0
  215. package/bin/managed-plugin-selection.cjs +116 -0
  216. package/bin/mistake-relevance-policy.cjs +1 -1
  217. package/bin/observer-hooks.cjs +14 -0
  218. package/bin/oversized-context-offload-policy.cjs +86 -0
  219. package/bin/plugin-bootstrap.js +7 -40
  220. package/bin/proactive-compaction-policy.cjs +1 -1
  221. package/bin/provider-model-refresh-deadline.cjs +53 -0
  222. package/bin/provider-model-refresh-policy.cjs +107 -0
  223. package/bin/release-artifact-freeze-policy.cjs +30 -0
  224. package/bin/repeated-user-message-projection.cjs +3 -126
  225. package/bin/research-page-result.cjs +74 -0
  226. package/bin/runtime-exit-ledger.cjs +1 -0
  227. package/bin/session-compaction-policy.cjs +84 -0
  228. package/bin/skill-listing-performance-policy.cjs +2 -2
  229. package/bin/standard-tools-bootstrap.js +0 -37
  230. package/bin/subagent-skill-policy.cjs +3 -1
  231. package/bin/telegram-approval-relay.cjs +12 -7
  232. package/bin/telegram-private-conversation-policy.cjs +3 -2
  233. package/bin/telegram-queue-handoff-policy.cjs +24 -0
  234. package/bin/thinking-activity-status-policy.cjs +1 -1
  235. package/bin/thinking-only-guard.cjs +16 -12
  236. package/bin/tool-call-loop-policy.cjs +0 -2
  237. package/bin/tool-result-offload-policy.cjs +11 -2
  238. package/bin/tui-functional-contract.cjs +55 -0
  239. package/bin/update-notice.js +18 -14
  240. package/bin/user-message-offload-policy.cjs +1 -1
  241. package/bin/user-prompt-hook-origin-policy.cjs +34 -0
  242. package/bin/windows-node-crash-dump.cjs +110 -0
  243. package/blun.mjs +2517 -1048
  244. package/codebase-index/codebase_index.py +4 -3
  245. package/package.json +3 -17
  246. package/standard-skills/research-evidence/SKILL.md +39 -0
  247. package/standard-skills/research-evidence/references/evidence-format.md +82 -0
  248. package/standard-skills/research-evidence/scripts/evidence-collection.cjs +254 -0
  249. package/standard-skills/research-evidence/scripts/score-report.cjs +112 -0
  250. package/standard-skills/web-lesen/SKILL.md +37 -22
  251. package/standard-skills/web-lesen/scripts/crawl_public.py +376 -0
  252. package/telegram-plugin/DELIVERY.md +36 -0
  253. package/telegram-plugin/bin/telegram-approval-relay.cjs +13 -7
  254. package/telegram-plugin/bin/telegram-launcher-status-queue.cjs +122 -0
  255. package/telegram-plugin/bin/telegram-private-conversation-policy.cjs +3 -2
  256. package/telegram-plugin/bin/telegram-reply-parts.cjs +149 -0
  257. package/telegram-plugin/dist/bridge.mjs +11 -57
  258. package/telegram-plugin/dist/mcp-server.mjs +33 -4
  259. package/agent-spine-plugin/skill/SKILL.md +0 -76
  260. package/bin/mnemo-connect-heartbeat.cjs +0 -204
  261. package/bin/mnemo-tool-agent-policy.cjs +0 -22
  262. package/telegram-plugin/bin/telegram-mnemo-capture.cjs +0 -297
@@ -0,0 +1,372 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import {
3
+ METRIC_DIRECTIONS, ID_RE, DIGEST_RE, TRIAL_RETRY_EVALUATIONS, STALENESS_BOUND_EVALUATIONS, CANDIDATE_EVIDENCE_BOUND_EVALUATIONS,
4
+ BLOCKING_DEFECT_BOUND_EVALUATIONS, EVIDENCE_SOURCE_BOUND_EVALUATIONS, EVIDENCE_SOURCE_ATTESTED_EVALUATIONS, CANDIDATE_EVIDENCE_LINEAGE_EVALUATIONS
5
+ } from "./learning-schema.js";
6
+ import {
7
+ normalizeScope, scopeContains, exactScope, digest, learningTargetForCandidate, evaluationBindingPayload,
8
+ activeEvaluatorRecord
9
+ } from "./learning-scope-targets.js";
10
+ import {
11
+ initialTrialDigest, completionPolicyPayload, stalenessPolicyPayload, blockingDefectPolicyPayload, evidenceSourcePolicyPayload, qualifyingEvidenceCount,
12
+ evidenceSourceAttestations
13
+ } from "./learning-measurement-contracts.js";
14
+ import {
15
+ candidateEvidencePolicyPayload, eligibleCandidateEvidence, candidateEvidenceCohort, candidateEvidenceLineageReceipts, candidateAdmissionPayload, trialComparisonDigest,
16
+ trialRetryPayload
17
+ } from "./learning-evidence-contracts.js";
18
+ import {
19
+ evaluationPayload
20
+ } from "./learning-evaluation-contracts.js";
21
+ import {
22
+ retryableTrialFailures, trialRetryMatchesState
23
+ } from "./learning-retry-contracts.js";
24
+ import {
25
+ date, integer, safeText, mutation, evidenceConfidence
26
+ } from "./learning-storage.js";
27
+
28
+ export async function registerLearningEvaluation({
29
+ root = process.cwd(), id = `evaluation:${randomUUID()}`, learningId, scope, metric, benchmark,
30
+ evaluatorIds, evaluatorRoots, expiresAt = null, retryTrialFailureId = null,
31
+ confirmLocalEvaluation = false, confirmLocalTrialRetry = false,
32
+ confirmLocalEvidenceSources = false, now = new Date()
33
+ }) {
34
+ if (!confirmLocalEvaluation) throw new Error("evaluation registration requires explicit local confirmation");
35
+ if (!ID_RE.test(id || "") || !ID_RE.test(learningId || "")) {
36
+ throw new Error("evaluation id and learningId must be stable identifiers");
37
+ }
38
+ const timestamp = date(now, "now");
39
+ return mutation(root, (state, _catalog, learningPath) => {
40
+ const candidate = state.candidates.find((entry) => entry.id === learningId);
41
+ if (!candidate) throw new Error(`unknown learning candidate: ${learningId}`);
42
+ if (candidate.kind !== "behavior" || candidate.status !== "candidate" || candidate.requiresLocalReview) {
43
+ throw new Error("evaluation contracts are limited to unreviewed, low-risk behavior candidates");
44
+ }
45
+ const normalizedScope = normalizeScope(scope);
46
+ if (!scopeContains(candidate.scope, normalizedScope)) throw new Error("evaluation scope does not match the learning candidate");
47
+ const name = safeText(metric?.name, "evaluation.metric.name", 120);
48
+ const direction = metric?.direction;
49
+ if (!METRIC_DIRECTIONS.has(direction)) throw new Error("evaluation.metric.direction must be higher or lower");
50
+ const digests = {
51
+ taskDigest: benchmark?.taskDigest,
52
+ datasetDigest: benchmark?.datasetDigest,
53
+ protocolDigest: benchmark?.protocolDigest
54
+ };
55
+ if (Object.entries(digests).some(([, value]) => !DIGEST_RE.test(value || ""))) {
56
+ throw new Error("evaluation benchmark digests must be SHA-256 values");
57
+ }
58
+ const requestedEvaluators = [...new Set((evaluatorIds || []).map((value) => String(value)))];
59
+ const normalizedEvaluators = [...requestedEvaluators].sort();
60
+ const requestedRoots = new Map((evaluatorRoots || []).map((root) => [String(root?.evaluatorId || ""),
61
+ String(root?.principalDigest || "")]));
62
+ const requiredEvaluators = Math.max(state.config.minOutcomeReceipts, state.config.canaryReceipts, 2);
63
+ if (normalizedEvaluators.length < requiredEvaluators || normalizedEvaluators.some((value) => !ID_RE.test(value))) {
64
+ throw new Error(`evaluation requires at least ${requiredEvaluators} distinct stable evaluator IDs`);
65
+ }
66
+ const registeredRoots = normalizedEvaluators.map((evaluatorId) => activeEvaluatorRecord(state, evaluatorId));
67
+ if (registeredRoots.some((record) => !record)
68
+ || new Set(registeredRoots.map((record) => record.principalDigest)).size !== registeredRoots.length) {
69
+ throw new Error("evaluation requires every evaluator root to be active in the locally confirmed registry");
70
+ }
71
+ if (requestedRoots.size && (requestedRoots.size !== registeredRoots.length
72
+ || registeredRoots.some((record) => requestedRoots.get(record.id) !== record.principalDigest))) {
73
+ throw new Error("evaluation evaluator roots do not match the active local registry");
74
+ }
75
+ const normalizedRoots = registeredRoots.map((record) => ({
76
+ evaluatorId: record.id, principalDigest: record.principalDigest, authority: "context-only"
77
+ })).sort((a, b) => a.evaluatorId.localeCompare(b.evaluatorId));
78
+ const expiry = date(expiresAt || new Date(new Date(timestamp).getTime()
79
+ + state.config.outcomeMaxAgeDays * 86400000), "evaluation.expiresAt");
80
+ if (new Date(expiry).getTime() <= new Date(timestamp).getTime()
81
+ || new Date(expiry).getTime() > new Date(timestamp).getTime() + 365 * 86400000) {
82
+ throw new Error("evaluation expiry must be in the future and no more than 365 days away");
83
+ }
84
+ const normalizedMetric = { name, direction };
85
+ const normalizedBenchmark = {
86
+ ...digests, minCases: integer(benchmark?.minCases, "evaluation.benchmark.minCases", 1, 1000000)
87
+ };
88
+ const selectedRoots = requestedEvaluators.slice(0, requiredEvaluators).map((evaluatorId) => {
89
+ const rootRecord = registeredRoots.find((record) => record.id === evaluatorId);
90
+ return { evaluatorId, evaluatorRootDigest: rootRecord.principalDigest };
91
+ });
92
+ const trialsForPhase = (phase) => selectedRoots.map((rootRecord, index) => {
93
+ const trial = {
94
+ slot: index + 1,
95
+ evaluatorId: rootRecord.evaluatorId,
96
+ evaluatorRootDigest: rootRecord.evaluatorRootDigest,
97
+ runId: `run:initial:${phase}:${digest([id, learningId, normalizedScope, normalizedMetric,
98
+ normalizedBenchmark, phase, index + 1, rootRecord.evaluatorId,
99
+ rootRecord.evaluatorRootDigest]).slice(0, 32)}`,
100
+ caseCount: normalizedBenchmark.minCases,
101
+ authority: "context-only"
102
+ };
103
+ return { ...trial, trialDigest: initialTrialDigest({
104
+ evaluationId: id, learningId, scope: normalizedScope, metric: normalizedMetric,
105
+ benchmark: normalizedBenchmark, phase, slot: trial.slot, evaluatorId: trial.evaluatorId,
106
+ evaluatorRootDigest: trial.evaluatorRootDigest, runId: trial.runId, caseCount: trial.caseCount
107
+ }) };
108
+ });
109
+ const initialTrials = {
110
+ schema: "agentspine.learning-initial-trials/v1",
111
+ mode: "first-admitted-trials",
112
+ requiredTrials: requiredEvaluators,
113
+ benchmarkDigest: digest(normalizedBenchmark),
114
+ before: trialsForPhase("before"),
115
+ after: trialsForPhase("after"),
116
+ authority: "context-only"
117
+ };
118
+ let thresholds = {
119
+ minImprovement: state.config.minImprovement,
120
+ regressionTolerance: state.config.regressionTolerance,
121
+ beforeReceipts: requiredEvaluators,
122
+ afterReceipts: requiredEvaluators,
123
+ minConfidence: state.config.minConfidence,
124
+ minEvidence: Math.max(2, state.config.minEvidence)
125
+ };
126
+ const pairing = {
127
+ mode: "same-evaluator",
128
+ maxOutcomesPerEvaluatorPerPhase: 1,
129
+ matchMeasurementKind: true,
130
+ matchCaseCount: true,
131
+ authority: "context-only"
132
+ };
133
+ const target = learningTargetForCandidate(candidate);
134
+ const stalenessPolicyPayloadValue = stalenessPolicyPayload({
135
+ outcomeMaxAgeDays: state.config.outcomeMaxAgeDays,
136
+ canaryTtlDays: state.config.canaryTtlDays
137
+ });
138
+ const stalenessPolicy = {
139
+ ...stalenessPolicyPayloadValue,
140
+ digest: digest(stalenessPolicyPayloadValue)
141
+ };
142
+ const blockingDefectPolicyPayloadValue = blockingDefectPolicyPayload();
143
+ const blockingDefectPolicy = {
144
+ ...blockingDefectPolicyPayloadValue,
145
+ digest: digest(blockingDefectPolicyPayloadValue)
146
+ };
147
+ const evidenceSourcePolicyPayloadValue = evidenceSourcePolicyPayload(
148
+ "agentspine.learning-evidence-source-policy/v2");
149
+ let evidenceSourcePolicy = {
150
+ ...evidenceSourcePolicyPayloadValue,
151
+ digest: digest(evidenceSourcePolicyPayloadValue)
152
+ };
153
+ const retryable = retryableTrialFailures(state, candidate);
154
+ let retry = null;
155
+ let evaluationSchema = "agentspine.learning-evaluation/v28";
156
+ if (retryable.length) {
157
+ if (!confirmLocalTrialRetry) {
158
+ throw new Error("a repeated failed learning requires explicit local trial-retry confirmation");
159
+ }
160
+ if (!ID_RE.test(retryTrialFailureId || "")) {
161
+ throw new Error("retryTrialFailureId must identify the revoked failure being retried");
162
+ }
163
+ const selected = retryable.find((entry) => entry.failure.id === retryTrialFailureId);
164
+ const latest = retryable[retryable.length - 1];
165
+ if (!selected || selected.revocation.id !== latest.revocation.id) {
166
+ throw new Error("trial retry must bind the latest matching revoked failure");
167
+ }
168
+ const predecessorEvaluation = state.evaluations.find((entry) =>
169
+ entry.id === selected.failure.evaluationId && entry.digest === selected.failure.evaluationDigest);
170
+ if (!predecessorEvaluation) {
171
+ throw new Error("trial retry predecessor evaluation is missing or changed");
172
+ }
173
+ if (EVIDENCE_SOURCE_BOUND_EVALUATIONS.has(predecessorEvaluation.schema)
174
+ && !EVIDENCE_SOURCE_ATTESTED_EVALUATIONS.has(predecessorEvaluation.schema)) {
175
+ evidenceSourcePolicy = predecessorEvaluation.evidenceSourcePolicy;
176
+ }
177
+ if (state.trialRetryExhaustions.some((receipt) => receipt.trialFailureId === selected.failure.id
178
+ && receipt.trialFailureDigest === selected.failure.digest)) {
179
+ throw new Error("trial retry budget is exhausted by an immutable terminal receipt");
180
+ }
181
+ if (TRIAL_RETRY_EVALUATIONS.has(predecessorEvaluation.schema)) {
182
+ throw new Error("trial retry budget is exhausted; a failed corrective Canary cannot be retried again");
183
+ }
184
+ evaluationSchema = EVIDENCE_SOURCE_ATTESTED_EVALUATIONS.has(predecessorEvaluation.schema)
185
+ ? "agentspine.learning-evaluation/v29"
186
+ : EVIDENCE_SOURCE_BOUND_EVALUATIONS.has(predecessorEvaluation.schema)
187
+ ? "agentspine.learning-evaluation/v25"
188
+ : BLOCKING_DEFECT_BOUND_EVALUATIONS.has(predecessorEvaluation.schema)
189
+ ? "agentspine.learning-evaluation/v23"
190
+ : STALENESS_BOUND_EVALUATIONS.has(predecessorEvaluation.schema)
191
+ ? "agentspine.learning-evaluation/v21"
192
+ : "agentspine.learning-evaluation/v13";
193
+ const comparisonDigest = trialComparisonDigest({
194
+ schema: evaluationSchema,
195
+ metric: normalizedMetric,
196
+ benchmark: normalizedBenchmark,
197
+ evaluatorRoots: normalizedRoots,
198
+ thresholds,
199
+ pairing,
200
+ stalenessPolicy,
201
+ blockingDefectPolicy,
202
+ evidenceSourcePolicy
203
+ });
204
+ if (comparisonDigest !== trialComparisonDigest(predecessorEvaluation, selected.predecessor.promotion)) {
205
+ throw new Error("trial retry objective contract drift is not allowed");
206
+ }
207
+ const retryPayload = trialRetryPayload({
208
+ schema: "agentspine.learning-trial-retry/v3",
209
+ trialFailureId: selected.failure.id,
210
+ trialFailureDigest: selected.failure.digest,
211
+ trialFailureRevocationId: selected.revocation.id,
212
+ trialFailureRevocationDigest: selected.revocation.digest,
213
+ predecessorLearningId: selected.predecessor.id,
214
+ predecessorEvaluationId: predecessorEvaluation.id,
215
+ predecessorEvaluationDigest: predecessorEvaluation.digest,
216
+ comparisonDigest,
217
+ rootEvaluationId: predecessorEvaluation.id,
218
+ rootEvaluationDigest: predecessorEvaluation.digest,
219
+ attempt: 2,
220
+ maxAttempts: 2,
221
+ learningId: candidate.id,
222
+ targetDigest: target.digest,
223
+ scopeDigest: digest(candidate.scope),
224
+ minimumEvidenceObservedAt: selected.revocation.revokedAt,
225
+ admittedAt: timestamp
226
+ });
227
+ retry = { ...retryPayload, digest: digest(retryPayload) };
228
+ } else if (retryTrialFailureId !== null || confirmLocalTrialRetry) {
229
+ throw new Error("trial retry confirmation does not match a revoked failure for this exact learning scope");
230
+ }
231
+ const evidencePolicyPayloadValue = candidateEvidencePolicyPayload({
232
+ maxAgeDays: stalenessPolicy.outcomeMaxAgeDays,
233
+ minimumIndependentEvidence: thresholds.minEvidence
234
+ });
235
+ const evidencePolicy = {
236
+ ...evidencePolicyPayloadValue,
237
+ digest: digest(evidencePolicyPayloadValue)
238
+ };
239
+ const eligibleEvidence = eligibleCandidateEvidence(candidate, timestamp, evidencePolicy.maxAgeDays);
240
+ const evidenceCohort = candidateEvidenceCohort(eligibleEvidence);
241
+ if (EVIDENCE_SOURCE_ATTESTED_EVALUATIONS.has(evaluationSchema) && !confirmLocalEvidenceSources) {
242
+ throw new Error("qualifying evidence sources require explicit local confirmation");
243
+ }
244
+ const sourceAttestations = EVIDENCE_SOURCE_ATTESTED_EVALUATIONS.has(evaluationSchema)
245
+ ? evidenceSourceAttestations(evidenceCohort, timestamp) : [];
246
+ const evidenceCount = new Set(evidenceCohort.map((entry) => entry.independenceDigest)).size;
247
+ const observedConfidence = evidenceConfidence(eligibleEvidence);
248
+ if (candidate.evidence.some((entry) => new Date(entry.observedAt).getTime() > new Date(timestamp).getTime())) {
249
+ throw new Error("evaluation candidate evidence cannot be observed in the future");
250
+ }
251
+ if (EVIDENCE_SOURCE_BOUND_EVALUATIONS.has(evaluationSchema)
252
+ && qualifyingEvidenceCount(evidenceCohort, evidenceSourcePolicy, sourceAttestations)
253
+ < evidenceSourcePolicy.minimumQualifyingEvidence) {
254
+ throw new Error("evaluation requires fresh explicit-user or objective-test evidence; interaction- or document-only cohorts are insufficient");
255
+ }
256
+ if (!retry && (observedConfidence < thresholds.minConfidence || evidenceCount < thresholds.minEvidence)) {
257
+ throw new Error("evaluation requires a candidate that already satisfies the frozen confidence and evidence gates");
258
+ }
259
+ const evidenceLineage = CANDIDATE_EVIDENCE_LINEAGE_EVALUATIONS.has(evaluationSchema)
260
+ ? candidateEvidenceLineageReceipts({
261
+ learningId: candidate.id,
262
+ evaluationId: id,
263
+ attestations: sourceAttestations,
264
+ targetDigest: target.digest,
265
+ scopeDigest: digest(normalizedScope),
266
+ admittedAt: timestamp
267
+ }) : [];
268
+ const liveIdempotentEvaluation = state.evaluations.some((entry) => entry.id === id
269
+ && entry.learningId === candidate.id);
270
+ for (const lineage of evidenceLineage) {
271
+ const existingLineage = state.candidateEvidenceLineage.find((entry) =>
272
+ entry.scopeDigest === lineage.scopeDigest
273
+ && (entry.evidenceDigest === lineage.evidenceDigest
274
+ || entry.independenceDigest === lineage.independenceDigest));
275
+ if (existingLineage && (!liveIdempotentEvaluation || existingLineage.evaluationId !== id
276
+ || existingLineage.learningId !== candidate.id || existingLineage.digest !== lineage.digest)) {
277
+ throw new Error("qualifying candidate evidence was already admitted in this exact scope and cannot be replayed");
278
+ }
279
+ }
280
+ const candidateAdmissionPayloadValue = candidateAdmissionPayload({
281
+ schema: EVIDENCE_SOURCE_ATTESTED_EVALUATIONS.has(evaluationSchema)
282
+ ? CANDIDATE_EVIDENCE_LINEAGE_EVALUATIONS.has(evaluationSchema)
283
+ ? "agentspine.learning-candidate-admission/v4"
284
+ : "agentspine.learning-candidate-admission/v3"
285
+ : CANDIDATE_EVIDENCE_BOUND_EVALUATIONS.has(evaluationSchema)
286
+ ? "agentspine.learning-candidate-admission/v2"
287
+ : "agentspine.learning-candidate-admission/v1",
288
+ learningId: candidate.id,
289
+ targetDigest: target.digest,
290
+ scopeDigest: digest(normalizedScope),
291
+ minConfidence: thresholds.minConfidence,
292
+ minEvidence: thresholds.minEvidence,
293
+ observedConfidence,
294
+ evidenceCount,
295
+ evidencePolicy,
296
+ evidenceCohort,
297
+ evidenceSourceAttestations: sourceAttestations,
298
+ evidenceLineageDigest: evidenceLineage.length ? digest(evidenceLineage) : undefined,
299
+ admittedAt: timestamp
300
+ });
301
+ const candidateAdmission = {
302
+ ...candidateAdmissionPayloadValue,
303
+ digest: digest(candidateAdmissionPayloadValue)
304
+ };
305
+ const completionPolicyPayloadValue = completionPolicyPayload({
306
+ deliveryTimeoutMs: 5 * 60_000,
307
+ outcomeTimeoutMs: state.config.initialTrialOutcomeTimeoutMinutes * 60_000
308
+ });
309
+ const completionPolicy = {
310
+ ...completionPolicyPayloadValue,
311
+ digest: digest(completionPolicyPayloadValue)
312
+ };
313
+ const payload = evaluationPayload({
314
+ schema: evaluationSchema,
315
+ id, learningId, scope: normalizedScope, metric: normalizedMetric,
316
+ benchmark: normalizedBenchmark,
317
+ evaluatorIds: normalizedEvaluators,
318
+ evaluatorRoots: normalizedRoots,
319
+ thresholds,
320
+ pairing,
321
+ initialTrials,
322
+ target,
323
+ completionPolicy,
324
+ stalenessPolicy,
325
+ blockingDefectPolicy,
326
+ evidenceSourcePolicy,
327
+ candidateAdmission,
328
+ retry,
329
+ registeredAt: timestamp, expiresAt: expiry
330
+ });
331
+ const contract = { ...payload, digest: digest(payload) };
332
+ if (retry && !trialRetryMatchesState(state, contract)) {
333
+ throw new Error("trial retry requires a fresh candidate and independently observed evidence after revocation");
334
+ }
335
+ if (retry && (observedConfidence < thresholds.minConfidence || evidenceCount < thresholds.minEvidence)) {
336
+ throw new Error("evaluation requires a candidate that already satisfies the frozen confidence and evidence gates");
337
+ }
338
+ const existing = state.evaluations.find((entry) => entry.id === id);
339
+ if (existing) {
340
+ if (existing.digest === contract.digest) {
341
+ const binding = state.evaluationBindings.find((entry) => entry.evaluationId === existing.id) || null;
342
+ return { contract: existing, binding, learningPath, unchanged: true };
343
+ }
344
+ throw new Error("evaluation contract IDs are immutable");
345
+ }
346
+ if (retry && state.evaluations.some((entry) => TRIAL_RETRY_EVALUATIONS.has(entry.schema)
347
+ && entry.retry.trialFailureRevocationId === retry.trialFailureRevocationId)) {
348
+ throw new Error("the revoked trial failure already has an immutable retry contract");
349
+ }
350
+ if (state.evaluations.some((entry) => entry.learningId === learningId
351
+ && exactScope(entry.scope, normalizedScope) && new Date(entry.expiresAt).getTime() >= new Date(timestamp).getTime())) {
352
+ throw new Error("an active evaluation contract already exists for this learning and exact scope");
353
+ }
354
+ state.evaluations.push(contract);
355
+ state.evaluations.sort((a, b) => a.id.localeCompare(b.id));
356
+ state.candidateEvidenceLineage.push(...evidenceLineage);
357
+ state.candidateEvidenceLineage.sort((a, b) => a.evidenceDigest.localeCompare(b.evidenceDigest)
358
+ || a.scopeDigest.localeCompare(b.scopeDigest));
359
+ const bindingPayload = evaluationBindingPayload({
360
+ evaluationId: contract.id,
361
+ evaluationDigest: contract.digest,
362
+ evaluators: registeredRoots.map((record) => ({ evaluatorId: record.id,
363
+ principalDigest: record.principalDigest, registryDigest: record.digest, authority: "context-only" }))
364
+ .sort((a, b) => a.evaluatorId.localeCompare(b.evaluatorId)),
365
+ boundAt: timestamp
366
+ });
367
+ const binding = { ...bindingPayload, digest: digest(bindingPayload) };
368
+ state.evaluationBindings.push(binding);
369
+ state.evaluationBindings.sort((a, b) => a.evaluationId.localeCompare(b.evaluationId));
370
+ return { contract, binding, learningPath, unchanged: false };
371
+ });
372
+ }
@@ -0,0 +1,292 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import {
3
+ ID_RE, DIGEST_RE, REGISTRY_BOUND_EVALUATIONS, EVIDENCE_SOURCE_ATTESTED_EVALUATIONS, EVALUATION_REVOCATION_REASONS, EVIDENCE_SOURCE_ATTESTATION_REVOCATION_REASONS,
4
+ VALIDATION_REVOCATION_REASONS
5
+ } from "./learning-schema.js";
6
+ import {
7
+ digest, learningTargetForCandidate, evaluationRevocationPayload, revokedEvaluation
8
+ } from "./learning-scope-targets.js";
9
+ import {
10
+ revokedEvaluationForCandidate, revalidationWindowPayload, revalidationTrialDigest, storedValidationLeaseStructure
11
+ } from "./learning-validation-contracts.js";
12
+ import {
13
+ validationLeaseRecord, validationLeaseChain, validationRevocationPayload, revokedValidation, validationLeaseState
14
+ } from "./learning-validation-runtime.js";
15
+ import {
16
+ evaluationStalenessPolicy, evidenceSourceAttestations, evidenceSourceAttestationRevocationPayload, revokedEvidenceSourceAttestation, revokedEvidenceSourceAttestationForCandidate
17
+ } from "./learning-measurement-contracts.js";
18
+ import {
19
+ revokedApplicationForCandidate, revokedOutcomeForCandidate
20
+ } from "./learning-delivery-contracts.js";
21
+ import {
22
+ date, safeText, mutation, preserve
23
+ } from "./learning-storage.js";
24
+
25
+ export async function revokeLearningEvaluation({
26
+ root = process.cwd(), evaluationId, reasonCode, reason, confirmation, now = new Date()
27
+ }) {
28
+ if (confirmation !== "local-evaluation-revocation-confirmed") {
29
+ throw new Error("evaluation revocation requires explicit local confirmation");
30
+ }
31
+ if (!ID_RE.test(evaluationId || "")) throw new Error("evaluationId must be a stable identifier");
32
+ if (!EVALUATION_REVOCATION_REASONS.has(reasonCode)) {
33
+ throw new Error("reasonCode must be benchmark-invalid, protocol-invalid, scope-invalid, threshold-invalid, duplicate, or other");
34
+ }
35
+ const revokeReason = safeText(reason, "reason", 500);
36
+ const timestamp = date(now, "now");
37
+ return mutation(root, (state, _catalog, learningPath) => {
38
+ const evaluation = state.evaluations.find((entry) => entry.id === evaluationId);
39
+ if (!evaluation) throw new Error(`unknown learning evaluation: ${evaluationId}`);
40
+ const candidate = state.candidates.find((entry) => entry.id === evaluation.learningId);
41
+ if (!candidate) throw new Error("evaluation revocation requires its immutable candidate lineage");
42
+ const binding = state.evaluationBindings.find((entry) => entry.evaluationId === evaluation.id
43
+ && entry.evaluationDigest === evaluation.digest) || null;
44
+ if (REGISTRY_BOUND_EVALUATIONS.has(evaluation.schema) && !binding) {
45
+ throw new Error("evaluation revocation requires its immutable evaluator binding");
46
+ }
47
+ const targetDigest = learningTargetForCandidate(candidate).digest;
48
+ const reasonDigest = digest(revokeReason);
49
+ const existing = revokedEvaluation(state, evaluationId);
50
+ if (existing) {
51
+ if (existing.reasonCode !== reasonCode || existing.reasonDigest !== reasonDigest
52
+ || existing.learningId !== candidate.id || existing.evaluationDigest !== evaluation.digest
53
+ || existing.evaluatorBindingDigest !== (binding?.digest || null)
54
+ || existing.targetDigest !== targetDigest) {
55
+ throw new Error("learning evaluation revocations are immutable");
56
+ }
57
+ return { receipt: existing, learningPath, unchanged: true, authority: "context-only" };
58
+ }
59
+ const id = `evaluation-revocation:${createHash("sha256")
60
+ .update(`${evaluation.id}\0${evaluation.digest}\0${targetDigest}`).digest("hex").slice(0, 32)}`;
61
+ const payload = evaluationRevocationPayload({
62
+ id,
63
+ learningId: candidate.id,
64
+ evaluationId: evaluation.id,
65
+ evaluationDigest: evaluation.digest,
66
+ evaluatorBindingDigest: binding?.digest || null,
67
+ targetDigest,
68
+ reasonCode,
69
+ reasonDigest,
70
+ revokedAt: timestamp
71
+ });
72
+ const receipt = { ...payload, digest: digest(payload) };
73
+ state.evaluationRevocations.push(receipt);
74
+ state.evaluationRevocations.sort((a, b) => a.id.localeCompare(b.id));
75
+ return { receipt, learningPath, unchanged: false, authority: "context-only" };
76
+ });
77
+ }
78
+
79
+ export async function revokeLearningEvidenceSourceAttestation({
80
+ root = process.cwd(), evaluationId, evidenceDigest, reasonCode, reason, confirmation, now = new Date()
81
+ }) {
82
+ if (confirmation !== "local-evidence-source-attestation-revocation-confirmed") {
83
+ throw new Error("evidence source attestation revocation requires explicit local confirmation");
84
+ }
85
+ if (!ID_RE.test(evaluationId || "")) throw new Error("evaluationId must be a stable identifier");
86
+ if (!DIGEST_RE.test(evidenceDigest || "")) throw new Error("evidenceDigest must be one SHA-256 digest");
87
+ if (!EVIDENCE_SOURCE_ATTESTATION_REVOCATION_REASONS.has(reasonCode)) {
88
+ throw new Error("reasonCode must be source-class-invalid, confirmation-invalid, scope-invalid, duplicate, or other");
89
+ }
90
+ const revokeReason = safeText(reason, "reason", 500);
91
+ const timestamp = date(now, "now");
92
+ return mutation(root, (state, _catalog, learningPath) => {
93
+ const evaluation = state.evaluations.find((entry) => entry.id === evaluationId);
94
+ if (!evaluation || !EVIDENCE_SOURCE_ATTESTED_EVALUATIONS.has(evaluation.schema)) {
95
+ throw new Error(`unknown source-attested learning evaluation: ${evaluationId}`);
96
+ }
97
+ const candidate = state.candidates.find((entry) => entry.id === evaluation.learningId);
98
+ const attestation = evaluation.candidateAdmission.evidenceSourceAttestations.find((entry) =>
99
+ entry.evidenceDigest === evidenceDigest);
100
+ if (!candidate || !attestation) {
101
+ throw new Error("source attestation revocation requires its exact immutable candidate and evidence binding");
102
+ }
103
+ const targetDigest = learningTargetForCandidate(candidate).digest;
104
+ const scopeDigest = digest(evaluation.scope);
105
+ const reasonDigest = digest(revokeReason);
106
+ const existing = revokedEvidenceSourceAttestation(state, evaluationId, evidenceDigest);
107
+ if (existing) {
108
+ if (existing.reasonCode !== reasonCode || existing.reasonDigest !== reasonDigest
109
+ || existing.learningId !== candidate.id || existing.evaluationDigest !== evaluation.digest
110
+ || existing.candidateAdmissionDigest !== evaluation.candidateAdmission.digest
111
+ || existing.independenceDigest !== attestation.independenceDigest
112
+ || existing.sourceClass !== attestation.sourceClass || existing.targetDigest !== targetDigest
113
+ || existing.scopeDigest !== scopeDigest) {
114
+ throw new Error("learning evidence source attestation revocations are immutable");
115
+ }
116
+ return { receipt: existing, learningPath, unchanged: true, authority: "context-only" };
117
+ }
118
+ const id = `evidence-source-attestation-revocation:${createHash("sha256")
119
+ .update(`${evaluation.id}\0${evaluation.digest}\0${evidenceDigest}`).digest("hex").slice(0, 32)}`;
120
+ const payload = evidenceSourceAttestationRevocationPayload({
121
+ id,
122
+ learningId: candidate.id,
123
+ evaluationId: evaluation.id,
124
+ evaluationDigest: evaluation.digest,
125
+ candidateAdmissionDigest: evaluation.candidateAdmission.digest,
126
+ evidenceDigest,
127
+ independenceDigest: attestation.independenceDigest,
128
+ sourceClass: attestation.sourceClass,
129
+ targetDigest,
130
+ scopeDigest,
131
+ reasonCode,
132
+ reasonDigest,
133
+ revokedAt: timestamp
134
+ });
135
+ const receipt = { ...payload, digest: digest(payload) };
136
+ state.evidenceSourceAttestationRevocations.push(receipt);
137
+ state.evidenceSourceAttestationRevocations.sort((a, b) => a.id.localeCompare(b.id));
138
+ return { receipt, learningPath, unchanged: false, authority: "context-only" };
139
+ });
140
+ }
141
+
142
+ export async function revokeLearningValidation({
143
+ root = process.cwd(), validationLeaseId, reasonCode, reason, confirmation, now = new Date()
144
+ }) {
145
+ if (confirmation !== "local-validation-revocation-confirmed") {
146
+ throw new Error("validation revocation requires explicit local confirmation");
147
+ }
148
+ if (!ID_RE.test(validationLeaseId || "")) throw new Error("validationLeaseId must be a stable identifier");
149
+ if (!VALIDATION_REVOCATION_REASONS.has(reasonCode)) {
150
+ throw new Error("reasonCode must be decision-invalid, cohort-invalid, binding-invalid, scope-invalid, duplicate, or other");
151
+ }
152
+ const revokeReason = safeText(reason, "reason", 500);
153
+ const timestamp = date(now, "now");
154
+ return mutation(root, (state, _catalog, learningPath) => {
155
+ const lease = validationLeaseRecord(state, validationLeaseId);
156
+ if (!lease || !storedValidationLeaseStructure(lease)) {
157
+ throw new Error(`unknown learning validation lease: ${validationLeaseId}`);
158
+ }
159
+ const candidate = state.candidates.find((entry) => entry.id === lease.learningId);
160
+ const evaluation = state.evaluations.find((entry) => entry.id === lease.evaluationId
161
+ && entry.digest === lease.evaluationDigest);
162
+ const binding = state.evaluationBindings.find((entry) => entry.evaluationId === lease.evaluationId
163
+ && entry.evaluationDigest === lease.evaluationDigest);
164
+ if (!candidate || !evaluation || !binding || binding.digest !== lease.evaluatorRegistryBindingDigest) {
165
+ throw new Error("validation revocation requires its immutable candidate, evaluation, and evaluator binding");
166
+ }
167
+ const activeChain = validationLeaseChain(state, candidate);
168
+ if (!activeChain.some((entry) => entry.id === lease.id && entry.digest === lease.digest)) {
169
+ throw new Error("validation revocation requires a lease in the candidate's current immutable validation chain");
170
+ }
171
+ const targetDigest = learningTargetForCandidate(candidate).digest;
172
+ const scopeDigest = digest(lease.scope);
173
+ const reasonDigest = digest(revokeReason);
174
+ const existing = revokedValidation(state, validationLeaseId);
175
+ if (existing) {
176
+ if (existing.reasonCode !== reasonCode || existing.reasonDigest !== reasonDigest
177
+ || existing.learningId !== candidate.id || existing.evaluationId !== evaluation.id
178
+ || existing.evaluationDigest !== evaluation.digest || existing.evaluatorBindingDigest !== binding.digest
179
+ || existing.validationLeaseDigest !== lease.digest || existing.targetDigest !== targetDigest
180
+ || existing.scopeDigest !== scopeDigest) {
181
+ throw new Error("learning validation revocations are immutable");
182
+ }
183
+ return { receipt: existing, learningPath, unchanged: true, authority: "context-only" };
184
+ }
185
+ const id = `validation-revocation:${createHash("sha256")
186
+ .update(`${lease.id}\0${lease.digest}\0${targetDigest}`).digest("hex").slice(0, 32)}`;
187
+ const payload = validationRevocationPayload({
188
+ id,
189
+ learningId: candidate.id,
190
+ evaluationId: evaluation.id,
191
+ evaluationDigest: evaluation.digest,
192
+ evaluatorBindingDigest: binding.digest,
193
+ validationLeaseId: lease.id,
194
+ validationLeaseDigest: lease.digest,
195
+ targetDigest,
196
+ scopeDigest,
197
+ reasonCode,
198
+ reasonDigest,
199
+ revokedAt: timestamp
200
+ });
201
+ const receipt = { ...payload, digest: digest(payload) };
202
+ state.validationRevocations.push(receipt);
203
+ state.validationRevocations.sort((a, b) => a.id.localeCompare(b.id));
204
+ return { receipt, learningPath, unchanged: false, authority: "context-only" };
205
+ });
206
+ }
207
+
208
+ export async function beginLearningRevalidation({
209
+ root = process.cwd(), learningId, confirmLocalValidation = false, now = new Date()
210
+ }) {
211
+ if (!confirmLocalValidation) throw new Error("revalidation requires explicit local confirmation");
212
+ if (!ID_RE.test(learningId || "")) throw new Error("learningId is required");
213
+ const timestamp = date(now, "now");
214
+ return mutation(root, (state, _catalog, learningPath) => {
215
+ const candidate = state.candidates.find((entry) => entry.id === learningId);
216
+ if (revokedEvaluationForCandidate(state, candidate)) {
217
+ throw new Error("revalidation baseline contains an explicitly revoked evaluation contract");
218
+ }
219
+ if (revokedEvidenceSourceAttestationForCandidate(state, candidate)) {
220
+ throw new Error("revalidation baseline contains an explicitly revoked evidence source attestation");
221
+ }
222
+ if (revokedApplicationForCandidate(state, candidate)) {
223
+ throw new Error("revalidation baseline contains an explicitly revoked application");
224
+ }
225
+ if (revokedOutcomeForCandidate(state, candidate)) {
226
+ throw new Error("revalidation baseline contains an explicitly revoked outcome");
227
+ }
228
+ const validation = validationLeaseState(state, candidate, timestamp);
229
+ if (validation.status !== "active"
230
+ || !REGISTRY_BOUND_EVALUATIONS.has(validation.evaluation?.schema)) {
231
+ throw new Error("revalidation requires one current registry-bound validation lease");
232
+ }
233
+ const canary = candidate.promotion.canary;
234
+ if (canary.revalidation?.status === "active"
235
+ && new Date(canary.revalidation.expiresAt).getTime() > new Date(timestamp).getTime()) {
236
+ return { candidate, revalidation: canary.revalidation, learningPath, unchanged: true };
237
+ }
238
+ const possibleExpiry = Math.min(new Date(validation.evaluation.expiresAt).getTime(),
239
+ new Date(timestamp).getTime()
240
+ + evaluationStalenessPolicy(validation.evaluation, state.config).canaryTtlDays * 86400000);
241
+ if (possibleExpiry <= new Date(validation.lease.expiresAt).getTime()) {
242
+ throw new Error("revalidation cannot extend evidence within the current evaluation contract window");
243
+ }
244
+ const baselineReferences = validation.lease.schema === "agentspine.learning-validation/v1"
245
+ ? validation.lease.beforeOutcomes : validation.lease.baselineOutcomes;
246
+ const baselines = baselineReferences.map((reference) => state.outcomes.find((outcome) =>
247
+ outcome.id === reference.id && outcome.digest === reference.digest));
248
+ if (baselines.some((outcome) => outcome?.schema !== "agentspine.learning-outcome/v9"
249
+ || !DIGEST_RE.test(outcome.measurement?.evaluatorRootDigest || ""))) {
250
+ throw new Error("revalidation requires a complete root-bound frozen baseline cohort");
251
+ }
252
+ const orderedRoots = [...baselines].sort((a, b) => a.measurement.evaluatorId.localeCompare(b.measurement.evaluatorId))
253
+ .map((outcome, index) => {
254
+ const trial = {
255
+ evaluationId: validation.evaluation.id,
256
+ evaluationDigest: validation.evaluation.digest,
257
+ predecessorValidationId: validation.lease.id,
258
+ predecessorValidationDigest: validation.lease.digest,
259
+ slot: index + 1,
260
+ evaluatorId: outcome.measurement.evaluatorId,
261
+ evaluatorRootDigest: outcome.measurement.evaluatorRootDigest,
262
+ runId: `run:revalidation:${randomUUID()}`,
263
+ benchmark: validation.evaluation.benchmark,
264
+ caseCount: outcome.coverage.caseCount
265
+ };
266
+ return { slot: trial.slot, evaluatorId: trial.evaluatorId,
267
+ evaluatorRootDigest: trial.evaluatorRootDigest, runId: trial.runId,
268
+ caseCount: trial.caseCount, trialDigest: revalidationTrialDigest(trial), authority: "context-only" };
269
+ });
270
+ const revalidationPayload = revalidationWindowPayload({
271
+ schema: "agentspine.learning-revalidation-window/v4",
272
+ id: `revalidation:${randomUUID()}`,
273
+ status: "active",
274
+ startedAt: timestamp,
275
+ expiresAt: validation.lease.expiresAt,
276
+ predecessorValidationId: validation.lease.id,
277
+ predecessorValidationDigest: validation.lease.digest,
278
+ selection: { mode: "first-admitted-trials", requiredDeliveries: orderedRoots.length,
279
+ evaluatorRoots: orderedRoots, authority: "context-only" }
280
+ });
281
+ const revalidation = { ...revalidationPayload, digest: digest(revalidationPayload) };
282
+ preserve(state, "learning-candidate", candidate, timestamp);
283
+ const updated = {
284
+ ...candidate,
285
+ promotion: { ...candidate.promotion, canary: { ...canary, revalidation } },
286
+ updatedAt: timestamp,
287
+ authority: "context-only"
288
+ };
289
+ state.candidates = state.candidates.map((entry) => entry.id === learningId ? updated : entry);
290
+ return { candidate: updated, revalidation, learningPath, unchanged: false };
291
+ });
292
+ }