cool-workflow 0.1.97 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (309) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/README.md +11 -2
  4. package/apps/architecture-review/app.json +1 -1
  5. package/apps/architecture-review-fast/app.json +1 -1
  6. package/apps/end-to-end-golden-path/app.json +1 -1
  7. package/apps/pr-review-fix-ci/app.json +1 -1
  8. package/apps/release-cut/app.json +1 -1
  9. package/apps/research-synthesis/app.json +1 -1
  10. package/dist/cli/dispatch.js +236 -0
  11. package/dist/cli/entry.js +120 -0
  12. package/dist/cli/io.js +21 -4
  13. package/dist/cli/parseargv.js +157 -0
  14. package/dist/cli.js +6 -33
  15. package/dist/core/capability-table.js +3534 -0
  16. package/dist/core/format/help.js +314 -0
  17. package/dist/{state-explosion/format.js → core/format/state-explosion-text.js} +10 -1
  18. package/dist/core/hash.js +137 -0
  19. package/dist/core/multi-agent/candidate-scoring.js +219 -0
  20. package/dist/core/multi-agent/collaboration.js +481 -0
  21. package/dist/core/multi-agent/coordinator.js +515 -0
  22. package/dist/core/multi-agent/eval-replay.js +306 -0
  23. package/dist/core/multi-agent/runtime.js +929 -0
  24. package/dist/core/multi-agent/topology.js +298 -0
  25. package/dist/core/multi-agent/trust-policy.js +197 -0
  26. package/dist/core/pipeline/commit-gate.js +320 -0
  27. package/dist/{pipeline-contract.js → core/pipeline/contract.js} +24 -37
  28. package/dist/core/pipeline/dispatch.js +103 -0
  29. package/dist/core/pipeline/drive-decide.js +227 -0
  30. package/dist/core/pipeline/error-feedback.js +161 -0
  31. package/dist/core/pipeline/loop-expansion.js +124 -0
  32. package/dist/{result-normalize.js → core/pipeline/result-normalize.js} +14 -37
  33. package/dist/core/pipeline/runner.js +230 -0
  34. package/dist/{contract-migration.js → core/state/contract-migration.js} +68 -92
  35. package/dist/{state-migrations.js → core/state/migrations.js} +103 -96
  36. package/dist/core/state/node-projection.js +95 -0
  37. package/dist/core/state/node-snapshot.js +230 -0
  38. package/dist/core/state/run-paths.js +93 -0
  39. package/dist/{schema-validate.js → core/state/schema-validate.js} +35 -28
  40. package/dist/core/state/schema.js +50 -0
  41. package/dist/core/state/state-explosion/digest.js +243 -0
  42. package/dist/core/state/state-explosion/graph.js +527 -0
  43. package/dist/{state-explosion → core/state/state-explosion}/helpers.js +34 -3
  44. package/dist/core/state/state-explosion/report.js +187 -0
  45. package/dist/{state-explosion → core/state/state-explosion}/size.js +25 -7
  46. package/dist/{state-node.js → core/state/state-node.js} +90 -113
  47. package/dist/core/state/types.js +19 -0
  48. package/dist/{validation.js → core/state/validation.js} +64 -131
  49. package/dist/core/trust/evidence-grounding.js +134 -0
  50. package/dist/core/trust/ledger.js +199 -0
  51. package/dist/{telemetry-attestation.js → core/trust/telemetry-attestation.js} +94 -68
  52. package/dist/core/trust/telemetry-ledger.js +127 -0
  53. package/dist/core/types.js +20 -0
  54. package/dist/core/version.js +16 -0
  55. package/dist/{workflow-app-framework.js → core/workflow-apps/app-schema.js} +337 -426
  56. package/dist/mcp/dispatch.js +104 -0
  57. package/dist/mcp/server.js +144 -0
  58. package/dist/mcp-server.js +6 -84
  59. package/dist/{agent-config.js → shell/agent-config.js} +118 -71
  60. package/dist/shell/app-run-cli.js +88 -0
  61. package/dist/shell/audit-cli.js +259 -0
  62. package/dist/shell/audit-provenance.js +83 -0
  63. package/dist/shell/candidate-scoring-io.js +459 -0
  64. package/dist/shell/collaboration-io.js +264 -0
  65. package/dist/shell/commit-summary.js +104 -0
  66. package/dist/shell/commit.js +290 -0
  67. package/dist/shell/coordinator-io.js +476 -0
  68. package/dist/shell/demo-cli.js +19 -0
  69. package/dist/shell/dispatch.js +162 -0
  70. package/dist/{doctor.js → shell/doctor.js} +123 -56
  71. package/dist/shell/drive.js +873 -0
  72. package/dist/shell/error-feedback-io.js +305 -0
  73. package/dist/shell/eval-io.js +473 -0
  74. package/dist/{multi-agent-eval/format.js → shell/eval-text.js} +78 -49
  75. package/dist/{evidence-reasoning.js → shell/evidence-reasoning.js} +124 -255
  76. package/dist/shell/exec-backend-cli.js +88 -0
  77. package/dist/shell/execution-backend/agent.js +475 -0
  78. package/dist/shell/execution-backend/ci.js +15 -0
  79. package/dist/shell/execution-backend/container.js +69 -0
  80. package/dist/shell/execution-backend/envelopes.js +55 -0
  81. package/dist/shell/execution-backend/local.js +113 -0
  82. package/dist/shell/execution-backend/probes.js +175 -0
  83. package/dist/shell/execution-backend/registry.js +402 -0
  84. package/dist/shell/execution-backend/remote.js +128 -0
  85. package/dist/shell/execution-backend/types.js +11 -0
  86. package/dist/shell/feedback-cli.js +81 -0
  87. package/dist/shell/feedback-operations.js +48 -0
  88. package/dist/shell/fs-atomic.js +276 -0
  89. package/dist/shell/harness.js +98 -0
  90. package/dist/shell/ledger-cli.js +212 -0
  91. package/dist/shell/ledger-io.js +169 -0
  92. package/dist/shell/man-cli.js +89 -0
  93. package/dist/shell/metrics-cli.js +98 -0
  94. package/dist/shell/multi-agent-cli.js +1002 -0
  95. package/dist/shell/multi-agent-host.js +563 -0
  96. package/dist/shell/multi-agent-io.js +387 -0
  97. package/dist/{multi-agent-operator-ux.js → shell/multi-agent-operator-ux.js} +234 -191
  98. package/dist/shell/node-store.js +124 -0
  99. package/dist/{observability/format.js → shell/observability-format.js} +7 -1
  100. package/dist/{observability/intake.js → shell/observability-intake.js} +79 -56
  101. package/dist/{observability.js → shell/observability.js} +159 -332
  102. package/dist/{onramp.js → shell/onramp.js} +11 -0
  103. package/dist/{operator-ux/format.js → shell/operator-ux-text.js} +250 -239
  104. package/dist/shell/operator-ux.js +431 -0
  105. package/dist/shell/orchestrator.js +231 -0
  106. package/dist/shell/pipeline-cli.js +667 -0
  107. package/dist/shell/pipeline.js +217 -0
  108. package/dist/shell/reclamation-io.js +1366 -0
  109. package/dist/shell/registry-cli.js +329 -0
  110. package/dist/{remote-source.js → shell/remote-source.js} +12 -5
  111. package/dist/shell/report-cli.js +101 -0
  112. package/dist/shell/report-view-cli.js +117 -0
  113. package/dist/{orchestrator → shell}/report.js +289 -282
  114. package/dist/shell/reporter.js +62 -0
  115. package/dist/shell/run-export-cli.js +106 -0
  116. package/dist/shell/run-export.js +680 -0
  117. package/dist/shell/run-registry-io.js +1014 -0
  118. package/dist/shell/run-store.js +164 -0
  119. package/dist/{sandbox-profile.js → shell/sandbox-profile.js} +134 -95
  120. package/dist/{scheduler.js → shell/scheduler-io.js} +248 -48
  121. package/dist/shell/scheduling-io.js +311 -0
  122. package/dist/shell/state-cli.js +181 -0
  123. package/dist/shell/state-explosion-cli.js +197 -0
  124. package/dist/shell/telemetry-cli.js +85 -0
  125. package/dist/{telemetry-demo.js → shell/telemetry-demo.js} +149 -119
  126. package/dist/shell/telemetry-ledger-io.js +132 -0
  127. package/dist/{term.js → shell/term.js} +36 -46
  128. package/dist/shell/topology-io.js +361 -0
  129. package/dist/shell/trust-audit.js +471 -0
  130. package/dist/{multi-agent-trust.js → shell/trust-policy-io.js} +67 -205
  131. package/dist/shell/verifier.js +48 -0
  132. package/dist/shell/workbench-host.js +250 -0
  133. package/dist/shell/workbench-text.js +18 -0
  134. package/dist/shell/workbench.js +175 -0
  135. package/dist/shell/worker-cli.js +124 -0
  136. package/dist/shell/worker-isolation.js +852 -0
  137. package/dist/shell/workflow-app-loader.js +650 -0
  138. package/docs/agent-delegation-drive.7.md +4 -0
  139. package/docs/cli-mcp-parity.7.md +284 -211
  140. package/docs/contract-migration-tooling.7.md +4 -0
  141. package/docs/control-plane-scheduling.7.md +4 -0
  142. package/docs/cross-agent-ledger.7.md +217 -0
  143. package/docs/designs/handoff-ledger.md +145 -0
  144. package/docs/durable-state-and-locking.7.md +4 -0
  145. package/docs/evidence-adoption-reasoning-chain.7.md +4 -0
  146. package/docs/execution-backends.7.md +4 -0
  147. package/docs/handoff-setup.md +120 -0
  148. package/docs/multi-agent-cli-mcp-surface.7.md +4 -0
  149. package/docs/multi-agent-eval-replay-harness.7.md +4 -0
  150. package/docs/multi-agent-operator-ux.7.md +4 -0
  151. package/docs/node-snapshot-diff-replay.7.md +4 -0
  152. package/docs/observability-cost-accounting.7.md +4 -0
  153. package/docs/project-index.md +143 -71
  154. package/docs/real-execution-backends.7.md +4 -0
  155. package/docs/release-and-migration.7.md +4 -0
  156. package/docs/release-tooling.7.md +4 -0
  157. package/docs/run-registry-control-plane.7.md +4 -0
  158. package/docs/run-retention-reclamation.7.md +25 -0
  159. package/docs/state-explosion-management.7.md +4 -0
  160. package/docs/team-collaboration.7.md +4 -0
  161. package/docs/web-desktop-workbench.7.md +4 -0
  162. package/manifest/plugin.manifest.json +1 -1
  163. package/manifest/source-context-profiles.json +9 -13
  164. package/package.json +1 -1
  165. package/scripts/agents/codex-agent.js +34 -4
  166. package/scripts/agents/cw-attest-wrap.js +2 -2
  167. package/scripts/bump-version.js +4 -3
  168. package/scripts/canonical-apps.js +4 -4
  169. package/scripts/children/batch-delegate-child.js +30 -10
  170. package/scripts/dogfood-architecture-review.js +2 -3
  171. package/scripts/dogfood-release.js +3 -3
  172. package/scripts/gen-parity-doc.js +15 -2
  173. package/scripts/golden-path.js +4 -4
  174. package/scripts/onramp-check.js +1 -1
  175. package/scripts/parity-check.js +38 -21
  176. package/scripts/release-flow.js +9 -3
  177. package/scripts/sync-project-index.js +51 -27
  178. package/scripts/validate-run-state-schema.js +2 -2
  179. package/scripts/version-sync-check.js +30 -30
  180. package/dist/candidate-scoring.js +0 -729
  181. package/dist/capability-core.js +0 -1186
  182. package/dist/capability-registry.js +0 -879
  183. package/dist/cli/command-surface.js +0 -490
  184. package/dist/cli/format.js +0 -56
  185. package/dist/cli/handlers/audit.js +0 -82
  186. package/dist/cli/handlers/blackboard.js +0 -81
  187. package/dist/cli/handlers/candidate.js +0 -40
  188. package/dist/cli/handlers/clones.js +0 -34
  189. package/dist/cli/handlers/collaboration.js +0 -61
  190. package/dist/cli/handlers/eval.js +0 -40
  191. package/dist/cli/handlers/maintenance.js +0 -107
  192. package/dist/cli/handlers/multi-agent.js +0 -165
  193. package/dist/cli/handlers/node.js +0 -41
  194. package/dist/cli/handlers/operational.js +0 -155
  195. package/dist/cli/handlers/operator.js +0 -146
  196. package/dist/cli/handlers/registry.js +0 -68
  197. package/dist/cli/handlers/run.js +0 -153
  198. package/dist/cli/handlers/scheduling.js +0 -132
  199. package/dist/cli/handlers/workbench.js +0 -41
  200. package/dist/cli/handlers/worker.js +0 -45
  201. package/dist/cli/run-summary.js +0 -45
  202. package/dist/clones.js +0 -162
  203. package/dist/collaboration.js +0 -726
  204. package/dist/commit.js +0 -592
  205. package/dist/compare.js +0 -18
  206. package/dist/coordinator/classify.js +0 -45
  207. package/dist/coordinator/paths.js +0 -42
  208. package/dist/coordinator/util.js +0 -126
  209. package/dist/coordinator.js +0 -990
  210. package/dist/daemon.js +0 -44
  211. package/dist/dispatch.js +0 -250
  212. package/dist/drive.js +0 -827
  213. package/dist/error-feedback.js +0 -419
  214. package/dist/evidence-grounding.js +0 -184
  215. package/dist/execution-backend/agent.js +0 -294
  216. package/dist/execution-backend/probes.js +0 -112
  217. package/dist/execution-backend/util.js +0 -47
  218. package/dist/execution-backend.js +0 -961
  219. package/dist/gates.js +0 -48
  220. package/dist/harness.js +0 -61
  221. package/dist/loop-expansion.js +0 -60
  222. package/dist/mcp/tool-call.js +0 -434
  223. package/dist/mcp/tool-definitions.js +0 -1040
  224. package/dist/mcp-surface.js +0 -30
  225. package/dist/multi-agent/graph.js +0 -84
  226. package/dist/multi-agent/helpers.js +0 -141
  227. package/dist/multi-agent/ids.js +0 -20
  228. package/dist/multi-agent/paths.js +0 -22
  229. package/dist/multi-agent-eval/normalize.js +0 -51
  230. package/dist/multi-agent-eval.js +0 -678
  231. package/dist/multi-agent-host.js +0 -777
  232. package/dist/multi-agent.js +0 -984
  233. package/dist/node-projection.js +0 -59
  234. package/dist/node-snapshot.js +0 -260
  235. package/dist/operator-ux.js +0 -631
  236. package/dist/orchestrator/app-operations.js +0 -211
  237. package/dist/orchestrator/audit-operations.js +0 -182
  238. package/dist/orchestrator/candidate-operations.js +0 -117
  239. package/dist/orchestrator/cli-options.js +0 -294
  240. package/dist/orchestrator/collaboration-operations.js +0 -86
  241. package/dist/orchestrator/feedback-operations.js +0 -81
  242. package/dist/orchestrator/host-operations.js +0 -78
  243. package/dist/orchestrator/lifecycle-operations.js +0 -626
  244. package/dist/orchestrator/migration-operations.js +0 -44
  245. package/dist/orchestrator/multi-agent-operations.js +0 -362
  246. package/dist/orchestrator/topology-operations.js +0 -84
  247. package/dist/orchestrator.js +0 -917
  248. package/dist/pipeline-runner.js +0 -285
  249. package/dist/reclamation/hash.js +0 -72
  250. package/dist/reclamation.js +0 -812
  251. package/dist/reporter.js +0 -67
  252. package/dist/run-export.js +0 -784
  253. package/dist/run-registry/derive.js +0 -175
  254. package/dist/run-registry/format.js +0 -124
  255. package/dist/run-registry/gc.js +0 -251
  256. package/dist/run-registry/policy.js +0 -16
  257. package/dist/run-registry/queue.js +0 -115
  258. package/dist/run-registry.js +0 -850
  259. package/dist/run-state-schema.js +0 -68
  260. package/dist/scheduling.js +0 -184
  261. package/dist/state-explosion.js +0 -1014
  262. package/dist/state.js +0 -367
  263. package/dist/telemetry-ledger.js +0 -196
  264. package/dist/topology.js +0 -565
  265. package/dist/triggers.js +0 -184
  266. package/dist/trust-audit.js +0 -644
  267. package/dist/types/blackboard.js +0 -2
  268. package/dist/types/candidate.js +0 -2
  269. package/dist/types/collaboration.js +0 -2
  270. package/dist/types/core.js +0 -2
  271. package/dist/types/drive.js +0 -10
  272. package/dist/types/error-feedback.js +0 -2
  273. package/dist/types/evidence-reasoning.js +0 -2
  274. package/dist/types/execution-backend.js +0 -2
  275. package/dist/types/multi-agent.js +0 -2
  276. package/dist/types/observability.js +0 -2
  277. package/dist/types/pipeline.js +0 -2
  278. package/dist/types/reclamation.js +0 -8
  279. package/dist/types/report-bundle.js +0 -6
  280. package/dist/types/result.js +0 -2
  281. package/dist/types/run-registry.js +0 -2
  282. package/dist/types/run.js +0 -2
  283. package/dist/types/sandbox.js +0 -2
  284. package/dist/types/schedule.js +0 -2
  285. package/dist/types/state-node.js +0 -2
  286. package/dist/types/topology.js +0 -2
  287. package/dist/types/trust.js +0 -2
  288. package/dist/types/workbench.js +0 -2
  289. package/dist/types/worker.js +0 -2
  290. package/dist/types/workflow-app.js +0 -2
  291. package/dist/types.js +0 -44
  292. package/dist/util/fingerprint.js +0 -19
  293. package/dist/util/fingerprint.test.js +0 -27
  294. package/dist/verifier.js +0 -78
  295. package/dist/version.js +0 -8
  296. package/dist/workbench-host.js +0 -182
  297. package/dist/workbench.js +0 -192
  298. package/dist/worker-accept/acceptance.js +0 -114
  299. package/dist/worker-accept/blackboard-fanout.js +0 -80
  300. package/dist/worker-accept/blackboard-linkage.js +0 -19
  301. package/dist/worker-accept/context.js +0 -2
  302. package/dist/worker-accept/telemetry-ledger.js +0 -126
  303. package/dist/worker-accept/validation.js +0 -77
  304. package/dist/worker-accept/verifier-completion.js +0 -73
  305. package/dist/worker-isolation/helpers.js +0 -51
  306. package/dist/worker-isolation/paths.js +0 -46
  307. package/dist/worker-isolation.js +0 -656
  308. package/dist/workflow-api.js +0 -131
  309. /package/dist/{types → core/types}/boundary.js +0 -0
@@ -1,47 +1,58 @@
1
1
  "use strict";
2
- // Fail-closed shape guards for persisted per-record types (F5, integrity boundary).
2
+ // core/state/validation.ts RecordValidationError + per-record shape
3
+ // guards.
3
4
  //
4
- // `JSON.parse(...) as T` is a LIE: the cast asserts a shape TypeScript never
5
- // checked at runtime, so a corrupt/forged/old-schema record flows in as if it
6
- // were a valid T and is used/upserted unvalidated. These guards re-establish the
7
- // integrity boundary at the read edge: after parse, the raw value is structurally
8
- // validated against the type def in src/types/* BEFORE it is trusted.
5
+ // MILESTONE 3 (+ WorkerScope port). Byte-exact port of the old build's
6
+ // src/validation.ts. NodeSnapshot / NodeReplayRun are read at the
7
+ // `readNodeSnapshot`/`readNodeReplay` edge in shell/node-store.ts.
8
+ // WorkerScope is read at the `getWorkerScope`/`loadWorkerScopesFromDisk`
9
+ // edge in shell/worker-isolation.ts. CandidateScore/CandidateRecord
10
+ // guards still land with their owning milestone (9) rather than being
11
+ // spec'd ahead of need here.
9
12
  //
10
- // Two callers, two error semantics both fail closed, neither fabricates:
11
- // - validate*() throw a descriptive Error on mismatch (for readers that
12
- // already let parse errors propagate / require the record).
13
- // - tryValidate*() return null on mismatch (for best-effort readers that
14
- // swallow parse errors and SKIP the record the downstream
15
- // gate then fails closed on the absence). Never throws.
13
+ // WorkerScope itself is a shell-layer concept (its full type carries
14
+ // ResolvedSandboxPolicy/SandboxAttestation from shell/execution-backend),
15
+ // so this pure core module does not import that type. Instead it
16
+ // declares WorkerScopeShape: the subset of required fields the old guard
17
+ // actually checks. Any real WorkerScope satisfies this shape structurally
18
+ // (TypeScript structural typing), so shell/worker-isolation.ts can call
19
+ // this guard and get its own WorkerScope back without a core -> shell
20
+ // import.
16
21
  //
17
- // Dependency-light by construction: imports ONLY from ./types. No fs, no clock,
18
- // no randomness pure structural checks, safe in replay/core paths.
22
+ // Two callers, two error semantics, both fail closed:
23
+ // - validate*() throws a descriptive Error on mismatch.
24
+ // - tryValidate*() returns null on mismatch (never throws).
25
+ //
26
+ // Dependency-light by construction: imports only from ./types. No fs, no
27
+ // clock, no randomness — pure structural checks, safe in replay/core paths.
28
+ //
29
+ // Evidence: SPEC/state-core.md "src/validation.ts — persisted-record shape
30
+ // guards", "Fail-closed record reads".
19
31
  Object.defineProperty(exports, "__esModule", { value: true });
20
32
  exports.RecordValidationError = void 0;
21
33
  exports.validateWorkerScope = validateWorkerScope;
22
34
  exports.validateNodeSnapshot = validateNodeSnapshot;
23
35
  exports.validateNodeReplayRun = validateNodeReplayRun;
24
- exports.validateCandidateScore = validateCandidateScore;
25
36
  exports.tryValidateCandidateScore = tryValidateCandidateScore;
26
- exports.validateCandidateRecord = validateCandidateRecord;
27
- // ---------------------------------------------------------------------------
28
- // Primitive predicates — small, total, never throw.
29
- // ---------------------------------------------------------------------------
30
37
  function isRecord(value) {
31
38
  return value !== null && typeof value === "object" && !Array.isArray(value);
32
39
  }
33
40
  function isString(value) {
34
41
  return typeof value === "string";
35
42
  }
36
- function isFiniteNumber(value) {
37
- return typeof value === "number" && Number.isFinite(value);
43
+ function isObjectArray(value) {
44
+ return Array.isArray(value) && value.every((entry) => isRecord(entry));
38
45
  }
39
46
  function isStringArray(value) {
40
47
  return Array.isArray(value) && value.every((entry) => typeof entry === "string");
41
48
  }
42
- function isObjectArray(value) {
43
- return Array.isArray(value) && value.every((entry) => isRecord(entry));
49
+ function isFiniteNumber(value) {
50
+ return typeof value === "number" && Number.isFinite(value);
51
+ }
52
+ function isNumberRecord(value) {
53
+ return isRecord(value) && Object.values(value).every((entry) => isFiniteNumber(entry));
44
54
  }
55
+ const SNAPSHOT_FRESHNESS = new Set(["valid", "stale", "absent"]);
45
56
  const WORKER_STATUSES = new Set([
46
57
  "allocated",
47
58
  "running",
@@ -49,21 +60,10 @@ const WORKER_STATUSES = new Set([
49
60
  "failed",
50
61
  "rejected",
51
62
  "verified",
52
- "orphaned"
53
- ]);
54
- const SCORE_VERDICTS = new Set(["pass", "warn", "fail"]);
55
- const CANDIDATE_STATUSES = new Set([
56
- "registered",
57
- "scored",
58
- "selected",
59
- "rejected",
60
- "verified",
61
- "failed"
63
+ "orphaned",
62
64
  ]);
63
- const CANDIDATE_KINDS = new Set(["worker-output", "result", "artifact", "manual", "release"]);
64
- const SNAPSHOT_FRESHNESS = new Set(["valid", "stale", "absent"]);
65
- /** Descriptive integrity error — the message names the type and the field that
66
- * broke, so a corrupt record is diagnosable from logs alone. */
65
+ /** Descriptive integrity error the message names the type and the field
66
+ * that broke, so a corrupt record is diagnosable from logs alone. */
67
67
  class RecordValidationError extends Error {
68
68
  code = "record-shape-invalid";
69
69
  typeName;
@@ -77,11 +77,12 @@ class RecordValidationError extends Error {
77
77
  }
78
78
  exports.RecordValidationError = RecordValidationError;
79
79
  // ---------------------------------------------------------------------------
80
- // WorkerScope — worker-isolation.ts:309 / :905
81
- // Required (per src/types/worker.ts WorkerScope): schemaVersion===1, id, runId,
82
- // taskId, createdAt, updatedAt, status (enum), workerDir, inputPath, resultPath,
83
- // artifactsDir, logsDir are strings; allowedPaths string[]; feedbackIds string[];
84
- // errors object[]. Optional fields are not enforced (additive, may be absent).
80
+ // WorkerScope — worker-isolation.ts getWorkerScope / loadWorkerScopesFromDisk
81
+ // Required: schemaVersion===1, id, runId, taskId, createdAt, updatedAt,
82
+ // workerDir, inputPath, resultPath, artifactsDir, logsDir are strings;
83
+ // status a valid WorkerIsolationStatus; allowedPaths string[]; feedbackIds
84
+ // string[]; errors object[]. Optional fields are not enforced (additive,
85
+ // may be absent).
85
86
  // ---------------------------------------------------------------------------
86
87
  function workerScopeReason(value) {
87
88
  if (!isRecord(value))
@@ -98,7 +99,7 @@ function workerScopeReason(value) {
98
99
  "inputPath",
99
100
  "resultPath",
100
101
  "artifactsDir",
101
- "logsDir"
102
+ "logsDir",
102
103
  ];
103
104
  for (const field of requiredStrings) {
104
105
  if (!isString(value[field]))
@@ -115,19 +116,15 @@ function workerScopeReason(value) {
115
116
  return { field: "errors", reason: "must be a StateNodeError[]" };
116
117
  return undefined;
117
118
  }
118
- /** Throw-on-mismatch guard for WorkerScope (callers that require the record). */
119
+ /** Throw-on-mismatch guard for WorkerScope (callers that require the
120
+ * record). Returns WorkerScopeShape — the caller (shell/worker-isolation.ts)
121
+ * casts to its own richer WorkerScope, which is a structural superset. */
119
122
  function validateWorkerScope(value) {
120
123
  const problem = workerScopeReason(value);
121
124
  if (problem)
122
125
  throw new RecordValidationError("WorkerScope", problem.reason, problem.field);
123
126
  return value;
124
127
  }
125
- // ---------------------------------------------------------------------------
126
- // NodeSnapshotBody — shared by NodeSnapshot.body and NodeReplayRun.body.
127
- // Required (per src/types/state-node.ts): id, kind, status, loopStage strings;
128
- // inputs/outputs records; artifacts/evidence/errors object arrays;
129
- // parents/children string arrays.
130
- // ---------------------------------------------------------------------------
131
128
  function nodeSnapshotBodyReason(value, prefix) {
132
129
  if (!isRecord(value))
133
130
  return { field: prefix, reason: "must be a NodeSnapshotBody object" };
@@ -152,11 +149,6 @@ function nodeSnapshotBodyReason(value, prefix) {
152
149
  return { field: `${prefix}.children`, reason: "must be a string[]" };
153
150
  return undefined;
154
151
  }
155
- // ---------------------------------------------------------------------------
156
- // NodeSnapshot — node-snapshot.ts:121
157
- // Required: schemaVersion===1, snapshotId, runId, nodeId, capturedAt,
158
- // sourceFingerprint strings; body a valid NodeSnapshotBody.
159
- // ---------------------------------------------------------------------------
160
152
  function nodeSnapshotReason(value) {
161
153
  if (!isRecord(value))
162
154
  return { reason: "not an object" };
@@ -169,32 +161,20 @@ function nodeSnapshotReason(value) {
169
161
  }
170
162
  return nodeSnapshotBodyReason(value.body, "body");
171
163
  }
172
- /** Throw-on-mismatch guard for NodeSnapshot (read edge requires the record). */
164
+ /** Throw-on-mismatch guard for NodeSnapshot (read edge requires the
165
+ * record). */
173
166
  function validateNodeSnapshot(value) {
174
167
  const problem = nodeSnapshotReason(value);
175
168
  if (problem)
176
169
  throw new RecordValidationError("NodeSnapshot", problem.reason, problem.field);
177
170
  return value;
178
171
  }
179
- // ---------------------------------------------------------------------------
180
- // NodeReplayRun — node-snapshot.ts:133
181
- // Required: schemaVersion===1, replayId, runId, nodeId, snapshotId, replayedAt,
182
- // outputFingerprint strings; freshness enum; contractValidated boolean; body a
183
- // valid NodeSnapshotBody.
184
- // ---------------------------------------------------------------------------
185
172
  function nodeReplayRunReason(value) {
186
173
  if (!isRecord(value))
187
174
  return { reason: "not an object" };
188
175
  if (value.schemaVersion !== 1)
189
176
  return { field: "schemaVersion", reason: "must equal 1" };
190
- const requiredStrings = [
191
- "replayId",
192
- "runId",
193
- "nodeId",
194
- "snapshotId",
195
- "replayedAt",
196
- "outputFingerprint"
197
- ];
177
+ const requiredStrings = ["replayId", "runId", "nodeId", "snapshotId", "replayedAt", "outputFingerprint"];
198
178
  for (const field of requiredStrings) {
199
179
  if (!isString(value[field]))
200
180
  return { field: field, reason: "must be a string" };
@@ -207,7 +187,8 @@ function nodeReplayRunReason(value) {
207
187
  }
208
188
  return nodeSnapshotBodyReason(value.body, "body");
209
189
  }
210
- /** Throw-on-mismatch guard for NodeReplayRun (read edge requires the record). */
190
+ /** Throw-on-mismatch guard for NodeReplayRun (read edge requires the
191
+ * record). */
211
192
  function validateNodeReplayRun(value) {
212
193
  const problem = nodeReplayRunReason(value);
213
194
  if (problem)
@@ -215,14 +196,15 @@ function validateNodeReplayRun(value) {
215
196
  return value;
216
197
  }
217
198
  // ---------------------------------------------------------------------------
218
- // CandidateScore — multi-agent-operator-ux.ts:502 / evidence-reasoning.ts:750
219
- // Required (per src/types/candidate.ts): schemaVersion===1, id, candidateId,
220
- // runId, createdAt, scorer strings; criteria record of numbers; total/maxTotal/
221
- // normalized finite numbers; verdict enum; evidence/artifacts object arrays.
199
+ // CandidateScore — the score records evidence-reasoning.ts reads off disk to
200
+ // build the score gate. A corrupt/forged score must NOT flow into the run as a
201
+ // trusted record: the gate that backs commit selection reads these fields, so
202
+ // we fail closed at the read edge. schemaVersion===1; id/candidateId/runId/
203
+ // createdAt/scorer strings; criteria a Record<string, number>; total/maxTotal/
204
+ // normalized finite numbers; verdict a pass|warn|fail enum; evidence/artifacts
205
+ // object arrays. Byte-behavior port of the old build's src/validation.ts guard.
222
206
  // ---------------------------------------------------------------------------
223
- function isNumberRecord(value) {
224
- return isRecord(value) && Object.values(value).every((entry) => isFiniteNumber(entry));
225
- }
207
+ const SCORE_VERDICTS = new Set(["pass", "warn", "fail"]);
226
208
  function candidateScoreReason(value) {
227
209
  if (!isRecord(value))
228
210
  return { reason: "not an object" };
@@ -242,7 +224,7 @@ function candidateScoreReason(value) {
242
224
  if (!isFiniteNumber(value.normalized))
243
225
  return { field: "normalized", reason: "must be a finite number" };
244
226
  if (!isString(value.verdict) || !SCORE_VERDICTS.has(value.verdict)) {
245
- return { field: "verdict", reason: "must be a valid CandidateScoreVerdict" };
227
+ return { field: "verdict", reason: "must be a valid CandidateScore verdict (pass|warn|fail)" };
246
228
  }
247
229
  if (!isObjectArray(value.evidence))
248
230
  return { field: "evidence", reason: "must be a StateEvidence[]" };
@@ -250,58 +232,9 @@ function candidateScoreReason(value) {
250
232
  return { field: "artifacts", reason: "must be a StateArtifact[]" };
251
233
  return undefined;
252
234
  }
253
- /** Throw-on-mismatch guard for CandidateScore (callers that require the record). */
254
- function validateCandidateScore(value) {
255
- const problem = candidateScoreReason(value);
256
- if (problem)
257
- throw new RecordValidationError("CandidateScore", problem.reason, problem.field);
258
- return value;
259
- }
260
- /** Best-effort variant: returns null on mismatch (caller skips the record so the
261
- * downstream score gate fails closed on its absence). */
235
+ /** Best-effort guard: returns null on a shape mismatch so the caller skips the
236
+ * record and the downstream score gate fails closed on its absence, rather
237
+ * than trusting a forged/corrupt score. */
262
238
  function tryValidateCandidateScore(value) {
263
239
  return candidateScoreReason(value) ? null : value;
264
240
  }
265
- // ---------------------------------------------------------------------------
266
- // CandidateRecord — candidate-scoring.ts getCandidate / loadCandidatesFromDisk.
267
- // Required (per src/types/candidate.ts): schemaVersion===1, id, runId, createdAt,
268
- // updatedAt strings; kind/status enums; artifacts/evidence object arrays;
269
- // scores/feedbackIds string arrays. Optional ids (workerId/taskId/…) not enforced.
270
- // A corrupt/forged candidate record must NOT flow into the run as a trusted T —
271
- // the gate that backs commit selection reads these fields, so we fail closed at
272
- // the read edge instead of upserting an unvalidated cast.
273
- // ---------------------------------------------------------------------------
274
- function candidateRecordReason(value) {
275
- if (!isRecord(value))
276
- return { reason: "not an object" };
277
- if (value.schemaVersion !== 1)
278
- return { field: "schemaVersion", reason: "must equal 1" };
279
- const requiredStrings = ["id", "runId", "createdAt", "updatedAt"];
280
- for (const field of requiredStrings) {
281
- if (!isString(value[field]))
282
- return { field: field, reason: "must be a string" };
283
- }
284
- if (!isString(value.kind) || !CANDIDATE_KINDS.has(value.kind)) {
285
- return { field: "kind", reason: "must be a valid CandidateKind" };
286
- }
287
- if (!isString(value.status) || !CANDIDATE_STATUSES.has(value.status)) {
288
- return { field: "status", reason: "must be a valid CandidateStatus" };
289
- }
290
- if (!isObjectArray(value.artifacts))
291
- return { field: "artifacts", reason: "must be a StateArtifact[]" };
292
- if (!isObjectArray(value.evidence))
293
- return { field: "evidence", reason: "must be a StateEvidence[]" };
294
- if (!isStringArray(value.scores))
295
- return { field: "scores", reason: "must be a string[]" };
296
- if (!isStringArray(value.feedbackIds))
297
- return { field: "feedbackIds", reason: "must be a string[]" };
298
- return undefined;
299
- }
300
- /** Throw-on-mismatch guard for CandidateRecord (callers that require the record
301
- * at the disk read edge — getCandidate / loadCandidatesFromDisk). */
302
- function validateCandidateRecord(value) {
303
- const problem = candidateRecordReason(value);
304
- if (problem)
305
- throw new RecordValidationError("CandidateRecord", problem.reason, problem.field);
306
- return value;
307
- }
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+ // core/trust/evidence-grounding.ts — isGroundedEvidence, confidence tiers,
3
+ // extractEvidenceContent's PURE decision half.
4
+ //
5
+ // MILESTONE 6+7 (combined). Byte-exact port of the old build's
6
+ // src/evidence-grounding.ts, split so this file stays pure (no fs):
7
+ // resolution against real disk paths takes a caller-supplied
8
+ // `pathExists`/`readFile`, matching the core/shell split. Real callers
9
+ // (shell/) pass `fs.existsSync`/`fs.readFileSync`.
10
+ //
11
+ // Evidence: SPEC/pipeline-run.md "Result ingest — src/result-normalize.ts"
12
+ // (isGroundedEvidence is its evidence-classification primitive);
13
+ // plugins/cool-workflow/src/evidence-grounding.ts (byte-exact source).
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.isGroundedEvidence = isGroundedEvidence;
16
+ exports.hasGroundedEvidence = hasGroundedEvidence;
17
+ exports.requireResolvableEvidence = requireResolvableEvidence;
18
+ exports.resolveEvidenceLocator = resolveEvidenceLocator;
19
+ exports.unresolvedFileEvidence = unresolvedFileEvidence;
20
+ exports.computeEvidenceConfidence = computeEvidenceConfidence;
21
+ exports.extractEvidenceContent = extractEvidenceContent;
22
+ const URL_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
23
+ const PATH_SEP_RE = /[\\/]/;
24
+ const FILE_EXT_RE = /\.[A-Za-z0-9]{1,12}(?::\d+(?:-\d+)?)?$/;
25
+ const NAMESPACE_TOKEN_RE = /^[A-Za-z][A-Za-z0-9_.-]*:\S/;
26
+ const LINE_SUFFIX_RE = /:(\d+(?:-\d+)?)$/;
27
+ /** A single evidence string is "grounded" if it is machine-shaped — a
28
+ * URL, a path-like locator, or a `namespace:value` token — rather than
29
+ * free prose. */
30
+ function isGroundedEvidence(raw) {
31
+ const value = String(raw ?? "").trim();
32
+ if (!value)
33
+ return false;
34
+ if (URL_RE.test(value))
35
+ return true;
36
+ if (PATH_SEP_RE.test(value))
37
+ return true;
38
+ if (FILE_EXT_RE.test(value))
39
+ return true;
40
+ if (NAMESPACE_TOKEN_RE.test(value))
41
+ return true;
42
+ return false;
43
+ }
44
+ /** An evidence array passes the gate if at least one entry is grounded. */
45
+ function hasGroundedEvidence(evidence) {
46
+ return Array.isArray(evidence) && evidence.some((entry) => isGroundedEvidence(entry));
47
+ }
48
+ /** Whether opt-in strict resolution is requested via the environment.
49
+ * Enabled by DEFAULT: file-style evidence locators MUST exist on disk.
50
+ * `CW_REQUIRE_RESOLVABLE_EVIDENCE=0` restores the prior shape-only
51
+ * check. */
52
+ function requireResolvableEvidence(env = process.env) {
53
+ const raw = env.CW_REQUIRE_RESOLVABLE_EVIDENCE;
54
+ if (raw === undefined || raw === null || raw === "")
55
+ return true;
56
+ if (/^(0|false|no|off)$/i.test(raw))
57
+ return false;
58
+ return true;
59
+ }
60
+ function classify(raw) {
61
+ const value = raw.trim();
62
+ if (!value)
63
+ return { kind: "opaque" };
64
+ if (URL_RE.test(value))
65
+ return { kind: "url" };
66
+ const line = value.match(LINE_SUFFIX_RE);
67
+ const pathPart = line ? value.slice(0, value.length - line[0].length) : value;
68
+ const looksFile = (PATH_SEP_RE.test(pathPart) || FILE_EXT_RE.test(value)) && !/\s/.test(pathPart);
69
+ return looksFile ? { kind: "file", pathPart } : { kind: "opaque" };
70
+ }
71
+ /** Resolve one locator against base dirs (used only in strict mode).
72
+ * `resolve(candidatePath)` is caller-supplied (real fs.existsSync in
73
+ * shell/), matching the core/shell split — this pure module never
74
+ * imports fs itself. */
75
+ function resolveEvidenceLocator(raw, baseDirs, exists, isAbsolute, resolvePath) {
76
+ const shape = classify(raw);
77
+ if (shape.kind === "url")
78
+ return "external";
79
+ if (shape.kind === "opaque" || !shape.pathPart)
80
+ return "opaque";
81
+ const candidates = isAbsolute(shape.pathPart) ? [shape.pathPart] : baseDirs.filter(Boolean).map((base) => resolvePath(base, shape.pathPart));
82
+ for (const candidate of candidates) {
83
+ if (exists(candidate))
84
+ return "resolved";
85
+ }
86
+ return "unresolved";
87
+ }
88
+ /** In strict mode, the file-style locators that could NOT be resolved on
89
+ * disk. Returns [] when strict mode is off or all file locators
90
+ * resolve. */
91
+ function unresolvedFileEvidence(evidence, baseDirs, ops, env = process.env) {
92
+ if (!requireResolvableEvidence(env) || !Array.isArray(evidence))
93
+ return [];
94
+ return evidence.map((entry) => String(entry)).filter((entry) => resolveEvidenceLocator(entry, baseDirs, ops.exists, ops.isAbsolute, ops.resolve) === "unresolved");
95
+ }
96
+ /** Compute the confidence tier for a single evidence string. */
97
+ function computeEvidenceConfidence(raw, baseDirs, ops, env = process.env) {
98
+ if (!isGroundedEvidence(raw))
99
+ return "ungrounded";
100
+ if (!baseDirs || !baseDirs.length || !ops || !requireResolvableEvidence(env))
101
+ return "grounded";
102
+ const value = String(raw).trim();
103
+ const shape = classify(value);
104
+ if (shape.kind === "url")
105
+ return "grounded";
106
+ if (shape.kind === "opaque")
107
+ return "grounded";
108
+ const resolution = resolveEvidenceLocator(value, baseDirs, ops.exists, ops.isAbsolute, ops.resolve);
109
+ return resolution === "resolved" ? "resolvable" : "grounded";
110
+ }
111
+ /** Extract actual content from a file-style evidence locator. Never
112
+ * fabricates: returns undefined when the file doesn't exist or the
113
+ * locator is not file-style. Lines are 1-indexed. `readFile` is
114
+ * caller-supplied (real fs.readFileSync in shell/). */
115
+ function extractEvidenceContent(locator, baseDirs, ops, readFile) {
116
+ const shape = classify(locator);
117
+ if (shape.kind !== "file" || !shape.pathPart)
118
+ return undefined;
119
+ const lineMatch = locator.match(/:(\d+)$/);
120
+ const lineNum = lineMatch ? Number(lineMatch[1]) : undefined;
121
+ const candidatePath = ops.isAbsolute(shape.pathPart)
122
+ ? shape.pathPart
123
+ : baseDirs.filter(Boolean).map((base) => ops.resolve(base, shape.pathPart)).find((p) => ops.exists(p));
124
+ if (!candidatePath)
125
+ return undefined;
126
+ const content = readFile(candidatePath);
127
+ if (content === undefined)
128
+ return undefined;
129
+ if (lineNum && lineNum > 0) {
130
+ const lines = content.split("\n");
131
+ return lines[lineNum - 1] || undefined;
132
+ }
133
+ return content.slice(0, 200);
134
+ }
@@ -0,0 +1,199 @@
1
+ "use strict";
2
+ // core/trust/ledger.ts — the cross-agent handoff ledger kernel.
3
+ //
4
+ // MILESTONE 8 (v2/PLAN.md build order, step 8; SPEC/ledger-trust.md in
5
+ // full). Pure: computeLedgerDigest, buildLedgerProposal/Review,
6
+ // verifyLedgerEntry, applyLedgerProposal. Directory reads
7
+ // (listLedgerEntries/unionLedgerEntries) live in shell/ledger-io.ts, NOT
8
+ // here — this file touches no fs.
9
+ //
10
+ // Byte-exact port of the old build's src/ledger.ts. Uses
11
+ // core/hash.ts's `ledgerStableStringify` (byte-identical to
12
+ // `stableStringify`, kept as its own named export per PLAN.md's Hash
13
+ // dedup rule) rather than reimplementing a private copy.
14
+ //
15
+ // Evidence: SPEC/ledger-trust.md "Handoff ledger entry", "Exported
16
+ // functions", "Edge cases"; plugins/cool-workflow/src/ledger.ts:1-429.
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.computeLedgerDigest = computeLedgerDigest;
19
+ exports.buildLedgerProposal = buildLedgerProposal;
20
+ exports.buildLedgerReview = buildLedgerReview;
21
+ exports.verifyLedgerEntry = verifyLedgerEntry;
22
+ exports.applyLedgerProposal = applyLedgerProposal;
23
+ exports.resolveLedgerInbox = resolveLedgerInbox;
24
+ const hash_1 = require("../hash");
25
+ /** sha256 over the canonical content (every field except `id` and
26
+ * `digest`, which are derived FROM it). Returns the full `sha256:<hex>`
27
+ * form — the same prefixed spelling every other hash chain uses. */
28
+ function computeLedgerDigest(entry) {
29
+ return (0, hash_1.sha256)((0, hash_1.ledgerStableStringify)(entry));
30
+ }
31
+ /** Content-addressed id: `ldg-` + the first 16 hex chars of the digest. */
32
+ function deriveId(digest) {
33
+ return `ldg-${digest.replace(/^sha256:/, "").slice(0, 16)}`;
34
+ }
35
+ function seal(content) {
36
+ const digest = computeLedgerDigest(content);
37
+ return { ...content, id: deriveId(digest), digest };
38
+ }
39
+ function buildLedgerProposal(input) {
40
+ const content = {
41
+ kind: "proposal",
42
+ schemaVersion: 1,
43
+ from: input.from,
44
+ to: input.to,
45
+ title: input.title,
46
+ rationale: input.rationale,
47
+ targetFiles: [...input.targetFiles],
48
+ suggestedDiff: input.suggestedDiff || "",
49
+ createdAt: input.createdAt,
50
+ };
51
+ return seal(content);
52
+ }
53
+ function buildLedgerReview(input) {
54
+ const content = {
55
+ kind: "review",
56
+ schemaVersion: 1,
57
+ from: input.from,
58
+ to: input.to,
59
+ target: input.target,
60
+ verdict: input.verdict,
61
+ findings: [...input.findings],
62
+ createdAt: input.createdAt,
63
+ };
64
+ return seal(content);
65
+ }
66
+ function isRecord(value) {
67
+ return typeof value === "object" && value !== null && !Array.isArray(value);
68
+ }
69
+ const PROPOSAL_FIELDS = ["from", "to", "title", "rationale", "targetFiles", "suggestedDiff", "createdAt"];
70
+ const REVIEW_FIELDS = ["from", "to", "target", "verdict", "findings", "createdAt"];
71
+ /** Fail-closed verification. Any structural defect, unknown kind, or
72
+ * digest mismatch yields `ok:false`. Check names run in this order and
73
+ * stop at the first failure: structure, kind, schema, digest-present,
74
+ * fields (verdict nested inside for reviews), digest, id. */
75
+ function verifyLedgerEntry(raw) {
76
+ const checks = [];
77
+ const fail = (name, code, detail) => {
78
+ checks.push({ name, pass: false, code, detail });
79
+ return {
80
+ ok: false,
81
+ id: isRecord(raw) && typeof raw.id === "string" ? raw.id : null,
82
+ kind: isRecord(raw) && typeof raw.kind === "string" ? raw.kind : null,
83
+ checks,
84
+ failedChecks: checks
85
+ .filter((c) => !c.pass)
86
+ .map((c) => ({ name: c.name, code: c.code, detail: c.detail })),
87
+ };
88
+ };
89
+ if (!isRecord(raw))
90
+ return fail("structure", "ledger-not-object", "entry is not a JSON object");
91
+ checks.push({ name: "structure", pass: true });
92
+ const kind = raw.kind;
93
+ if (kind !== "proposal" && kind !== "review") {
94
+ return fail("kind", "ledger-unknown-kind", `kind must be proposal|review, got ${JSON.stringify(kind)}`);
95
+ }
96
+ checks.push({ name: "kind", pass: true });
97
+ if (raw.schemaVersion !== 1) {
98
+ return fail("schema", "ledger-bad-schema", `schemaVersion must be 1, got ${JSON.stringify(raw.schemaVersion)}`);
99
+ }
100
+ checks.push({ name: "schema", pass: true });
101
+ if (typeof raw.digest !== "string" || !raw.digest) {
102
+ return fail("digest-present", "ledger-missing-digest", "digest is absent or not a string");
103
+ }
104
+ checks.push({ name: "digest-present", pass: true });
105
+ const fields = kind === "proposal" ? PROPOSAL_FIELDS : REVIEW_FIELDS;
106
+ const content = { kind, schemaVersion: 1 };
107
+ for (const field of fields) {
108
+ if (!(field in raw))
109
+ return fail("fields", "ledger-missing-field", `required field ${field} is absent`);
110
+ content[field] = raw[field];
111
+ }
112
+ if (kind === "review" && raw.verdict !== "APPROVED" && raw.verdict !== "REJECTED") {
113
+ return fail("verdict", "ledger-bad-verdict", `verdict must be APPROVED|REJECTED, got ${JSON.stringify(raw.verdict)}`);
114
+ }
115
+ checks.push({ name: "fields", pass: true });
116
+ const recomputed = computeLedgerDigest(content);
117
+ if (recomputed !== raw.digest) {
118
+ return fail("digest", "ledger-digest-mismatch", `stored digest does not match content (recomputed ${recomputed})`);
119
+ }
120
+ checks.push({ name: "digest", pass: true });
121
+ // Bind the id to the content: it MUST be the content-addressed id
122
+ // derived from the digest. Without this, `id` is a free, unverified
123
+ // field (excluded from the digest) — a forged entry could set `id` to
124
+ // collide with a legit one, and any id-keyed de-duplication (`cw
125
+ // ledger list` union) would silently drop one of them.
126
+ const expectedId = deriveId(raw.digest);
127
+ if (raw.id !== expectedId) {
128
+ return fail("id", "ledger-id-mismatch", `id ${JSON.stringify(raw.id)} is not the content-addressed id for this digest (expected ${expectedId})`);
129
+ }
130
+ checks.push({ name: "id", pass: true });
131
+ return { ok: true, id: expectedId, kind, checks, failedChecks: [] };
132
+ }
133
+ /** Fail-closed extraction of a proposal's `suggestedDiff`. The diff can
134
+ * ONLY escape after the entry verifies: a tampered entry, a review (not
135
+ * a proposal), or a proposal with no diff all yield `ok:false` and
136
+ * `diff:null`. */
137
+ function applyLedgerProposal(raw) {
138
+ const verified = verifyLedgerEntry(raw);
139
+ if (!verified.ok) {
140
+ return { ok: false, id: verified.id, kind: verified.kind, diff: null, failedChecks: verified.failedChecks };
141
+ }
142
+ if (verified.kind !== "proposal") {
143
+ return {
144
+ ok: false,
145
+ id: verified.id,
146
+ kind: verified.kind,
147
+ diff: null,
148
+ failedChecks: [{ name: "kind", code: "ledger-not-a-proposal", detail: "apply expects a proposal entry, not a review" }],
149
+ };
150
+ }
151
+ const rec = isRecord(raw) ? raw : {};
152
+ const diff = typeof rec.suggestedDiff === "string" ? rec.suggestedDiff : "";
153
+ if (!diff) {
154
+ return {
155
+ ok: false,
156
+ id: verified.id,
157
+ kind: verified.kind,
158
+ diff: null,
159
+ failedChecks: [{ name: "diff", code: "ledger-empty-diff", detail: "proposal carries no suggestedDiff to apply" }],
160
+ };
161
+ }
162
+ return { ok: true, id: verified.id, kind: verified.kind, diff, failedChecks: [] };
163
+ }
164
+ /** Derive a machine-actionable inbox summary: pair each proposal with the
165
+ * review(s) that target it and report whether it is pending, approved,
166
+ * rejected, or contested. Only VERIFIED entries take part — a tampered
167
+ * review must never resolve a proposal. */
168
+ function resolveLedgerInbox(entries) {
169
+ const verified = entries.filter((e) => e.ok);
170
+ const reviews = verified.filter((e) => e.kind === "review" && e.target);
171
+ const proposals = verified
172
+ .filter((e) => e.kind === "proposal" && e.id)
173
+ .map((p) => {
174
+ const answering = reviews.filter((r) => r.target === p.id);
175
+ const verdicts = new Set(answering.map((r) => r.verdict));
176
+ let resolution;
177
+ if (answering.length === 0)
178
+ resolution = "pending";
179
+ else if (verdicts.size > 1)
180
+ resolution = "contested";
181
+ else
182
+ resolution = verdicts.has("APPROVED") ? "approved" : "rejected";
183
+ return {
184
+ id: p.id,
185
+ title: p.title,
186
+ resolution,
187
+ reviews: answering.map((r) => r.id).sort(),
188
+ };
189
+ })
190
+ .sort((a, b) => a.id.localeCompare(b.id));
191
+ const tally = (s) => proposals.filter((p) => p.resolution === s).length;
192
+ return {
193
+ proposals,
194
+ pending: tally("pending"),
195
+ approved: tally("approved"),
196
+ rejected: tally("rejected"),
197
+ contested: tally("contested"),
198
+ };
199
+ }