cool-workflow 0.1.98 → 0.2.1

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 (315) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/README.md +18 -7
  4. package/apps/architecture-review/app.json +2 -2
  5. package/apps/architecture-review/workflow.js +12 -12
  6. package/apps/architecture-review-fast/app.json +1 -1
  7. package/apps/end-to-end-golden-path/app.json +1 -1
  8. package/apps/pr-review-fix-ci/app.json +1 -1
  9. package/apps/release-cut/app.json +1 -1
  10. package/apps/research-synthesis/app.json +1 -1
  11. package/dist/cli/dispatch.js +236 -0
  12. package/dist/cli/entry.js +120 -0
  13. package/dist/cli/io.js +21 -4
  14. package/dist/cli/parseargv.js +157 -0
  15. package/dist/cli.js +6 -33
  16. package/dist/core/capability-table.js +3518 -0
  17. package/dist/core/format/help.js +314 -0
  18. package/dist/{state-explosion/format.js → core/format/state-explosion-text.js} +10 -1
  19. package/dist/core/hash.js +137 -0
  20. package/dist/core/multi-agent/candidate-scoring.js +219 -0
  21. package/dist/core/multi-agent/collaboration.js +481 -0
  22. package/dist/core/multi-agent/coordinator.js +515 -0
  23. package/dist/core/multi-agent/eval-replay.js +306 -0
  24. package/dist/core/multi-agent/runtime.js +929 -0
  25. package/dist/core/multi-agent/topology.js +298 -0
  26. package/dist/core/multi-agent/trust-policy.js +197 -0
  27. package/dist/core/pipeline/commit-gate.js +320 -0
  28. package/dist/{pipeline-contract.js → core/pipeline/contract.js} +24 -37
  29. package/dist/core/pipeline/dispatch.js +103 -0
  30. package/dist/core/pipeline/drive-decide.js +227 -0
  31. package/dist/core/pipeline/error-feedback.js +161 -0
  32. package/dist/core/pipeline/loop-expansion.js +124 -0
  33. package/dist/{result-normalize.js → core/pipeline/result-normalize.js} +14 -37
  34. package/dist/core/pipeline/runner.js +230 -0
  35. package/dist/{contract-migration.js → core/state/contract-migration.js} +68 -92
  36. package/dist/{state-migrations.js → core/state/migrations.js} +103 -96
  37. package/dist/core/state/node-projection.js +95 -0
  38. package/dist/core/state/node-snapshot.js +230 -0
  39. package/dist/core/state/run-paths.js +93 -0
  40. package/dist/{schema-validate.js → core/state/schema-validate.js} +35 -28
  41. package/dist/core/state/schema.js +50 -0
  42. package/dist/core/state/state-explosion/digest.js +243 -0
  43. package/dist/core/state/state-explosion/graph.js +527 -0
  44. package/dist/{state-explosion → core/state/state-explosion}/helpers.js +34 -3
  45. package/dist/core/state/state-explosion/report.js +187 -0
  46. package/dist/{state-explosion → core/state/state-explosion}/size.js +25 -7
  47. package/dist/{state-node.js → core/state/state-node.js} +90 -113
  48. package/dist/core/state/types.js +19 -0
  49. package/dist/{validation.js → core/state/validation.js} +64 -131
  50. package/dist/core/trust/evidence-grounding.js +134 -0
  51. package/dist/core/trust/ledger.js +199 -0
  52. package/dist/{telemetry-attestation.js → core/trust/telemetry-attestation.js} +94 -68
  53. package/dist/core/trust/telemetry-ledger.js +127 -0
  54. package/dist/core/types.js +20 -0
  55. package/dist/core/version.js +16 -0
  56. package/dist/{workflow-app-framework.js → core/workflow-apps/app-schema.js} +337 -426
  57. package/dist/mcp/dispatch.js +104 -0
  58. package/dist/mcp/server.js +144 -0
  59. package/dist/mcp-server.js +6 -84
  60. package/dist/{agent-config.js → shell/agent-config.js} +118 -71
  61. package/dist/shell/app-run-cli.js +88 -0
  62. package/dist/shell/audit-cli.js +259 -0
  63. package/dist/shell/audit-provenance.js +83 -0
  64. package/dist/shell/candidate-scoring-io.js +459 -0
  65. package/dist/shell/collaboration-io.js +264 -0
  66. package/dist/shell/commit-summary.js +104 -0
  67. package/dist/shell/commit.js +290 -0
  68. package/dist/shell/coordinator-io.js +476 -0
  69. package/dist/shell/demo-cli.js +19 -0
  70. package/dist/shell/dispatch.js +162 -0
  71. package/dist/shell/doctor.js +292 -0
  72. package/dist/shell/drive.js +885 -0
  73. package/dist/shell/error-feedback-io.js +305 -0
  74. package/dist/shell/eval-io.js +473 -0
  75. package/dist/{multi-agent-eval/format.js → shell/eval-text.js} +78 -49
  76. package/dist/{evidence-reasoning.js → shell/evidence-reasoning.js} +124 -255
  77. package/dist/shell/exec-backend-cli.js +88 -0
  78. package/dist/shell/execution-backend/agent.js +507 -0
  79. package/dist/shell/execution-backend/ci.js +15 -0
  80. package/dist/shell/execution-backend/container.js +69 -0
  81. package/dist/shell/execution-backend/envelopes.js +55 -0
  82. package/dist/shell/execution-backend/local.js +113 -0
  83. package/dist/shell/execution-backend/probes.js +175 -0
  84. package/dist/shell/execution-backend/registry.js +402 -0
  85. package/dist/shell/execution-backend/remote.js +128 -0
  86. package/dist/shell/execution-backend/types.js +11 -0
  87. package/dist/shell/feedback-cli.js +81 -0
  88. package/dist/shell/feedback-operations.js +48 -0
  89. package/dist/shell/fs-atomic.js +276 -0
  90. package/dist/shell/harness.js +98 -0
  91. package/dist/shell/ledger-cli.js +212 -0
  92. package/dist/shell/ledger-io.js +169 -0
  93. package/dist/shell/man-cli.js +89 -0
  94. package/dist/shell/metrics-cli.js +100 -0
  95. package/dist/shell/multi-agent-cli.js +1002 -0
  96. package/dist/shell/multi-agent-host.js +563 -0
  97. package/dist/shell/multi-agent-io.js +387 -0
  98. package/dist/{multi-agent-operator-ux.js → shell/multi-agent-operator-ux.js} +234 -191
  99. package/dist/shell/node-store.js +124 -0
  100. package/dist/{observability/format.js → shell/observability-format.js} +7 -1
  101. package/dist/{observability/intake.js → shell/observability-intake.js} +79 -56
  102. package/dist/{observability.js → shell/observability.js} +159 -332
  103. package/dist/{onramp.js → shell/onramp.js} +11 -0
  104. package/dist/{operator-ux/format.js → shell/operator-ux-text.js} +250 -239
  105. package/dist/shell/operator-ux.js +431 -0
  106. package/dist/shell/orchestrator.js +231 -0
  107. package/dist/shell/pipeline-cli.js +667 -0
  108. package/dist/shell/pipeline.js +217 -0
  109. package/dist/shell/reclamation-io.js +1366 -0
  110. package/dist/shell/registry-cli.js +344 -0
  111. package/dist/{remote-source.js → shell/remote-source.js} +2 -2
  112. package/dist/shell/report-cli.js +101 -0
  113. package/dist/shell/report-view-cli.js +117 -0
  114. package/dist/{orchestrator → shell}/report.js +289 -282
  115. package/dist/shell/reporter.js +62 -0
  116. package/dist/shell/run-export-cli.js +106 -0
  117. package/dist/shell/run-export.js +680 -0
  118. package/dist/shell/run-registry-io.js +1014 -0
  119. package/dist/shell/run-store.js +164 -0
  120. package/dist/{sandbox-profile.js → shell/sandbox-profile.js} +134 -95
  121. package/dist/{scheduler.js → shell/scheduler-io.js} +257 -48
  122. package/dist/shell/scheduling-io.js +321 -0
  123. package/dist/shell/state-cli.js +181 -0
  124. package/dist/shell/state-explosion-cli.js +197 -0
  125. package/dist/shell/telemetry-cli.js +85 -0
  126. package/dist/{telemetry-demo.js → shell/telemetry-demo.js} +149 -119
  127. package/dist/shell/telemetry-ledger-io.js +132 -0
  128. package/dist/{term.js → shell/term.js} +36 -46
  129. package/dist/shell/topology-io.js +361 -0
  130. package/dist/shell/trust-audit.js +472 -0
  131. package/dist/{multi-agent-trust.js → shell/trust-policy-io.js} +67 -205
  132. package/dist/shell/verifier.js +48 -0
  133. package/dist/shell/workbench-host.js +258 -0
  134. package/dist/shell/workbench-text.js +18 -0
  135. package/dist/shell/workbench.js +170 -0
  136. package/dist/shell/worker-cli.js +124 -0
  137. package/dist/shell/worker-isolation.js +852 -0
  138. package/dist/shell/workflow-app-loader.js +650 -0
  139. package/docs/agent-delegation-drive.7.md +4 -0
  140. package/docs/cli-mcp-parity.7.md +282 -219
  141. package/docs/contract-migration-tooling.7.md +4 -0
  142. package/docs/control-plane-scheduling.7.md +4 -0
  143. package/docs/durable-state-and-locking.7.md +4 -0
  144. package/docs/evidence-adoption-reasoning-chain.7.md +4 -0
  145. package/docs/execution-backends.7.md +4 -0
  146. package/docs/multi-agent-cli-mcp-surface.7.md +4 -0
  147. package/docs/multi-agent-eval-replay-harness.7.md +4 -0
  148. package/docs/multi-agent-operator-ux.7.md +4 -0
  149. package/docs/node-snapshot-diff-replay.7.md +4 -0
  150. package/docs/observability-cost-accounting.7.md +6 -1
  151. package/docs/project-index.md +137 -71
  152. package/docs/real-execution-backends.7.md +4 -0
  153. package/docs/release-and-migration.7.md +4 -0
  154. package/docs/release-tooling.7.md +4 -0
  155. package/docs/remote-source-review.7.md +4 -4
  156. package/docs/report-verifiable-bundle.7.md +1 -1
  157. package/docs/run-registry-control-plane.7.md +4 -0
  158. package/docs/run-retention-reclamation.7.md +25 -0
  159. package/docs/security-trust-hardening.7.md +3 -1
  160. package/docs/state-explosion-management.7.md +4 -0
  161. package/docs/team-collaboration.7.md +4 -0
  162. package/docs/web-desktop-workbench.7.md +15 -1
  163. package/manifest/plugin.manifest.json +1 -1
  164. package/manifest/source-context-profiles.json +9 -13
  165. package/package.json +1 -1
  166. package/scripts/agents/agent-adapter-core.js +22 -2
  167. package/scripts/agents/claude-p-agent.js +8 -3
  168. package/scripts/agents/cw-attest-wrap.js +2 -2
  169. package/scripts/agents/gemini-agent.js +5 -1
  170. package/scripts/agents/opencode-agent.js +5 -1
  171. package/scripts/bump-version.js +4 -3
  172. package/scripts/canonical-apps.js +4 -4
  173. package/scripts/dogfood-architecture-review.js +2 -3
  174. package/scripts/dogfood-release.js +3 -3
  175. package/scripts/gen-parity-doc.js +15 -2
  176. package/scripts/golden-path.js +4 -4
  177. package/scripts/onramp-check.js +1 -1
  178. package/scripts/parity-check.js +38 -21
  179. package/scripts/release-flow.js +2 -2
  180. package/scripts/sync-project-index.js +51 -27
  181. package/scripts/validate-run-state-schema.js +2 -2
  182. package/scripts/version-sync-check.js +30 -30
  183. package/dist/candidate-scoring.js +0 -729
  184. package/dist/capability-core.js +0 -1189
  185. package/dist/capability-registry.js +0 -885
  186. package/dist/cli/command-surface.js +0 -494
  187. package/dist/cli/format.js +0 -56
  188. package/dist/cli/handlers/audit.js +0 -82
  189. package/dist/cli/handlers/blackboard.js +0 -81
  190. package/dist/cli/handlers/candidate.js +0 -40
  191. package/dist/cli/handlers/clones.js +0 -34
  192. package/dist/cli/handlers/collaboration.js +0 -61
  193. package/dist/cli/handlers/eval.js +0 -40
  194. package/dist/cli/handlers/ledger.js +0 -169
  195. package/dist/cli/handlers/maintenance.js +0 -107
  196. package/dist/cli/handlers/multi-agent.js +0 -165
  197. package/dist/cli/handlers/node.js +0 -41
  198. package/dist/cli/handlers/operational.js +0 -155
  199. package/dist/cli/handlers/operator.js +0 -146
  200. package/dist/cli/handlers/registry.js +0 -68
  201. package/dist/cli/handlers/run.js +0 -153
  202. package/dist/cli/handlers/scheduling.js +0 -132
  203. package/dist/cli/handlers/workbench.js +0 -41
  204. package/dist/cli/handlers/worker.js +0 -45
  205. package/dist/cli/run-summary.js +0 -45
  206. package/dist/clones.js +0 -162
  207. package/dist/collaboration.js +0 -726
  208. package/dist/commit.js +0 -592
  209. package/dist/compare.js +0 -18
  210. package/dist/coordinator/classify.js +0 -45
  211. package/dist/coordinator/paths.js +0 -42
  212. package/dist/coordinator/util.js +0 -126
  213. package/dist/coordinator.js +0 -990
  214. package/dist/daemon.js +0 -44
  215. package/dist/dispatch.js +0 -250
  216. package/dist/doctor.js +0 -212
  217. package/dist/drive.js +0 -864
  218. package/dist/error-feedback.js +0 -419
  219. package/dist/evidence-grounding.js +0 -184
  220. package/dist/execution-backend/agent.js +0 -354
  221. package/dist/execution-backend/probes.js +0 -112
  222. package/dist/execution-backend/util.js +0 -47
  223. package/dist/execution-backend.js +0 -961
  224. package/dist/gates.js +0 -48
  225. package/dist/harness.js +0 -61
  226. package/dist/ledger.js +0 -313
  227. package/dist/loop-expansion.js +0 -60
  228. package/dist/mcp/tool-call.js +0 -470
  229. package/dist/mcp/tool-definitions.js +0 -1066
  230. package/dist/mcp-surface.js +0 -30
  231. package/dist/multi-agent/graph.js +0 -84
  232. package/dist/multi-agent/helpers.js +0 -141
  233. package/dist/multi-agent/ids.js +0 -20
  234. package/dist/multi-agent/paths.js +0 -22
  235. package/dist/multi-agent-eval/normalize.js +0 -51
  236. package/dist/multi-agent-eval.js +0 -678
  237. package/dist/multi-agent-host.js +0 -777
  238. package/dist/multi-agent.js +0 -984
  239. package/dist/node-projection.js +0 -59
  240. package/dist/node-snapshot.js +0 -260
  241. package/dist/operator-ux.js +0 -631
  242. package/dist/orchestrator/app-operations.js +0 -211
  243. package/dist/orchestrator/audit-operations.js +0 -182
  244. package/dist/orchestrator/candidate-operations.js +0 -117
  245. package/dist/orchestrator/cli-options.js +0 -294
  246. package/dist/orchestrator/collaboration-operations.js +0 -86
  247. package/dist/orchestrator/feedback-operations.js +0 -81
  248. package/dist/orchestrator/host-operations.js +0 -78
  249. package/dist/orchestrator/lifecycle-operations.js +0 -650
  250. package/dist/orchestrator/migration-operations.js +0 -44
  251. package/dist/orchestrator/multi-agent-operations.js +0 -362
  252. package/dist/orchestrator/topology-operations.js +0 -84
  253. package/dist/orchestrator.js +0 -925
  254. package/dist/pipeline-runner.js +0 -285
  255. package/dist/reclamation/hash.js +0 -72
  256. package/dist/reclamation.js +0 -812
  257. package/dist/reporter.js +0 -67
  258. package/dist/run-export.js +0 -815
  259. package/dist/run-registry/derive.js +0 -175
  260. package/dist/run-registry/format.js +0 -124
  261. package/dist/run-registry/gc.js +0 -251
  262. package/dist/run-registry/policy.js +0 -16
  263. package/dist/run-registry/queue.js +0 -115
  264. package/dist/run-registry.js +0 -850
  265. package/dist/run-state-schema.js +0 -68
  266. package/dist/scheduling.js +0 -184
  267. package/dist/state-explosion.js +0 -1014
  268. package/dist/state.js +0 -367
  269. package/dist/telemetry-ledger.js +0 -196
  270. package/dist/topology.js +0 -565
  271. package/dist/triggers.js +0 -184
  272. package/dist/trust-audit.js +0 -644
  273. package/dist/types/blackboard.js +0 -2
  274. package/dist/types/candidate.js +0 -2
  275. package/dist/types/collaboration.js +0 -2
  276. package/dist/types/core.js +0 -2
  277. package/dist/types/drive.js +0 -10
  278. package/dist/types/error-feedback.js +0 -2
  279. package/dist/types/evidence-reasoning.js +0 -2
  280. package/dist/types/execution-backend.js +0 -2
  281. package/dist/types/multi-agent.js +0 -2
  282. package/dist/types/observability.js +0 -2
  283. package/dist/types/pipeline.js +0 -2
  284. package/dist/types/reclamation.js +0 -8
  285. package/dist/types/report-bundle.js +0 -6
  286. package/dist/types/result.js +0 -2
  287. package/dist/types/run-registry.js +0 -2
  288. package/dist/types/run.js +0 -2
  289. package/dist/types/sandbox.js +0 -2
  290. package/dist/types/schedule.js +0 -2
  291. package/dist/types/state-node.js +0 -2
  292. package/dist/types/topology.js +0 -2
  293. package/dist/types/trust.js +0 -2
  294. package/dist/types/workbench.js +0 -2
  295. package/dist/types/worker.js +0 -2
  296. package/dist/types/workflow-app.js +0 -2
  297. package/dist/types.js +0 -44
  298. package/dist/util/fingerprint.js +0 -19
  299. package/dist/util/fingerprint.test.js +0 -27
  300. package/dist/verifier.js +0 -78
  301. package/dist/version.js +0 -8
  302. package/dist/workbench-host.js +0 -192
  303. package/dist/workbench.js +0 -192
  304. package/dist/worker-accept/acceptance.js +0 -114
  305. package/dist/worker-accept/blackboard-fanout.js +0 -80
  306. package/dist/worker-accept/blackboard-linkage.js +0 -19
  307. package/dist/worker-accept/context.js +0 -2
  308. package/dist/worker-accept/telemetry-ledger.js +0 -126
  309. package/dist/worker-accept/validation.js +0 -77
  310. package/dist/worker-accept/verifier-completion.js +0 -73
  311. package/dist/worker-isolation/helpers.js +0 -51
  312. package/dist/worker-isolation/paths.js +0 -46
  313. package/dist/worker-isolation.js +0 -656
  314. package/dist/workflow-api.js +0 -131
  315. /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 (docs/rebuild/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
+ }