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.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +18 -7
- package/apps/architecture-review/app.json +2 -2
- package/apps/architecture-review/workflow.js +12 -12
- package/apps/architecture-review-fast/app.json +1 -1
- package/apps/end-to-end-golden-path/app.json +1 -1
- package/apps/pr-review-fix-ci/app.json +1 -1
- package/apps/release-cut/app.json +1 -1
- package/apps/research-synthesis/app.json +1 -1
- package/dist/cli/dispatch.js +236 -0
- package/dist/cli/entry.js +120 -0
- package/dist/cli/io.js +21 -4
- package/dist/cli/parseargv.js +157 -0
- package/dist/cli.js +6 -33
- package/dist/core/capability-table.js +3518 -0
- package/dist/core/format/help.js +314 -0
- package/dist/{state-explosion/format.js → core/format/state-explosion-text.js} +10 -1
- package/dist/core/hash.js +137 -0
- package/dist/core/multi-agent/candidate-scoring.js +219 -0
- package/dist/core/multi-agent/collaboration.js +481 -0
- package/dist/core/multi-agent/coordinator.js +515 -0
- package/dist/core/multi-agent/eval-replay.js +306 -0
- package/dist/core/multi-agent/runtime.js +929 -0
- package/dist/core/multi-agent/topology.js +298 -0
- package/dist/core/multi-agent/trust-policy.js +197 -0
- package/dist/core/pipeline/commit-gate.js +320 -0
- package/dist/{pipeline-contract.js → core/pipeline/contract.js} +24 -37
- package/dist/core/pipeline/dispatch.js +103 -0
- package/dist/core/pipeline/drive-decide.js +227 -0
- package/dist/core/pipeline/error-feedback.js +161 -0
- package/dist/core/pipeline/loop-expansion.js +124 -0
- package/dist/{result-normalize.js → core/pipeline/result-normalize.js} +14 -37
- package/dist/core/pipeline/runner.js +230 -0
- package/dist/{contract-migration.js → core/state/contract-migration.js} +68 -92
- package/dist/{state-migrations.js → core/state/migrations.js} +103 -96
- package/dist/core/state/node-projection.js +95 -0
- package/dist/core/state/node-snapshot.js +230 -0
- package/dist/core/state/run-paths.js +93 -0
- package/dist/{schema-validate.js → core/state/schema-validate.js} +35 -28
- package/dist/core/state/schema.js +50 -0
- package/dist/core/state/state-explosion/digest.js +243 -0
- package/dist/core/state/state-explosion/graph.js +527 -0
- package/dist/{state-explosion → core/state/state-explosion}/helpers.js +34 -3
- package/dist/core/state/state-explosion/report.js +187 -0
- package/dist/{state-explosion → core/state/state-explosion}/size.js +25 -7
- package/dist/{state-node.js → core/state/state-node.js} +90 -113
- package/dist/core/state/types.js +19 -0
- package/dist/{validation.js → core/state/validation.js} +64 -131
- package/dist/core/trust/evidence-grounding.js +134 -0
- package/dist/core/trust/ledger.js +199 -0
- package/dist/{telemetry-attestation.js → core/trust/telemetry-attestation.js} +94 -68
- package/dist/core/trust/telemetry-ledger.js +127 -0
- package/dist/core/types.js +20 -0
- package/dist/core/version.js +16 -0
- package/dist/{workflow-app-framework.js → core/workflow-apps/app-schema.js} +337 -426
- package/dist/mcp/dispatch.js +104 -0
- package/dist/mcp/server.js +144 -0
- package/dist/mcp-server.js +6 -84
- package/dist/{agent-config.js → shell/agent-config.js} +118 -71
- package/dist/shell/app-run-cli.js +88 -0
- package/dist/shell/audit-cli.js +259 -0
- package/dist/shell/audit-provenance.js +83 -0
- package/dist/shell/candidate-scoring-io.js +459 -0
- package/dist/shell/collaboration-io.js +264 -0
- package/dist/shell/commit-summary.js +104 -0
- package/dist/shell/commit.js +290 -0
- package/dist/shell/coordinator-io.js +476 -0
- package/dist/shell/demo-cli.js +19 -0
- package/dist/shell/dispatch.js +162 -0
- package/dist/shell/doctor.js +292 -0
- package/dist/shell/drive.js +885 -0
- package/dist/shell/error-feedback-io.js +305 -0
- package/dist/shell/eval-io.js +473 -0
- package/dist/{multi-agent-eval/format.js → shell/eval-text.js} +78 -49
- package/dist/{evidence-reasoning.js → shell/evidence-reasoning.js} +124 -255
- package/dist/shell/exec-backend-cli.js +88 -0
- package/dist/shell/execution-backend/agent.js +507 -0
- package/dist/shell/execution-backend/ci.js +15 -0
- package/dist/shell/execution-backend/container.js +69 -0
- package/dist/shell/execution-backend/envelopes.js +55 -0
- package/dist/shell/execution-backend/local.js +113 -0
- package/dist/shell/execution-backend/probes.js +175 -0
- package/dist/shell/execution-backend/registry.js +402 -0
- package/dist/shell/execution-backend/remote.js +128 -0
- package/dist/shell/execution-backend/types.js +11 -0
- package/dist/shell/feedback-cli.js +81 -0
- package/dist/shell/feedback-operations.js +48 -0
- package/dist/shell/fs-atomic.js +276 -0
- package/dist/shell/harness.js +98 -0
- package/dist/shell/ledger-cli.js +212 -0
- package/dist/shell/ledger-io.js +169 -0
- package/dist/shell/man-cli.js +89 -0
- package/dist/shell/metrics-cli.js +100 -0
- package/dist/shell/multi-agent-cli.js +1002 -0
- package/dist/shell/multi-agent-host.js +563 -0
- package/dist/shell/multi-agent-io.js +387 -0
- package/dist/{multi-agent-operator-ux.js → shell/multi-agent-operator-ux.js} +234 -191
- package/dist/shell/node-store.js +124 -0
- package/dist/{observability/format.js → shell/observability-format.js} +7 -1
- package/dist/{observability/intake.js → shell/observability-intake.js} +79 -56
- package/dist/{observability.js → shell/observability.js} +159 -332
- package/dist/{onramp.js → shell/onramp.js} +11 -0
- package/dist/{operator-ux/format.js → shell/operator-ux-text.js} +250 -239
- package/dist/shell/operator-ux.js +431 -0
- package/dist/shell/orchestrator.js +231 -0
- package/dist/shell/pipeline-cli.js +667 -0
- package/dist/shell/pipeline.js +217 -0
- package/dist/shell/reclamation-io.js +1366 -0
- package/dist/shell/registry-cli.js +344 -0
- package/dist/{remote-source.js → shell/remote-source.js} +2 -2
- package/dist/shell/report-cli.js +101 -0
- package/dist/shell/report-view-cli.js +117 -0
- package/dist/{orchestrator → shell}/report.js +289 -282
- package/dist/shell/reporter.js +62 -0
- package/dist/shell/run-export-cli.js +106 -0
- package/dist/shell/run-export.js +680 -0
- package/dist/shell/run-registry-io.js +1014 -0
- package/dist/shell/run-store.js +164 -0
- package/dist/{sandbox-profile.js → shell/sandbox-profile.js} +134 -95
- package/dist/{scheduler.js → shell/scheduler-io.js} +257 -48
- package/dist/shell/scheduling-io.js +321 -0
- package/dist/shell/state-cli.js +181 -0
- package/dist/shell/state-explosion-cli.js +197 -0
- package/dist/shell/telemetry-cli.js +85 -0
- package/dist/{telemetry-demo.js → shell/telemetry-demo.js} +149 -119
- package/dist/shell/telemetry-ledger-io.js +132 -0
- package/dist/{term.js → shell/term.js} +36 -46
- package/dist/shell/topology-io.js +361 -0
- package/dist/shell/trust-audit.js +472 -0
- package/dist/{multi-agent-trust.js → shell/trust-policy-io.js} +67 -205
- package/dist/shell/verifier.js +48 -0
- package/dist/shell/workbench-host.js +258 -0
- package/dist/shell/workbench-text.js +18 -0
- package/dist/shell/workbench.js +170 -0
- package/dist/shell/worker-cli.js +124 -0
- package/dist/shell/worker-isolation.js +852 -0
- package/dist/shell/workflow-app-loader.js +650 -0
- package/docs/agent-delegation-drive.7.md +4 -0
- package/docs/cli-mcp-parity.7.md +282 -219
- package/docs/contract-migration-tooling.7.md +4 -0
- package/docs/control-plane-scheduling.7.md +4 -0
- package/docs/durable-state-and-locking.7.md +4 -0
- package/docs/evidence-adoption-reasoning-chain.7.md +4 -0
- package/docs/execution-backends.7.md +4 -0
- package/docs/multi-agent-cli-mcp-surface.7.md +4 -0
- package/docs/multi-agent-eval-replay-harness.7.md +4 -0
- package/docs/multi-agent-operator-ux.7.md +4 -0
- package/docs/node-snapshot-diff-replay.7.md +4 -0
- package/docs/observability-cost-accounting.7.md +6 -1
- package/docs/project-index.md +137 -71
- package/docs/real-execution-backends.7.md +4 -0
- package/docs/release-and-migration.7.md +4 -0
- package/docs/release-tooling.7.md +4 -0
- package/docs/remote-source-review.7.md +4 -4
- package/docs/report-verifiable-bundle.7.md +1 -1
- package/docs/run-registry-control-plane.7.md +4 -0
- package/docs/run-retention-reclamation.7.md +25 -0
- package/docs/security-trust-hardening.7.md +3 -1
- package/docs/state-explosion-management.7.md +4 -0
- package/docs/team-collaboration.7.md +4 -0
- package/docs/web-desktop-workbench.7.md +15 -1
- package/manifest/plugin.manifest.json +1 -1
- package/manifest/source-context-profiles.json +9 -13
- package/package.json +1 -1
- package/scripts/agents/agent-adapter-core.js +22 -2
- package/scripts/agents/claude-p-agent.js +8 -3
- package/scripts/agents/cw-attest-wrap.js +2 -2
- package/scripts/agents/gemini-agent.js +5 -1
- package/scripts/agents/opencode-agent.js +5 -1
- package/scripts/bump-version.js +4 -3
- package/scripts/canonical-apps.js +4 -4
- package/scripts/dogfood-architecture-review.js +2 -3
- package/scripts/dogfood-release.js +3 -3
- package/scripts/gen-parity-doc.js +15 -2
- package/scripts/golden-path.js +4 -4
- package/scripts/onramp-check.js +1 -1
- package/scripts/parity-check.js +38 -21
- package/scripts/release-flow.js +2 -2
- package/scripts/sync-project-index.js +51 -27
- package/scripts/validate-run-state-schema.js +2 -2
- package/scripts/version-sync-check.js +30 -30
- package/dist/candidate-scoring.js +0 -729
- package/dist/capability-core.js +0 -1189
- package/dist/capability-registry.js +0 -885
- package/dist/cli/command-surface.js +0 -494
- package/dist/cli/format.js +0 -56
- package/dist/cli/handlers/audit.js +0 -82
- package/dist/cli/handlers/blackboard.js +0 -81
- package/dist/cli/handlers/candidate.js +0 -40
- package/dist/cli/handlers/clones.js +0 -34
- package/dist/cli/handlers/collaboration.js +0 -61
- package/dist/cli/handlers/eval.js +0 -40
- package/dist/cli/handlers/ledger.js +0 -169
- package/dist/cli/handlers/maintenance.js +0 -107
- package/dist/cli/handlers/multi-agent.js +0 -165
- package/dist/cli/handlers/node.js +0 -41
- package/dist/cli/handlers/operational.js +0 -155
- package/dist/cli/handlers/operator.js +0 -146
- package/dist/cli/handlers/registry.js +0 -68
- package/dist/cli/handlers/run.js +0 -153
- package/dist/cli/handlers/scheduling.js +0 -132
- package/dist/cli/handlers/workbench.js +0 -41
- package/dist/cli/handlers/worker.js +0 -45
- package/dist/cli/run-summary.js +0 -45
- package/dist/clones.js +0 -162
- package/dist/collaboration.js +0 -726
- package/dist/commit.js +0 -592
- package/dist/compare.js +0 -18
- package/dist/coordinator/classify.js +0 -45
- package/dist/coordinator/paths.js +0 -42
- package/dist/coordinator/util.js +0 -126
- package/dist/coordinator.js +0 -990
- package/dist/daemon.js +0 -44
- package/dist/dispatch.js +0 -250
- package/dist/doctor.js +0 -212
- package/dist/drive.js +0 -864
- package/dist/error-feedback.js +0 -419
- package/dist/evidence-grounding.js +0 -184
- package/dist/execution-backend/agent.js +0 -354
- package/dist/execution-backend/probes.js +0 -112
- package/dist/execution-backend/util.js +0 -47
- package/dist/execution-backend.js +0 -961
- package/dist/gates.js +0 -48
- package/dist/harness.js +0 -61
- package/dist/ledger.js +0 -313
- package/dist/loop-expansion.js +0 -60
- package/dist/mcp/tool-call.js +0 -470
- package/dist/mcp/tool-definitions.js +0 -1066
- package/dist/mcp-surface.js +0 -30
- package/dist/multi-agent/graph.js +0 -84
- package/dist/multi-agent/helpers.js +0 -141
- package/dist/multi-agent/ids.js +0 -20
- package/dist/multi-agent/paths.js +0 -22
- package/dist/multi-agent-eval/normalize.js +0 -51
- package/dist/multi-agent-eval.js +0 -678
- package/dist/multi-agent-host.js +0 -777
- package/dist/multi-agent.js +0 -984
- package/dist/node-projection.js +0 -59
- package/dist/node-snapshot.js +0 -260
- package/dist/operator-ux.js +0 -631
- package/dist/orchestrator/app-operations.js +0 -211
- package/dist/orchestrator/audit-operations.js +0 -182
- package/dist/orchestrator/candidate-operations.js +0 -117
- package/dist/orchestrator/cli-options.js +0 -294
- package/dist/orchestrator/collaboration-operations.js +0 -86
- package/dist/orchestrator/feedback-operations.js +0 -81
- package/dist/orchestrator/host-operations.js +0 -78
- package/dist/orchestrator/lifecycle-operations.js +0 -650
- package/dist/orchestrator/migration-operations.js +0 -44
- package/dist/orchestrator/multi-agent-operations.js +0 -362
- package/dist/orchestrator/topology-operations.js +0 -84
- package/dist/orchestrator.js +0 -925
- package/dist/pipeline-runner.js +0 -285
- package/dist/reclamation/hash.js +0 -72
- package/dist/reclamation.js +0 -812
- package/dist/reporter.js +0 -67
- package/dist/run-export.js +0 -815
- package/dist/run-registry/derive.js +0 -175
- package/dist/run-registry/format.js +0 -124
- package/dist/run-registry/gc.js +0 -251
- package/dist/run-registry/policy.js +0 -16
- package/dist/run-registry/queue.js +0 -115
- package/dist/run-registry.js +0 -850
- package/dist/run-state-schema.js +0 -68
- package/dist/scheduling.js +0 -184
- package/dist/state-explosion.js +0 -1014
- package/dist/state.js +0 -367
- package/dist/telemetry-ledger.js +0 -196
- package/dist/topology.js +0 -565
- package/dist/triggers.js +0 -184
- package/dist/trust-audit.js +0 -644
- package/dist/types/blackboard.js +0 -2
- package/dist/types/candidate.js +0 -2
- package/dist/types/collaboration.js +0 -2
- package/dist/types/core.js +0 -2
- package/dist/types/drive.js +0 -10
- package/dist/types/error-feedback.js +0 -2
- package/dist/types/evidence-reasoning.js +0 -2
- package/dist/types/execution-backend.js +0 -2
- package/dist/types/multi-agent.js +0 -2
- package/dist/types/observability.js +0 -2
- package/dist/types/pipeline.js +0 -2
- package/dist/types/reclamation.js +0 -8
- package/dist/types/report-bundle.js +0 -6
- package/dist/types/result.js +0 -2
- package/dist/types/run-registry.js +0 -2
- package/dist/types/run.js +0 -2
- package/dist/types/sandbox.js +0 -2
- package/dist/types/schedule.js +0 -2
- package/dist/types/state-node.js +0 -2
- package/dist/types/topology.js +0 -2
- package/dist/types/trust.js +0 -2
- package/dist/types/workbench.js +0 -2
- package/dist/types/worker.js +0 -2
- package/dist/types/workflow-app.js +0 -2
- package/dist/types.js +0 -44
- package/dist/util/fingerprint.js +0 -19
- package/dist/util/fingerprint.test.js +0 -27
- package/dist/verifier.js +0 -78
- package/dist/version.js +0 -8
- package/dist/workbench-host.js +0 -192
- package/dist/workbench.js +0 -192
- package/dist/worker-accept/acceptance.js +0 -114
- package/dist/worker-accept/blackboard-fanout.js +0 -80
- package/dist/worker-accept/blackboard-linkage.js +0 -19
- package/dist/worker-accept/context.js +0 -2
- package/dist/worker-accept/telemetry-ledger.js +0 -126
- package/dist/worker-accept/validation.js +0 -77
- package/dist/worker-accept/verifier-completion.js +0 -73
- package/dist/worker-isolation/helpers.js +0 -51
- package/dist/worker-isolation/paths.js +0 -46
- package/dist/worker-isolation.js +0 -656
- package/dist/workflow-api.js +0 -131
- /package/dist/{types → core/types}/boundary.js +0 -0
|
@@ -1,47 +1,58 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
//
|
|
2
|
+
// core/state/validation.ts — RecordValidationError + per-record shape
|
|
3
|
+
// guards.
|
|
3
4
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
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
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
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
|
-
//
|
|
18
|
-
//
|
|
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
|
|
37
|
-
return
|
|
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
|
|
43
|
-
return
|
|
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
|
-
|
|
64
|
-
|
|
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
|
|
81
|
-
// Required
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
// errors object[]. Optional fields are not enforced (additive,
|
|
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
|
|
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
|
|
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
|
|
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 —
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
-
/**
|
|
254
|
-
|
|
255
|
-
|
|
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
|
+
}
|