@kontextmind/kxm 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +19 -0
- package/.kxm/README.md +14 -0
- package/.kxm/assets/README.md +5 -0
- package/.kxm/assets/retrospectives/README.md +5 -0
- package/.kxm/config/README.md +5 -0
- package/.kxm/config/agents.json +43 -0
- package/.kxm/config/env.example +56 -0
- package/.kxm/config/update.example.yaml +9 -0
- package/.kxm/config/workflows/fix.json +160 -0
- package/.kxm/config/workflows/jira-development.json +116 -0
- package/.kxm/config/workflows/provenance-quorum.json +150 -0
- package/.kxm/config/workflows/v04-dogfood.json +72 -0
- package/CHANGELOG.md +465 -0
- package/LICENSE +21 -0
- package/README.md +306 -0
- package/SECURITY.md +72 -0
- package/docs/README.md +48 -0
- package/docs/agent-communication-envelopes-and-gates.md +553 -0
- package/docs/architecture.md +242 -0
- package/docs/assignment-runner.md +241 -0
- package/docs/configuration.md +361 -0
- package/docs/continuous-improvement.md +114 -0
- package/docs/getting-started.md +253 -0
- package/docs/kxm-handbook.md +1090 -0
- package/docs/operations.md +205 -0
- package/docs/provenance-gates.md +291 -0
- package/docs/skills.md +45 -0
- package/docs/templates/README.md +95 -0
- package/docs/templates/adr.md +88 -0
- package/docs/templates/architecture.md +120 -0
- package/docs/templates/bug-fix.md +109 -0
- package/docs/templates/feature.md +108 -0
- package/docs/templates/handoff.md +72 -0
- package/docs/templates/postmortem.md +77 -0
- package/docs/templates/research.md +100 -0
- package/docs/templates/review.md +85 -0
- package/docs/templates/runbook.md +73 -0
- package/docs/templates/test-plan.md +87 -0
- package/docs/templates/test-report.md +72 -0
- package/docs/test-matrix.md +121 -0
- package/docs/troubleshooting.md +249 -0
- package/docs/vnext/README.md +62 -0
- package/docs/vnext/architecture.md +185 -0
- package/docs/vnext/effects-and-recovery.md +172 -0
- package/docs/vnext/lifecycles.md +235 -0
- package/docs/vnext/migration.md +220 -0
- package/docs/vnext/routing.md +184 -0
- package/docs/vnext/synchronization.md +172 -0
- package/docs/vnext/terminology.md +240 -0
- package/docs/vnext/validation.md +335 -0
- package/docs/webhook-workflows.md +240 -0
- package/docs/workflow-guide.md +1150 -0
- package/examples/README.md +102 -0
- package/examples/provenance-workflow.json +40 -0
- package/examples/requester.ts +30 -0
- package/examples/reviewer-agent.ts +29 -0
- package/examples/roundtrip.ts +46 -0
- package/examples/vnext/.kxm/agents/coordinator.yaml +16 -0
- package/examples/vnext/.kxm/agents/critic-1.yaml +16 -0
- package/examples/vnext/.kxm/agents/critic-2.yaml +15 -0
- package/examples/vnext/.kxm/agents/critic-3.yaml +15 -0
- package/examples/vnext/.kxm/agents/implementer.yaml +15 -0
- package/examples/vnext/.kxm/agents/planner.yaml +13 -0
- package/examples/vnext/.kxm/agents/reproducer.yaml +15 -0
- package/examples/vnext/.kxm/agents/reviewer.yaml +15 -0
- package/examples/vnext/.kxm/gates.yaml +8 -0
- package/examples/vnext/.kxm/models/critic-claude.yaml +11 -0
- package/examples/vnext/.kxm/models/critic-gemini.yaml +11 -0
- package/examples/vnext/.kxm/models/critic-grok.yaml +12 -0
- package/examples/vnext/.kxm/models/implementation.yaml +14 -0
- package/examples/vnext/.kxm/models/primary.yaml +17 -0
- package/examples/vnext/.kxm/prices.yaml +111 -0
- package/examples/vnext/.kxm/project/env.yaml +7 -0
- package/examples/vnext/.kxm/project.yaml +32 -0
- package/examples/vnext/.kxm/repo/repo.yaml +8 -0
- package/examples/vnext/.kxm/workflows/default.yaml +92 -0
- package/examples/vnext/.kxm/workflows/fix.yaml +376 -0
- package/examples/vnext/.kxm/workflows/improve.yaml +57 -0
- package/examples/vnext/README.md +53 -0
- package/examples/vnext/records/assignment-result-recorded.json +63 -0
- package/examples/vnext/records/assignment-result.json +46 -0
- package/examples/vnext/records/context-candidate.json +42 -0
- package/examples/vnext/records/delivery-manifest.json +66 -0
- package/examples/vnext/records/effect-uncertainty-resolved-sync.json +67 -0
- package/examples/vnext/records/effect-uncertainty-resolved.json +62 -0
- package/examples/vnext/records/run-created.json +54 -0
- package/examples/vnext/records/sync-event.json +65 -0
- package/examples/vnext/repositories/api/.kxm/repo/env.yaml +7 -0
- package/examples/vnext/repositories/api/.kxm/repo/repo.yaml +8 -0
- package/examples/vnext/repositories/web/.kxm/repo/repo.yaml +8 -0
- package/examples/workflow-signal.ts +63 -0
- package/package.json +129 -0
- package/plugins/kxm/.claude-plugin/plugin.json +73 -0
- package/plugins/kxm/.mcp.json +19 -0
- package/plugins/kxm/README.md +93 -0
- package/plugins/kxm/dist/cli.js +42853 -0
- package/plugins/kxm/dist/client.js +416 -0
- package/plugins/kxm/dist/core.js +1823 -0
- package/plugins/kxm/dist/extension.js +3797 -0
- package/plugins/kxm/dist/mcp-server.js +17104 -0
- package/plugins/kxm/dist/runtime.js +23361 -0
- package/plugins/kxm/dist/server.js +13640 -0
- package/plugins/kxm/dist/vnext-runtime-supervisor.js +21109 -0
- package/plugins/kxm/package.json +12 -0
- package/plugins/kxm/skills/kxm/SKILL.md +97 -0
- package/plugins/kxm/skills/kxm/references/protocol.md +103 -0
- package/plugins/kxm/skills/kxm-session/SKILL.md +53 -0
- package/plugins/kxm/src/arbiter.ts +355 -0
- package/plugins/kxm/src/artifacts-exist.ts +62 -0
- package/plugins/kxm/src/autocomplete.ts +236 -0
- package/plugins/kxm/src/cli.ts +3707 -0
- package/plugins/kxm/src/client.ts +614 -0
- package/plugins/kxm/src/commands.ts +1063 -0
- package/plugins/kxm/src/config.ts +290 -0
- package/plugins/kxm/src/context/providers.ts +101 -0
- package/plugins/kxm/src/context-packet.ts +332 -0
- package/plugins/kxm/src/context.ts +499 -0
- package/plugins/kxm/src/core.ts +6 -0
- package/plugins/kxm/src/database.ts +563 -0
- package/plugins/kxm/src/diagnostics.ts +184 -0
- package/plugins/kxm/src/envelope.ts +118 -0
- package/plugins/kxm/src/extension.ts +895 -0
- package/plugins/kxm/src/external-effects.ts +299 -0
- package/plugins/kxm/src/github-watch.ts +255 -0
- package/plugins/kxm/src/hub-binding.ts +160 -0
- package/plugins/kxm/src/hub.ts +2502 -0
- package/plugins/kxm/src/improve.ts +383 -0
- package/plugins/kxm/src/inbox.ts +10 -0
- package/plugins/kxm/src/kxm-install-kind.ts +113 -0
- package/plugins/kxm/src/kxm-update-config.ts +39 -0
- package/plugins/kxm/src/kxm-update.ts +238 -0
- package/plugins/kxm/src/local-snapshot.ts +406 -0
- package/plugins/kxm/src/logger.ts +198 -0
- package/plugins/kxm/src/mcp-server.ts +143 -0
- package/plugins/kxm/src/memory.ts +385 -0
- package/plugins/kxm/src/nous-pi.ts +287 -0
- package/plugins/kxm/src/nous-provider.ts +729 -0
- package/plugins/kxm/src/price-calc.ts +87 -0
- package/plugins/kxm/src/prices.ts +121 -0
- package/plugins/kxm/src/protocol.ts +172 -0
- package/plugins/kxm/src/recovery.ts +211 -0
- package/plugins/kxm/src/redact.ts +26 -0
- package/plugins/kxm/src/retrospective.ts +400 -0
- package/plugins/kxm/src/routing.ts +830 -0
- package/plugins/kxm/src/runtime.ts +9 -0
- package/plugins/kxm/src/server.ts +117 -0
- package/plugins/kxm/src/session-work.ts +571 -0
- package/plugins/kxm/src/session.ts +184 -0
- package/plugins/kxm/src/skills.ts +535 -0
- package/plugins/kxm/src/state.ts +326 -0
- package/plugins/kxm/src/store.ts +637 -0
- package/plugins/kxm/src/studio-layout.ts +268 -0
- package/plugins/kxm/src/suggest.ts +162 -0
- package/plugins/kxm/src/task-manager.ts +244 -0
- package/plugins/kxm/src/telemetry.ts +116 -0
- package/plugins/kxm/src/tui.ts +1046 -0
- package/plugins/kxm/src/vnext-bindings.ts +403 -0
- package/plugins/kxm/src/vnext-config.ts +1646 -0
- package/plugins/kxm/src/vnext-engine-artifacts.ts +86 -0
- package/plugins/kxm/src/vnext-engine-command.ts +533 -0
- package/plugins/kxm/src/vnext-engine-compile.ts +722 -0
- package/plugins/kxm/src/vnext-engine-evidence.ts +273 -0
- package/plugins/kxm/src/vnext-engine-fold.ts +1400 -0
- package/plugins/kxm/src/vnext-engine-gate-records.ts +583 -0
- package/plugins/kxm/src/vnext-engine-plan.ts +717 -0
- package/plugins/kxm/src/vnext-engine.ts +2458 -0
- package/plugins/kxm/src/vnext-gate-hash.ts +10 -0
- package/plugins/kxm/src/vnext-harness.ts +1142 -0
- package/plugins/kxm/src/vnext-init.ts +430 -0
- package/plugins/kxm/src/vnext-migrate.ts +1848 -0
- package/plugins/kxm/src/vnext-oneshot-producer.ts +424 -0
- package/plugins/kxm/src/vnext-permission.ts +936 -0
- package/plugins/kxm/src/vnext-pi-producer.ts +628 -0
- package/plugins/kxm/src/vnext-repair.ts +1094 -0
- package/plugins/kxm/src/vnext-runtime-owner.ts +320 -0
- package/plugins/kxm/src/vnext-runtime-store.ts +1560 -0
- package/plugins/kxm/src/vnext-runtime-supervisor.ts +586 -0
- package/plugins/kxm/src/vnext-runtime.ts +663 -0
- package/plugins/kxm/src/vnext-template.ts +247 -0
- package/plugins/kxm/src/wiki.ts +313 -0
- package/plugins/kxm/src/workflow.ts +1548 -0
- package/schemas/vnext/README.md +46 -0
- package/schemas/vnext/agent.schema.json +40 -0
- package/schemas/vnext/assignment-result.schema.json +66 -0
- package/schemas/vnext/backup-manifest.schema.json +89 -0
- package/schemas/vnext/candidate.schema.json +109 -0
- package/schemas/vnext/common.schema.json +422 -0
- package/schemas/vnext/context-candidate.schema.json +76 -0
- package/schemas/vnext/context-packet.schema.json +192 -0
- package/schemas/vnext/delivery-manifest.schema.json +159 -0
- package/schemas/vnext/environment.schema.json +66 -0
- package/schemas/vnext/gate-registry.schema.json +109 -0
- package/schemas/vnext/handoff-manifest.schema.json +146 -0
- package/schemas/vnext/init-operation.schema.json +61 -0
- package/schemas/vnext/local-repository-bindings.schema.json +30 -0
- package/schemas/vnext/memory-record.schema.json +45 -0
- package/schemas/vnext/migration-decision.schema.json +26 -0
- package/schemas/vnext/migration-plan.schema.json +123 -0
- package/schemas/vnext/migration-receipt.schema.json +52 -0
- package/schemas/vnext/model.schema.json +42 -0
- package/schemas/vnext/permission-diff.schema.json +57 -0
- package/schemas/vnext/prices.schema.json +115 -0
- package/schemas/vnext/project.schema.json +85 -0
- package/schemas/vnext/repository.schema.json +24 -0
- package/schemas/vnext/run-event.schema.json +460 -0
- package/schemas/vnext/session-brief.schema.json +153 -0
- package/schemas/vnext/sync-event.schema.json +234 -0
- package/schemas/vnext/template-provenance.schema.json +38 -0
- package/schemas/vnext/workflow.schema.json +248 -0
- package/scripts/assignment-run.d.mts +354 -0
- package/scripts/assignment-run.mjs +4451 -0
- package/scripts/build-runtime.mjs +56 -0
- package/scripts/check-generated.mjs +77 -0
- package/scripts/check-versions.mjs +34 -0
- package/scripts/emit-codex-artifacts.d.mts +9 -0
- package/scripts/emit-codex-artifacts.mjs +91 -0
- package/scripts/harness-run.d.mts +83 -0
- package/scripts/harness-run.mjs +2095 -0
- package/scripts/kxm-hub.mjs +105 -0
- package/scripts/kxm-publish-npm.mjs +327 -0
- package/scripts/kxm-release-github.mjs +472 -0
- package/scripts/kxm-runtime-supervisor.mjs +7 -0
- package/scripts/kxm-worker.mjs +1127 -0
- package/scripts/kxm.mjs +27 -0
- package/scripts/roster-policy.d.mts +20 -0
- package/scripts/roster-policy.mjs +161 -0
- package/scripts/smoke-multi-pi.mjs +479 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { gateDefinitionHash } from "./vnext-gate-hash.ts";
|
|
2
|
+
import type { VnextRunPlanEnvelope } from "./vnext-engine-plan.ts";
|
|
3
|
+
import type { VnextRunState } from "./vnext-engine-fold.ts";
|
|
4
|
+
import { vnextCanonicalJson, type JsonValue } from "./vnext-config.ts";
|
|
5
|
+
import {
|
|
6
|
+
assertClosedGateObservation,
|
|
7
|
+
computeGateEvidenceOutcome,
|
|
8
|
+
gateRowContentHash,
|
|
9
|
+
runtimeError,
|
|
10
|
+
type VnextGateAttemptRow,
|
|
11
|
+
type VnextGateEvidenceRow,
|
|
12
|
+
type VnextGateObservationRow,
|
|
13
|
+
type VnextRunEvent,
|
|
14
|
+
type VnextRunEventStore,
|
|
15
|
+
type VnextRunRecord,
|
|
16
|
+
} from "./vnext-runtime-store.ts";
|
|
17
|
+
|
|
18
|
+
export function verifyVnextGateEvidence(
|
|
19
|
+
store: VnextRunEventStore,
|
|
20
|
+
run: VnextRunRecord,
|
|
21
|
+
envelope: VnextRunPlanEnvelope,
|
|
22
|
+
events: readonly VnextRunEvent[],
|
|
23
|
+
state: VnextRunState,
|
|
24
|
+
): void {
|
|
25
|
+
const attempts = store.gateAttemptsForRun(run.runId);
|
|
26
|
+
const observations = store.gateObservationsForRun(run.runId);
|
|
27
|
+
const evidence = store.gateEvidenceForRun(run.runId);
|
|
28
|
+
const counts = store.gateRowCounts(run.runId);
|
|
29
|
+
if (counts.attempts !== attempts.length || counts.observations !== observations.length || counts.evidence !== evidence.length) {
|
|
30
|
+
throw runtimeError("gate_evidence_orphan", run.runId, "gate row counts do not match loaded attempt/observation/evidence tuples");
|
|
31
|
+
}
|
|
32
|
+
const attemptById = new Map(attempts.map((row) => [row.attemptId, row]));
|
|
33
|
+
const observationByAttempt = new Map(observations.map((row) => [row.attemptId, row]));
|
|
34
|
+
const evidenceByAttempt = new Map(evidence.map((row) => [row.attemptId, row]));
|
|
35
|
+
const intentEvents = events.filter((event) => event.eventType === "effect.intent_recorded");
|
|
36
|
+
const observedEvents = events.filter((event) => event.eventType === "effect.observed" || event.eventType === "effect.blocked_uncertain");
|
|
37
|
+
const settledEvents = events.filter((event) => event.eventType === "effect.settled");
|
|
38
|
+
|
|
39
|
+
for (const event of intentEvents) {
|
|
40
|
+
const attemptId = stringField(event, "attemptId");
|
|
41
|
+
const row = attemptById.get(attemptId);
|
|
42
|
+
if (!row) throw runtimeError("gate_attempt_pin_mismatch", run.runId, `effect.intent_recorded ${event.eventId} has no gate_attempts row`);
|
|
43
|
+
assertAttemptMatchesEvent(run, envelope, state, event, row);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
for (const event of observedEvents) {
|
|
47
|
+
const attemptId = stringField(event, "attemptId");
|
|
48
|
+
const receipt = event.payload.receipt as { id?: unknown; hash?: unknown } | undefined;
|
|
49
|
+
const observationId = typeof receipt?.id === "string" ? receipt.id : undefined;
|
|
50
|
+
const row = observationByAttempt.get(attemptId);
|
|
51
|
+
if (!row || row.observationId !== observationId) {
|
|
52
|
+
throw runtimeError("gate_observation_missing", run.runId, `${event.eventType} has no matching observation row`);
|
|
53
|
+
}
|
|
54
|
+
if (row.recordedEventId !== event.eventId) {
|
|
55
|
+
throw runtimeError("gate_observation_missing", run.runId, "observation recorded_event_id does not match the event");
|
|
56
|
+
}
|
|
57
|
+
if (gateRowContentHash("gate_observations", row) !== row.contentHash) {
|
|
58
|
+
throw runtimeError("gate_evidence_hash_mismatch", run.runId, "observation contentHash does not recompute");
|
|
59
|
+
}
|
|
60
|
+
if (typeof receipt?.hash === "string" && receipt.hash !== row.contentHash) {
|
|
61
|
+
throw runtimeError("gate_evidence_hash_mismatch", run.runId, "observation receipt.hash does not match the row");
|
|
62
|
+
}
|
|
63
|
+
const attempt = attemptById.get(attemptId);
|
|
64
|
+
if (!attempt) throw runtimeError("gate_attempt_pin_mismatch", run.runId, "observation has no gate attempt");
|
|
65
|
+
assertShared(run, row, attempt);
|
|
66
|
+
assertClosedGateObservation(attempt.gateKind, row, row.observationId);
|
|
67
|
+
if (event.eventType === "effect.observed" && row.completeness !== "complete" && row.completeness !== "no-start") {
|
|
68
|
+
throw runtimeError("gate_observation_missing", run.runId, "effect.observed requires complete or no-start completeness");
|
|
69
|
+
}
|
|
70
|
+
if (event.eventType === "effect.blocked_uncertain" && row.completeness !== "incomplete") {
|
|
71
|
+
throw runtimeError("gate_observation_missing", run.runId, "blocked_uncertain requires incomplete completeness");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
for (const event of settledEvents) {
|
|
76
|
+
const attemptId = stringField(event, "attemptId");
|
|
77
|
+
const refs = event.payload.evidenceRefs;
|
|
78
|
+
const evaluated = Array.isArray(refs) && refs.length > 0;
|
|
79
|
+
if (evaluated) {
|
|
80
|
+
const row = evidenceByAttempt.get(attemptId);
|
|
81
|
+
const ref = refs[0] as { id?: unknown; hash?: unknown };
|
|
82
|
+
if (!row || row.evidenceId !== ref.id) {
|
|
83
|
+
throw runtimeError("gate_evidence_missing", run.runId, "effect.settled has no matching evidence row");
|
|
84
|
+
}
|
|
85
|
+
if (row.settledEventId !== event.eventId) {
|
|
86
|
+
throw runtimeError("gate_evidence_missing", run.runId, "evidence settled_event_id does not match the event");
|
|
87
|
+
}
|
|
88
|
+
if (gateRowContentHash("gate_evidence", row) !== row.contentHash || (typeof ref.hash === "string" && ref.hash !== row.contentHash)) {
|
|
89
|
+
throw runtimeError("gate_evidence_hash_mismatch", run.runId, "evidence hash does not recompute");
|
|
90
|
+
}
|
|
91
|
+
if (row.outcome !== event.payload.outcome) {
|
|
92
|
+
throw runtimeError("gate_evidence_hash_mismatch", run.runId, "evidence outcome does not match the settled payload");
|
|
93
|
+
}
|
|
94
|
+
const attempt = attemptById.get(attemptId);
|
|
95
|
+
if (!attempt) throw runtimeError("gate_attempt_pin_mismatch", run.runId, "evidence has no gate attempt");
|
|
96
|
+
assertShared(run, row, attempt);
|
|
97
|
+
if (row.expect !== attempt.expect) {
|
|
98
|
+
throw runtimeError("gate_attempt_pin_mismatch", run.runId, "evidence expect does not match the gate attempt");
|
|
99
|
+
}
|
|
100
|
+
const observation = observationByAttempt.get(attemptId);
|
|
101
|
+
if (!observation || observation.observationId !== row.observationId) {
|
|
102
|
+
throw runtimeError("gate_observation_missing", run.runId, "evidence observation_id does not match the attempt observation");
|
|
103
|
+
}
|
|
104
|
+
const recomputed = computeGateEvidenceOutcome(attempt.gateKind, attempt.expect, observation);
|
|
105
|
+
if (row.outcome !== recomputed || event.payload.outcome !== recomputed) {
|
|
106
|
+
throw runtimeError("gate_evidence_hash_mismatch", run.runId, "evidence outcome does not match the observation and expect");
|
|
107
|
+
}
|
|
108
|
+
const step = envelope.plan.steps[row.stepId];
|
|
109
|
+
const required = step?.requiredEvidence.filter((item) => item.kind === "gate") ?? [];
|
|
110
|
+
const expectedKey = required.length === 1 ? required[0]!.key : null;
|
|
111
|
+
if (row.evidenceKey !== expectedKey) {
|
|
112
|
+
throw runtimeError("gate_evidence_unexpected", run.runId, "evidence_key does not match the declared requiredEvidence key");
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
if (evidenceByAttempt.has(attemptId)) {
|
|
116
|
+
throw runtimeError("gate_evidence_unexpected", run.runId, "proof-only settlement must not have a gate_evidence row");
|
|
117
|
+
}
|
|
118
|
+
const observation = observationByAttempt.get(attemptId);
|
|
119
|
+
const receipt = event.payload.receipt as { id?: unknown; hash?: unknown } | undefined;
|
|
120
|
+
if (!observation || observation.observationId !== receipt?.id || observation.contentHash !== receipt?.hash) {
|
|
121
|
+
throw runtimeError("gate_observation_missing", run.runId, "proof-only settlement does not bind the observation");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const namedAttemptIds = new Set(intentEvents.map((event) => stringField(event, "attemptId")));
|
|
127
|
+
for (const row of attempts) {
|
|
128
|
+
if (!namedAttemptIds.has(row.attemptId)) {
|
|
129
|
+
throw runtimeError("gate_evidence_orphan", run.runId, `gate_attempts row ${row.attemptId} is not named by effect.intent_recorded`);
|
|
130
|
+
}
|
|
131
|
+
if (row.runId !== run.runId) throw runtimeError("gate_evidence_orphan", run.runId, "gate attempt belongs to a foreign run");
|
|
132
|
+
if (gateRowContentHash("gate_attempts", row) !== row.contentHash) {
|
|
133
|
+
throw runtimeError("gate_evidence_hash_mismatch", run.runId, "gate attempt contentHash does not recompute");
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
for (const row of observations) {
|
|
137
|
+
if (!attemptById.has(row.attemptId)) {
|
|
138
|
+
throw runtimeError("gate_evidence_orphan", run.runId, `observation ${row.observationId} is owned by this run but names a foreign attempt`);
|
|
139
|
+
}
|
|
140
|
+
const event = events.find((item) => item.eventId === row.recordedEventId);
|
|
141
|
+
if (!event || (event.eventType !== "effect.observed" && event.eventType !== "effect.blocked_uncertain")) {
|
|
142
|
+
throw runtimeError("gate_evidence_orphan", run.runId, `observation ${row.observationId} is not named by an observation event`);
|
|
143
|
+
}
|
|
144
|
+
if (stringField(event, "attemptId") !== row.attemptId) {
|
|
145
|
+
throw runtimeError("gate_evidence_orphan", run.runId, `observation ${row.observationId} is not named by this attempt's observation event`);
|
|
146
|
+
}
|
|
147
|
+
const attempt = attemptById.get(row.attemptId)!;
|
|
148
|
+
assertShared(run, row, attempt);
|
|
149
|
+
assertClosedGateObservation(attempt.gateKind, row, row.observationId);
|
|
150
|
+
}
|
|
151
|
+
for (const row of evidence) {
|
|
152
|
+
if (!attemptById.has(row.attemptId)) {
|
|
153
|
+
throw runtimeError("gate_evidence_orphan", run.runId, `evidence ${row.evidenceId} is owned by this run but names a foreign attempt`);
|
|
154
|
+
}
|
|
155
|
+
const event = events.find((item) => item.eventId === row.settledEventId);
|
|
156
|
+
if (!event || event.eventType !== "effect.settled") {
|
|
157
|
+
throw runtimeError("gate_evidence_orphan", run.runId, `evidence ${row.evidenceId} is not named by effect.settled`);
|
|
158
|
+
}
|
|
159
|
+
if (stringField(event, "attemptId") !== row.attemptId) {
|
|
160
|
+
throw runtimeError("gate_evidence_orphan", run.runId, `evidence ${row.evidenceId} is not named by this attempt's settled event`);
|
|
161
|
+
}
|
|
162
|
+
const refs = event.payload.evidenceRefs;
|
|
163
|
+
if (!Array.isArray(refs) || refs.length !== 1 || (refs[0] as { id?: unknown }).id !== row.evidenceId) {
|
|
164
|
+
throw runtimeError("gate_evidence_orphan", run.runId, "evidence row is not the payload evidenceRef");
|
|
165
|
+
}
|
|
166
|
+
const attempt = attemptById.get(row.attemptId)!;
|
|
167
|
+
assertShared(run, row, attempt);
|
|
168
|
+
if (row.expect !== attempt.expect) {
|
|
169
|
+
throw runtimeError("gate_attempt_pin_mismatch", run.runId, "evidence expect does not match the gate attempt");
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (attempts.length !== intentEvents.length) {
|
|
174
|
+
throw runtimeError("gate_evidence_orphan", run.runId, "gate attempt count does not match intent events");
|
|
175
|
+
}
|
|
176
|
+
if (observations.length !== observedEvents.length) {
|
|
177
|
+
throw runtimeError("gate_observation_missing", run.runId, "observation count does not match observation events");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const lastEffect = lastEffectByAttempt(events);
|
|
181
|
+
for (const [attemptId, last] of lastEffect) {
|
|
182
|
+
const evidenceRow = evidenceByAttempt.get(attemptId);
|
|
183
|
+
if (last === "effect.settled") {
|
|
184
|
+
const settled = settledEvents.filter((event) => stringField(event, "attemptId") === attemptId).at(-1);
|
|
185
|
+
const evaluated = Array.isArray(settled?.payload.evidenceRefs) && (settled!.payload.evidenceRefs as unknown[]).length > 0;
|
|
186
|
+
if (evaluated && !evidenceRow) throw runtimeError("gate_evidence_missing", run.runId, "evaluated settlement is missing evidence");
|
|
187
|
+
if (!evaluated && evidenceRow) throw runtimeError("gate_evidence_unexpected", run.runId, "proof-only settlement has evidence");
|
|
188
|
+
const capability = store.capabilityByAttempt(attemptId);
|
|
189
|
+
if (evaluated && capability?.state === "issued") {
|
|
190
|
+
throw runtimeError("gate_evidence_unexpected", run.runId, "settled attempt still has issued capability");
|
|
191
|
+
}
|
|
192
|
+
} else if (evidenceRow) {
|
|
193
|
+
throw runtimeError("gate_evidence_unexpected", run.runId, "non-settled attempt has evidence");
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
void vnextCanonicalJson;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function lastEffectByAttempt(events: readonly VnextRunEvent[]): Map<string, string> {
|
|
200
|
+
const last = new Map<string, string>();
|
|
201
|
+
for (const event of events) {
|
|
202
|
+
if (!event.eventType.startsWith("effect.")) continue;
|
|
203
|
+
const attemptId = event.payload.attemptId;
|
|
204
|
+
if (typeof attemptId === "string") last.set(attemptId, event.eventType);
|
|
205
|
+
}
|
|
206
|
+
return last;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function assertAttemptMatchesEvent(
|
|
210
|
+
run: VnextRunRecord,
|
|
211
|
+
envelope: VnextRunPlanEnvelope,
|
|
212
|
+
state: VnextRunState,
|
|
213
|
+
event: VnextRunEvent,
|
|
214
|
+
row: VnextGateAttemptRow,
|
|
215
|
+
): void {
|
|
216
|
+
const effectId = (event.payload.effect as { id?: unknown } | undefined)?.id;
|
|
217
|
+
if (row.effectId !== effectId || row.intentEventId !== event.eventId) {
|
|
218
|
+
throw runtimeError("gate_attempt_pin_mismatch", run.runId, "intent event does not match the gate attempt");
|
|
219
|
+
}
|
|
220
|
+
if (row.runId !== run.runId || row.projectId !== run.projectId || row.homeRuntimeId !== run.homeRuntimeId) {
|
|
221
|
+
throw runtimeError("gate_attempt_pin_mismatch", run.runId, "gate attempt owner does not match the run");
|
|
222
|
+
}
|
|
223
|
+
if (row.stepId !== event.payload.stepId || row.stepAttempt !== event.payload.stepAttempt || row.assignmentId !== event.payload.assignmentId || row.attemptId !== event.payload.attemptId) {
|
|
224
|
+
throw runtimeError("gate_attempt_pin_mismatch", run.runId, "gate attempt identity does not match the intent payload");
|
|
225
|
+
}
|
|
226
|
+
const step = envelope.plan.steps[row.stepId];
|
|
227
|
+
if (!step || step.kind !== "gate") {
|
|
228
|
+
throw runtimeError("gate_attempt_pin_mismatch", run.runId, "gate attempt step is not a gate");
|
|
229
|
+
}
|
|
230
|
+
if (row.gateId !== step.gate || row.expect !== step.expect) {
|
|
231
|
+
throw runtimeError("gate_attempt_pin_mismatch", run.runId, "gate attempt gate/expect do not match the plan");
|
|
232
|
+
}
|
|
233
|
+
const definition = envelope.gates.definitions[step.gate];
|
|
234
|
+
if (!definition || definition.kind !== row.gateKind) {
|
|
235
|
+
throw runtimeError("gate_attempt_pin_mismatch", run.runId, "gate attempt kind does not match the pinned definition");
|
|
236
|
+
}
|
|
237
|
+
const expectedDefinitionHash = gateDefinitionHash(step.gate, definition as unknown as JsonValue);
|
|
238
|
+
if (row.gateDefinitionHash !== expectedDefinitionHash) {
|
|
239
|
+
throw runtimeError("gate_attempt_pin_mismatch", run.runId, "gate_definition_hash does not match the pinned definition");
|
|
240
|
+
}
|
|
241
|
+
if (!envelope.gates.registry || row.registryHash !== envelope.gates.registry.hash) {
|
|
242
|
+
throw runtimeError("gate_attempt_pin_mismatch", run.runId, "registry_hash does not match the pinned registry");
|
|
243
|
+
}
|
|
244
|
+
const planRowHash = envelope && state.runPlanHash;
|
|
245
|
+
if (row.runPlanHash !== planRowHash) {
|
|
246
|
+
throw runtimeError("gate_attempt_pin_mismatch", run.runId, "run_plan_hash does not match the pinned plan");
|
|
247
|
+
}
|
|
248
|
+
if (row.controlProjectKey !== envelope.gates.controlRoot.projectKey) {
|
|
249
|
+
throw runtimeError("gate_attempt_pin_mismatch", run.runId, "control_project_key does not match the envelope");
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function assertShared(run: VnextRunRecord, row: VnextGateObservationRow | VnextGateEvidenceRow, attempt: VnextGateAttemptRow): void {
|
|
254
|
+
if (
|
|
255
|
+
row.runId !== attempt.runId
|
|
256
|
+
|| row.projectId !== attempt.projectId
|
|
257
|
+
|| row.homeRuntimeId !== attempt.homeRuntimeId
|
|
258
|
+
|| row.stepId !== attempt.stepId
|
|
259
|
+
|| row.stepAttempt !== attempt.stepAttempt
|
|
260
|
+
|| row.assignmentId !== attempt.assignmentId
|
|
261
|
+
|| row.effectId !== attempt.effectId
|
|
262
|
+
) {
|
|
263
|
+
throw runtimeError("gate_attempt_pin_mismatch", run.runId, "row identity does not match the gate attempt");
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function stringField(event: VnextRunEvent, field: string): string {
|
|
268
|
+
const value = event.payload[field];
|
|
269
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
270
|
+
throw runtimeError("gate_attempt_pin_mismatch", event.runId, `${event.eventType} is missing ${field}`);
|
|
271
|
+
}
|
|
272
|
+
return value;
|
|
273
|
+
}
|