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.
- package/agent-spine-plugin/.claude-plugin/marketplace.json +1 -1
- package/agent-spine-plugin/.claude-plugin/plugin.json +1 -1
- package/agent-spine-plugin/.codex-plugin/plugin.json +2 -1
- package/agent-spine-plugin/CHANGELOG.md +1581 -0
- package/agent-spine-plugin/README.md +30 -4
- package/agent-spine-plugin/blun.plugin.json +3 -3
- package/agent-spine-plugin/docs/acceptance.md +61 -0
- package/agent-spine-plugin/docs/assignment-continuation.md +48 -0
- package/agent-spine-plugin/docs/host-integration.md +178 -0
- package/agent-spine-plugin/docs/preflight-recall.md +69 -0
- package/agent-spine-plugin/docs/preservation-contract.md +53 -0
- package/agent-spine-plugin/docs/quality-gates.md +50 -0
- package/agent-spine-plugin/docs/releasing.md +85 -0
- package/agent-spine-plugin/docs/session-timeline.md +251 -0
- package/agent-spine-plugin/docs/source-roots.md +113 -0
- package/agent-spine-plugin/docs/structured-completion.md +67 -0
- package/agent-spine-plugin/docs/world-model.md +94 -0
- package/agent-spine-plugin/hooks/codex.json +2 -2
- package/agent-spine-plugin/hooks/hooks.json +1 -1
- package/agent-spine-plugin/hooks/version.json +1 -1
- package/agent-spine-plugin/package.json +13 -3
- package/agent-spine-plugin/scripts/check-codex-install.js +226 -0
- package/agent-spine-plugin/scripts/check-hosts.js +14 -5
- package/agent-spine-plugin/scripts/check-install-hook.js +226 -0
- package/agent-spine-plugin/scripts/check-install-selfstarter.js +154 -0
- package/agent-spine-plugin/scripts/check-install.js +478 -0
- package/agent-spine-plugin/scripts/check-line-budget.js +58 -0
- package/agent-spine-plugin/scripts/check-syntax.js +29 -0
- package/agent-spine-plugin/scripts/github-actions.js +11 -0
- package/agent-spine-plugin/scripts/hermetic-process.js +183 -0
- package/agent-spine-plugin/scripts/release-check.js +145 -0
- package/agent-spine-plugin/scripts/run-acceptance.js +19 -0
- package/agent-spine-plugin/scripts/run-checks.js +47 -0
- package/agent-spine-plugin/scripts/run-tests-hermetic.js +89 -0
- package/agent-spine-plugin/skills/agent-spine/SKILL.md +16 -2
- package/agent-spine-plugin/spine-example/1-identity.md +12 -0
- package/agent-spine-plugin/spine-example/2-voice.md +6 -0
- package/agent-spine-plugin/spine-example/3-conduct.md +8 -0
- package/agent-spine-plugin/spine-example/4-history.md +4 -0
- package/agent-spine-plugin/src/cli-agent.js +296 -0
- package/agent-spine-plugin/src/cli-attention.js +95 -0
- package/agent-spine-plugin/src/cli-autonomy.js +36 -0
- package/agent-spine-plugin/src/cli-common.js +71 -0
- package/agent-spine-plugin/src/cli-continuity.js +116 -0
- package/agent-spine-plugin/src/cli-core.js +128 -0
- package/agent-spine-plugin/src/cli-diagnostics.js +291 -0
- package/agent-spine-plugin/src/cli-host.js +21 -0
- package/agent-spine-plugin/src/cli-learning.js +305 -0
- package/agent-spine-plugin/src/cli-premortem.js +16 -0
- package/agent-spine-plugin/src/cli-sharing.js +230 -0
- package/agent-spine-plugin/src/cli.js +40 -1350
- package/agent-spine-plugin/src/codex-reader-launcher.js +182 -0
- package/agent-spine-plugin/src/hook.js +185 -567
- package/agent-spine-plugin/src/index.js +16 -2
- package/agent-spine-plugin/src/lib/acceptance.js +113 -11
- package/agent-spine-plugin/src/lib/action-lesson-recall.js +53 -0
- package/agent-spine-plugin/src/lib/attention-context.js +167 -0
- package/agent-spine-plugin/src/lib/attention-events.js +113 -0
- package/agent-spine-plugin/src/lib/attention-privacy.js +72 -0
- package/agent-spine-plugin/src/lib/attention-schema.js +164 -0
- package/agent-spine-plugin/src/lib/attention-storage.js +93 -0
- package/agent-spine-plugin/src/lib/attention.js +9 -600
- package/agent-spine-plugin/src/lib/audit-premortem.js +223 -0
- package/agent-spine-plugin/src/lib/audit.js +56 -6
- package/agent-spine-plugin/src/lib/autonomy-policy.js +110 -0
- package/agent-spine-plugin/src/lib/autonomy-store.js +202 -0
- package/agent-spine-plugin/src/lib/autonomy.js +8 -0
- package/agent-spine-plugin/src/lib/briefing.js +67 -4
- package/agent-spine-plugin/src/lib/catalog-document-read.js +51 -0
- package/agent-spine-plugin/src/lib/catalog.js +1 -1
- package/agent-spine-plugin/src/lib/codex-installation.js +231 -0
- package/agent-spine-plugin/src/lib/codex-skill-installation.js +211 -0
- package/agent-spine-plugin/src/lib/context.js +3 -1
- package/agent-spine-plugin/src/lib/delivery-agent-usage.js +224 -0
- package/agent-spine-plugin/src/lib/delivery-assignment.js +220 -0
- package/agent-spine-plugin/src/lib/delivery-command-actions.js +453 -0
- package/agent-spine-plugin/src/lib/delivery-knowledge.js +78 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-binding.js +107 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-closure.js +171 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-codec.js +45 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-correction.js +101 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-file.js +21 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-index.js +493 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-inspection.js +37 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-recovery.js +120 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-rejection.js +65 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-results.js +28 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-session-guard.js +46 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-write-ledger.js +285 -0
- package/agent-spine-plugin/src/lib/delivery-premortem.js +499 -0
- package/agent-spine-plugin/src/lib/delivery-shell-heredoc.js +115 -0
- package/agent-spine-plugin/src/lib/delivery-shell-substitutions.js +111 -0
- package/agent-spine-plugin/src/lib/delivery-shell-wrapper.js +64 -0
- package/agent-spine-plugin/src/lib/delivery-target.js +73 -0
- package/agent-spine-plugin/src/lib/delivery-verification.js +445 -0
- package/agent-spine-plugin/src/lib/documents.js +27 -5
- package/agent-spine-plugin/src/lib/filesystem-retry.js +2 -0
- package/agent-spine-plugin/src/lib/gateway-common.js +68 -0
- package/agent-spine-plugin/src/lib/gateway-control.js +302 -0
- package/agent-spine-plugin/src/lib/gateway-delivery.js +103 -0
- package/agent-spine-plugin/src/lib/gateway-execution.js +350 -0
- package/agent-spine-plugin/src/lib/gateway-host-lifecycle.js +185 -0
- package/agent-spine-plugin/src/lib/gateway-inspection.js +85 -0
- package/agent-spine-plugin/src/lib/gateway-knowledge.js +129 -0
- package/agent-spine-plugin/src/lib/gateway-policy-provenance.js +197 -0
- package/agent-spine-plugin/src/lib/gateway-premortem-disposition.js +82 -0
- package/agent-spine-plugin/src/lib/gateway-premortem.js +363 -0
- package/agent-spine-plugin/src/lib/gateway-runs.js +300 -0
- package/agent-spine-plugin/src/lib/gateway-runtime-identity.js +31 -0
- package/agent-spine-plugin/src/lib/gateway-runtime-records.js +21 -0
- package/agent-spine-plugin/src/lib/gateway-runtime.js +18 -1623
- package/agent-spine-plugin/src/lib/gateway-state-transaction.js +356 -0
- package/agent-spine-plugin/src/lib/gateway-state.js +342 -0
- package/agent-spine-plugin/src/lib/hook-artifact-guards.js +356 -0
- package/agent-spine-plugin/src/lib/hook-audit.js +16 -2
- package/agent-spine-plugin/src/lib/hook-briefing-use.js +68 -0
- package/agent-spine-plugin/src/lib/hook-context.js +424 -0
- package/agent-spine-plugin/src/lib/hook-final-message.js +26 -0
- package/agent-spine-plugin/src/lib/hook-input.js +27 -0
- package/agent-spine-plugin/src/lib/hook-output.js +140 -0
- package/agent-spine-plugin/src/lib/hook-premortem.js +257 -0
- package/agent-spine-plugin/src/lib/hook-process-advisory.js +37 -0
- package/agent-spine-plugin/src/lib/hook-protection.js +95 -0
- package/agent-spine-plugin/src/lib/hook-stop-verification.js +84 -0
- package/agent-spine-plugin/src/lib/hook-timeline.js +91 -0
- package/agent-spine-plugin/src/lib/identifier-analysis.js +446 -0
- package/agent-spine-plugin/src/lib/indexed-memory.js +23 -6
- package/agent-spine-plugin/src/lib/knowledge-evidence.js +431 -0
- package/agent-spine-plugin/src/lib/learning-applications.js +441 -0
- package/agent-spine-plugin/src/lib/learning-candidates.js +274 -0
- package/agent-spine-plugin/src/lib/learning-context.js +130 -0
- package/agent-spine-plugin/src/lib/learning-delivery-contracts.js +310 -0
- package/agent-spine-plugin/src/lib/learning-evaluation-contracts.js +350 -0
- package/agent-spine-plugin/src/lib/learning-evaluation-registration.js +372 -0
- package/agent-spine-plugin/src/lib/learning-evaluation-revocation.js +292 -0
- package/agent-spine-plugin/src/lib/learning-evidence-contracts.js +321 -0
- package/agent-spine-plugin/src/lib/learning-findings.js +458 -0
- package/agent-spine-plugin/src/lib/learning-measurement-contracts.js +285 -0
- package/agent-spine-plugin/src/lib/learning-measurements.js +265 -0
- package/agent-spine-plugin/src/lib/learning-outcome-contracts.js +165 -0
- package/agent-spine-plugin/src/lib/learning-outcomes.js +343 -0
- package/agent-spine-plugin/src/lib/learning-reconciliation.js +290 -0
- package/agent-spine-plugin/src/lib/learning-retry-contracts.js +188 -0
- package/agent-spine-plugin/src/lib/learning-schema.js +229 -0
- package/agent-spine-plugin/src/lib/learning-scope-targets.js +424 -0
- package/agent-spine-plugin/src/lib/learning-state-upgrade.js +477 -0
- package/agent-spine-plugin/src/lib/learning-status-configuration.js +476 -0
- package/agent-spine-plugin/src/lib/learning-storage.js +203 -0
- package/agent-spine-plugin/src/lib/learning-trial-recovery.js +218 -0
- package/agent-spine-plugin/src/lib/learning-validation-contracts.js +375 -0
- package/agent-spine-plugin/src/lib/learning-validation-renewal.js +298 -0
- package/agent-spine-plugin/src/lib/learning-validation-runtime.js +234 -0
- package/agent-spine-plugin/src/lib/learning.js +36 -6923
- package/agent-spine-plugin/src/lib/lesson-recall-session.js +172 -0
- package/agent-spine-plugin/src/lib/mcp-autonomy-tools.js +37 -0
- package/agent-spine-plugin/src/lib/mcp-delivery-completion.js +124 -0
- package/agent-spine-plugin/src/lib/mcp-delivery-tools.js +45 -0
- package/agent-spine-plugin/src/lib/mcp-premortem.js +68 -0
- package/agent-spine-plugin/src/lib/mcp-runtime.js +269 -0
- package/agent-spine-plugin/src/lib/mcp-source-context.js +51 -0
- package/agent-spine-plugin/src/lib/mcp-timeline-tools.js +164 -0
- package/agent-spine-plugin/src/lib/mcp-world-tools.js +52 -0
- package/agent-spine-plugin/src/lib/owned-file-lock.js +30 -10
- package/agent-spine-plugin/src/lib/project-portfolio.js +176 -0
- package/agent-spine-plugin/src/lib/selfstarter-core.js +382 -0
- package/agent-spine-plugin/src/lib/selfstarter-jobs.js +149 -0
- package/agent-spine-plugin/src/lib/selfstarter-lease.js +204 -0
- package/agent-spine-plugin/src/lib/selfstarter-policy.js +90 -0
- package/agent-spine-plugin/src/lib/selfstarter-workspace.js +111 -0
- package/agent-spine-plugin/src/lib/selfstarter.js +11 -911
- package/agent-spine-plugin/src/lib/session-timeline-auth.js +316 -0
- package/agent-spine-plugin/src/lib/session-timeline-codex.js +58 -0
- package/agent-spine-plugin/src/lib/session-timeline-contract.js +48 -0
- package/agent-spine-plugin/src/lib/session-timeline-enrollment-source.js +41 -0
- package/agent-spine-plugin/src/lib/session-timeline-enrollment-storage.js +132 -0
- package/agent-spine-plugin/src/lib/session-timeline-enrollment-transport.js +17 -0
- package/agent-spine-plugin/src/lib/session-timeline-enrollment.js +500 -0
- package/agent-spine-plugin/src/lib/session-timeline-event-extract.js +157 -0
- package/agent-spine-plugin/src/lib/session-timeline-host-origin.js +74 -0
- package/agent-spine-plugin/src/lib/session-timeline-host-receipt.js +117 -0
- package/agent-spine-plugin/src/lib/session-timeline-invocation.js +201 -0
- package/agent-spine-plugin/src/lib/session-timeline-king.js +79 -0
- package/agent-spine-plugin/src/lib/session-timeline-prior.js +59 -0
- package/agent-spine-plugin/src/lib/session-timeline-provider.js +34 -0
- package/agent-spine-plugin/src/lib/session-timeline-query.js +55 -0
- package/agent-spine-plugin/src/lib/session-timeline-results.js +31 -0
- package/agent-spine-plugin/src/lib/session-timeline-root.js +11 -0
- package/agent-spine-plugin/src/lib/session-timeline-search.js +82 -0
- package/agent-spine-plugin/src/lib/session-timeline-sid-acl.js +217 -0
- package/agent-spine-plugin/src/lib/session-timeline-source.js +83 -0
- package/agent-spine-plugin/src/lib/session-timeline-state.js +45 -0
- package/agent-spine-plugin/src/lib/session-timeline-transport.js +50 -0
- package/agent-spine-plugin/src/lib/session-timeline-windows-acl.js +148 -0
- package/agent-spine-plugin/src/lib/session-timeline.js +453 -0
- package/agent-spine-plugin/src/lib/source-roots.js +69 -136
- package/agent-spine-plugin/src/lib/source-tree-scan.js +178 -0
- package/agent-spine-plugin/src/lib/task-knowledge-context.js +78 -0
- package/agent-spine-plugin/src/lib/timeline-tool-guard.js +202 -0
- package/agent-spine-plugin/src/lib/world-knowledge.js +249 -0
- package/agent-spine-plugin/src/lib/world-model.js +278 -0
- package/agent-spine-plugin/src/mcp.js +20 -161
- package/agent-spine-plugin/src/version.js +1 -1
- package/agent-spine-plugin/src/worker.js +22 -5
- package/bin/agent-resume-snapshot.cjs +2 -2
- package/bin/agentspine-king-goal-inbox.mjs +111 -0
- package/bin/agentspine-king-goal-intake.mjs +106 -0
- package/bin/baseline-skill-performance-policy.cjs +1 -16
- package/bin/core-bootstrap.js +2 -0
- package/bin/curiosity-scout-policy.cjs +5 -1
- package/bin/input-draft-persistence.cjs +2 -2
- package/bin/king-tui-function-contract.json +33 -0
- package/bin/launcher-restart-policy.cjs +150 -0
- package/bin/launcher-runtime.js +56 -44
- package/bin/managed-context-startup-policy.cjs +27 -0
- package/bin/managed-plugin-selection.cjs +116 -0
- package/bin/mistake-relevance-policy.cjs +1 -1
- package/bin/observer-hooks.cjs +14 -0
- package/bin/oversized-context-offload-policy.cjs +86 -0
- package/bin/plugin-bootstrap.js +7 -40
- package/bin/proactive-compaction-policy.cjs +1 -1
- package/bin/provider-model-refresh-deadline.cjs +53 -0
- package/bin/provider-model-refresh-policy.cjs +107 -0
- package/bin/release-artifact-freeze-policy.cjs +30 -0
- package/bin/repeated-user-message-projection.cjs +3 -126
- package/bin/research-page-result.cjs +74 -0
- package/bin/runtime-exit-ledger.cjs +1 -0
- package/bin/session-compaction-policy.cjs +84 -0
- package/bin/skill-listing-performance-policy.cjs +2 -2
- package/bin/standard-tools-bootstrap.js +0 -37
- package/bin/subagent-skill-policy.cjs +3 -1
- package/bin/telegram-approval-relay.cjs +12 -7
- package/bin/telegram-private-conversation-policy.cjs +3 -2
- package/bin/telegram-queue-handoff-policy.cjs +24 -0
- package/bin/thinking-activity-status-policy.cjs +1 -1
- package/bin/thinking-only-guard.cjs +16 -12
- package/bin/tool-call-loop-policy.cjs +0 -2
- package/bin/tool-result-offload-policy.cjs +11 -2
- package/bin/tui-functional-contract.cjs +55 -0
- package/bin/update-notice.js +18 -14
- package/bin/user-message-offload-policy.cjs +1 -1
- package/bin/user-prompt-hook-origin-policy.cjs +34 -0
- package/bin/windows-node-crash-dump.cjs +110 -0
- package/blun.mjs +2517 -1048
- package/codebase-index/codebase_index.py +4 -3
- package/package.json +3 -17
- package/standard-skills/research-evidence/SKILL.md +39 -0
- package/standard-skills/research-evidence/references/evidence-format.md +82 -0
- package/standard-skills/research-evidence/scripts/evidence-collection.cjs +254 -0
- package/standard-skills/research-evidence/scripts/score-report.cjs +112 -0
- package/standard-skills/web-lesen/SKILL.md +37 -22
- package/standard-skills/web-lesen/scripts/crawl_public.py +376 -0
- package/telegram-plugin/DELIVERY.md +36 -0
- package/telegram-plugin/bin/telegram-approval-relay.cjs +13 -7
- package/telegram-plugin/bin/telegram-launcher-status-queue.cjs +122 -0
- package/telegram-plugin/bin/telegram-private-conversation-policy.cjs +3 -2
- package/telegram-plugin/bin/telegram-reply-parts.cjs +149 -0
- package/telegram-plugin/dist/bridge.mjs +11 -57
- package/telegram-plugin/dist/mcp-server.mjs +33 -4
- package/agent-spine-plugin/skill/SKILL.md +0 -76
- package/bin/mnemo-connect-heartbeat.cjs +0 -204
- package/bin/mnemo-tool-agent-policy.cjs +0 -22
- 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
|
+
}
|