owlrunkit 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/LICENSE +674 -0
- package/NOTICE.md +137 -0
- package/README.md +44 -0
- package/SOURCE.md +10 -0
- package/docs/architecture/OWLCODA_RUN_KIT_CONTRACT_V0_1.md +160 -0
- package/docs/architecture/OWLCODA_RUN_KIT_CONTRACT_V0_2.md +459 -0
- package/integrations/codex/skills/owlcoda-runkit/SKILL.md +408 -0
- package/integrations/codex/skills/owlcoda-runkit/agents/openai.yaml +4 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/agents-snippet.md +7 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/config.json +10 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/coverage-adopt-request.json +26 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/execution-delivery-packet.json +13 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/finalize-request.json +18 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/goal-contract.json +11 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/profiles.json +9 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/ready-for-commit-request.json +11 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/resource-preflight-request.json +56 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/resume-request.json +10 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/snapshot-request.json +23 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/verification-context.json +15 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/verification-receipt.json +11 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/verify-plan-request.json +31 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/visual-smoke-request.json +32 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/worker-lease.json +7 -0
- package/package.json +24 -0
- package/packages/attest/LICENSE +674 -0
- package/packages/attest/README.md +63 -0
- package/packages/attest/cli/owlcoda-attest.mjs +155 -0
- package/packages/attest/consumer-surface-v1.json +255 -0
- package/packages/attest/distribution-manifest-v1.json +104 -0
- package/packages/attest/fixtures/bad/accepted-authority-leak.json +7 -0
- package/packages/attest/fixtures/bad/accepted-failed-receipt.json +7 -0
- package/packages/attest/fixtures/bad/active-context-binding-mismatch.json +7 -0
- package/packages/attest/fixtures/bad/active-source-binding-mismatch.json +7 -0
- package/packages/attest/fixtures/bad/context-fingerprint-mismatch.json +7 -0
- package/packages/attest/fixtures/bad/duplicate-object-key.json +8 -0
- package/packages/attest/fixtures/bad/multiple-active-leaves.json +10 -0
- package/packages/attest/fixtures/bad/orphan-replacement.json +7 -0
- package/packages/attest/fixtures/bad/root-shape-extra-property.json +8 -0
- package/packages/attest/fixtures/bad/source-fingerprint-mismatch.json +7 -0
- package/packages/attest/fixtures/good/accepted-replacement-chain.json +59 -0
- package/packages/attest/fixtures/good/blocked-single-leaf.json +9 -0
- package/packages/attest/fixtures/index.json +90 -0
- package/packages/attest/sbom.spdx.json +33 -0
- package/packages/attest/schemas/attestation-ref-v1.schema.json +44 -0
- package/packages/attest/schemas/attestation-result-v1.schema.json +163 -0
- package/packages/attest/schemas/offline-attestation-bundle-v1.schema.json +30 -0
- package/packages/attest/schemas/public-verification-bundle-v1.schema.json +74 -0
- package/packages/attest/schemas/quick-verification-receipt-v1.schema.json +219 -0
- package/packages/attest/schemas/workspace-snapshot-v1.schema.json +135 -0
- package/packages/attest/src/canonical.mjs +12 -0
- package/packages/attest/src/formal.mjs +762 -0
- package/packages/attest/src/index.mjs +113 -0
- package/packages/attest/src/offline-bundle.mjs +108 -0
- package/packages/attest/src/quick-receipt-contract.mjs +156 -0
- package/packages/attest/src/quick.mjs +274 -0
- package/packages/attest/src/reference-contract.mjs +64 -0
- package/packages/attest/src/reference.mjs +131 -0
- package/packages/attest/src/workspace-snapshot.mjs +171 -0
- package/registry-release-policy-v1.json +34 -0
- package/sbom.spdx.json +39 -0
- package/scripts/runkit-contract/acceptance-evidence.mjs +103 -0
- package/scripts/runkit-contract/core-contract.mjs +392 -0
- package/scripts/runkit-contract/coverage-adoption.mjs +414 -0
- package/scripts/runkit-contract/delivery-create.mjs +252 -0
- package/scripts/runkit-contract/delivery-selection.mjs +31 -0
- package/scripts/runkit-contract/finalize.mjs +790 -0
- package/scripts/runkit-contract/inspect-presentation.mjs +183 -0
- package/scripts/runkit-contract/install-codex-skill.mjs +239 -0
- package/scripts/runkit-contract/lease-lifecycle.mjs +324 -0
- package/scripts/runkit-contract/lifecycle-orchestration.mjs +415 -0
- package/scripts/runkit-contract/offline-store.mjs +248 -0
- package/scripts/runkit-contract/profile-impact.mjs +223 -0
- package/scripts/runkit-contract/provenance-common.mjs +153 -0
- package/scripts/runkit-contract/quick-attest.mjs +4 -0
- package/scripts/runkit-contract/quick-canonical.mjs +41 -0
- package/scripts/runkit-contract/quick-metrics.mjs +86 -0
- package/scripts/runkit-contract/quick-receipt.mjs +139 -0
- package/scripts/runkit-contract/quick-verify.mjs +201 -0
- package/scripts/runkit-contract/quick-workspace-snapshot.mjs +3 -0
- package/scripts/runkit-contract/ready-for-commit.mjs +188 -0
- package/scripts/runkit-contract/receipt-lineage.mjs +332 -0
- package/scripts/runkit-contract/registry-adoption-gate.mjs +117 -0
- package/scripts/runkit-contract/repair-execution.mjs +997 -0
- package/scripts/runkit-contract/resource-preflight.mjs +829 -0
- package/scripts/runkit-contract/resume-execution.mjs +569 -0
- package/scripts/runkit-contract/runkit-cli.mjs +1214 -0
- package/scripts/runkit-contract/schemas/closeout-receipt-v2.schema.json +42 -0
- package/scripts/runkit-contract/schemas/core-artifact-v2.schema.json +23 -0
- package/scripts/runkit-contract/schemas/core-artifact.schema.json +23 -0
- package/scripts/runkit-contract/schemas/coverage-adopt-request-v1.schema.json +66 -0
- package/scripts/runkit-contract/schemas/engine-pin-v2.schema.json +14 -0
- package/scripts/runkit-contract/schemas/engine-pin.schema.json +14 -0
- package/scripts/runkit-contract/schemas/evidence-coverage-index-v1.schema.json +131 -0
- package/scripts/runkit-contract/schemas/finalize-request-v1.schema.json +131 -0
- package/scripts/runkit-contract/schemas/profiles.schema.json +47 -0
- package/scripts/runkit-contract/schemas/project-config-v2.schema.json +13 -0
- package/scripts/runkit-contract/schemas/project-config.schema.json +13 -0
- package/scripts/runkit-contract/schemas/ready-for-commit-v1.schema.json +26 -0
- package/scripts/runkit-contract/schemas/replayable-evidence-v1.schema.json +82 -0
- package/scripts/runkit-contract/schemas/resource-preflight-request-v1.schema.json +178 -0
- package/scripts/runkit-contract/schemas/resource-preflight-v1.schema.json +153 -0
- package/scripts/runkit-contract/schemas/resume-attempt-v1.schema.json +80 -0
- package/scripts/runkit-contract/schemas/resume-request-v1.schema.json +77 -0
- package/scripts/runkit-contract/schemas/snapshot-v1.schema.json +71 -0
- package/scripts/runkit-contract/schemas/verification-context-v1.schema.json +92 -0
- package/scripts/runkit-contract/schemas/verification-plan-v1.schema.json +150 -0
- package/scripts/runkit-contract/schemas/verification-receipt-v2.schema.json +118 -0
- package/scripts/runkit-contract/schemas/verify-plan-request-v1.schema.json +56 -0
- package/scripts/runkit-contract/schemas/visual-smoke-request-v1.schema.json +64 -0
- package/scripts/runkit-contract/schemas/visual-smoke-result-v1.schema.json +68 -0
- package/scripts/runkit-contract/snapshot.mjs +340 -0
- package/scripts/runkit-contract/source-fingerprint.mjs +404 -0
- package/scripts/runkit-contract/templates/coverage-adopt-request.json +26 -0
- package/scripts/runkit-contract/templates/finalize-request.json +18 -0
- package/scripts/runkit-contract/templates/goal-contract.json +11 -0
- package/scripts/runkit-contract/templates/ready-for-commit-request.json +11 -0
- package/scripts/runkit-contract/templates/resource-preflight-request.json +56 -0
- package/scripts/runkit-contract/templates/resume-request.json +10 -0
- package/scripts/runkit-contract/templates/snapshot-request.json +23 -0
- package/scripts/runkit-contract/templates/verify-plan-request.json +31 -0
- package/scripts/runkit-contract/templates/visual-smoke-request.json +32 -0
- package/scripts/runkit-contract/templates/worker-lease.json +7 -0
- package/scripts/runkit-contract/verification-context.mjs +141 -0
- package/scripts/runkit-contract/verification-plan.mjs +608 -0
- package/scripts/runkit-contract/verification-receipt-gate.mjs +566 -0
- package/scripts/runkit-contract/visual-smoke.mjs +410 -0
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync } from "node:fs";
|
|
3
|
+
import path, { isAbsolute, win32 } from "node:path";
|
|
4
|
+
|
|
5
|
+
import { RUNTIME_ROOT } from "./core-contract.mjs";
|
|
6
|
+
import { validateCoverageIndexArtifact } from "./coverage-adoption.mjs";
|
|
7
|
+
import { resolveProfileImpactDetailed } from "./profile-impact.mjs";
|
|
8
|
+
import {
|
|
9
|
+
assertAllowedKeys,
|
|
10
|
+
loadActiveExecution,
|
|
11
|
+
readJson,
|
|
12
|
+
relativeToWorkspace,
|
|
13
|
+
resolveExistingArtifact,
|
|
14
|
+
safeIdentifier,
|
|
15
|
+
safeRelativePath,
|
|
16
|
+
sha256 as hashBytes,
|
|
17
|
+
writeJsonExclusive,
|
|
18
|
+
} from "./provenance-common.mjs";
|
|
19
|
+
import {
|
|
20
|
+
validateVerificationContext,
|
|
21
|
+
verificationContextFingerprint,
|
|
22
|
+
} from "./verification-context.mjs";
|
|
23
|
+
|
|
24
|
+
const SHA256_PATTERN = /^[a-f0-9]{64}$/;
|
|
25
|
+
const STATUS_MODE = "porcelain-v1-z-untracked-all-runkit-excluded";
|
|
26
|
+
const REQUEST_KEYS = [
|
|
27
|
+
"schemaVersion",
|
|
28
|
+
"planId",
|
|
29
|
+
"deliveryPacketPath",
|
|
30
|
+
"statusMode",
|
|
31
|
+
"dependencies",
|
|
32
|
+
"verificationContext",
|
|
33
|
+
"coverageIndexPath",
|
|
34
|
+
"coverageIndexSha256",
|
|
35
|
+
"globalGates",
|
|
36
|
+
];
|
|
37
|
+
const COVERAGE_INDEX_KEYS = [
|
|
38
|
+
"schemaVersion",
|
|
39
|
+
"coverageId",
|
|
40
|
+
"runId",
|
|
41
|
+
"generatedFrom",
|
|
42
|
+
"entries",
|
|
43
|
+
"authorizationGranted",
|
|
44
|
+
];
|
|
45
|
+
const COVERAGE_SOURCE_KEYS = [
|
|
46
|
+
"gateInputPath",
|
|
47
|
+
"gateInputSha256",
|
|
48
|
+
"commandBindings",
|
|
49
|
+
"dependencyBindings",
|
|
50
|
+
"activeReceiptSha256",
|
|
51
|
+
"sourceRunId",
|
|
52
|
+
];
|
|
53
|
+
const COVERAGE_ENTRY_KEYS = [
|
|
54
|
+
"receiptId",
|
|
55
|
+
"receiptSha256",
|
|
56
|
+
"status",
|
|
57
|
+
"sourceFiles",
|
|
58
|
+
"dependencySha256",
|
|
59
|
+
"verificationContextFingerprint",
|
|
60
|
+
"profileIds",
|
|
61
|
+
"commandIds",
|
|
62
|
+
];
|
|
63
|
+
const COMMAND_BINDING_KEYS = ["receiptCommandId", "profileId", "commandId"];
|
|
64
|
+
const DEPENDENCY_BINDING_KEYS = ["dependencyId", "source"];
|
|
65
|
+
const DEPENDENCY_SOURCE_KEYS = ["kind", "identity"];
|
|
66
|
+
|
|
67
|
+
function isRecord(value) {
|
|
68
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function sortedUniqueStrings(values, label) {
|
|
72
|
+
if (!Array.isArray(values) || values.some(value => typeof value !== "string" || value.length === 0)) {
|
|
73
|
+
throw new Error(`${label} must contain non-empty strings.`);
|
|
74
|
+
}
|
|
75
|
+
return [...new Set(values)].sort();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function safePath(value, label) {
|
|
79
|
+
if (typeof value !== "string"
|
|
80
|
+
|| value.length === 0
|
|
81
|
+
|| isAbsolute(value)
|
|
82
|
+
|| win32.isAbsolute(value)
|
|
83
|
+
|| value.includes("\\")
|
|
84
|
+
|| value.includes("\0")) {
|
|
85
|
+
throw new Error(`${label} must be a safe repository-relative path.`);
|
|
86
|
+
}
|
|
87
|
+
const segments = value.split("/");
|
|
88
|
+
if (segments.some(segment => segment.length === 0 || segment === "." || segment === "..")) {
|
|
89
|
+
throw new Error(`${label} must not contain empty, dot, or parent segments.`);
|
|
90
|
+
}
|
|
91
|
+
if (value === ".owlcoda/runkit" || value.startsWith(".owlcoda/runkit/")) {
|
|
92
|
+
throw new Error(`${label} uses the reserved runtime path .owlcoda/runkit.`);
|
|
93
|
+
}
|
|
94
|
+
return value;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function sha256(value, label) {
|
|
98
|
+
if (typeof value !== "string" || !SHA256_PATTERN.test(value)) {
|
|
99
|
+
throw new Error(`${label} must be a lowercase SHA-256.`);
|
|
100
|
+
}
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function normalizeFileMap(value, label) {
|
|
105
|
+
if (!isRecord(value)) throw new Error(`${label} must be an object.`);
|
|
106
|
+
const normalized = {};
|
|
107
|
+
for (const [filePath, hash] of Object.entries(value).sort(([left], [right]) => left.localeCompare(right))) {
|
|
108
|
+
normalized[safePath(filePath, `${label} path`)] = sha256(hash, `${label} hash`);
|
|
109
|
+
}
|
|
110
|
+
return normalized;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function normalizeDependencies(values) {
|
|
114
|
+
if (!Array.isArray(values)) throw new Error("dependencies must be an array.");
|
|
115
|
+
const seen = new Set();
|
|
116
|
+
return values.map(value => {
|
|
117
|
+
if (!isRecord(value) || typeof value.id !== "string" || value.id.length === 0) {
|
|
118
|
+
throw new Error("Each dependency requires an id.");
|
|
119
|
+
}
|
|
120
|
+
if (seen.has(value.id)) throw new Error(`Dependency ids must be unique: ${value.id}`);
|
|
121
|
+
seen.add(value.id);
|
|
122
|
+
return {
|
|
123
|
+
id: value.id,
|
|
124
|
+
path: safePath(value.path, "Dependency path"),
|
|
125
|
+
baselineSha256: sha256(value.baselineSha256, "Dependency baselineSha256"),
|
|
126
|
+
currentSha256: value.currentSha256 === null
|
|
127
|
+
? null
|
|
128
|
+
: sha256(value.currentSha256, "Dependency currentSha256"),
|
|
129
|
+
};
|
|
130
|
+
}).sort((left, right) => left.id.localeCompare(right.id));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function normalizeGlobalGates(values) {
|
|
134
|
+
if (!Array.isArray(values)) throw new Error("globalGates must be an array.");
|
|
135
|
+
const seen = new Set();
|
|
136
|
+
return values.map(value => {
|
|
137
|
+
if (!isRecord(value) || typeof value.id !== "string" || value.id.length === 0) {
|
|
138
|
+
throw new Error("Each global gate requires an id.");
|
|
139
|
+
}
|
|
140
|
+
if (seen.has(value.id)) throw new Error(`Global gate ids must be unique: ${value.id}`);
|
|
141
|
+
seen.add(value.id);
|
|
142
|
+
if (!new Set(["passed", "failed"]).has(value.status)) {
|
|
143
|
+
throw new Error("Global gate status must be passed or failed.");
|
|
144
|
+
}
|
|
145
|
+
if (value.reason !== undefined && (typeof value.reason !== "string" || value.reason.length === 0)) {
|
|
146
|
+
throw new Error("Global gate reason must be a non-empty string when provided.");
|
|
147
|
+
}
|
|
148
|
+
return { id: value.id, status: value.status, reason: value.reason ?? null };
|
|
149
|
+
}).sort((left, right) => left.id.localeCompare(right.id));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function normalizeCoverageIndex(value) {
|
|
153
|
+
if (!isRecord(value)
|
|
154
|
+
|| value.schemaVersion !== "OwlCodaRunKitEvidenceCoverageIndexV1"
|
|
155
|
+
|| typeof value.coverageId !== "string"
|
|
156
|
+
|| value.coverageId.length === 0
|
|
157
|
+
|| typeof value.runId !== "string"
|
|
158
|
+
|| value.runId.length === 0
|
|
159
|
+
|| !Array.isArray(value.generatedFrom)
|
|
160
|
+
|| !Array.isArray(value.entries)
|
|
161
|
+
|| value.authorizationGranted !== false) {
|
|
162
|
+
throw new Error("coverageIndex must be OwlCodaRunKitEvidenceCoverageIndexV1.");
|
|
163
|
+
}
|
|
164
|
+
assertAllowedKeys(value, "Coverage index", COVERAGE_INDEX_KEYS);
|
|
165
|
+
const generatedReceiptHashes = new Set();
|
|
166
|
+
for (const source of value.generatedFrom) {
|
|
167
|
+
if (!isRecord(source)
|
|
168
|
+
|| typeof source.gateInputPath !== "string"
|
|
169
|
+
|| source.gateInputPath.length === 0
|
|
170
|
+
|| typeof source.sourceRunId !== "string"
|
|
171
|
+
|| source.sourceRunId.length === 0) {
|
|
172
|
+
throw new Error("Coverage generatedFrom entries are invalid.");
|
|
173
|
+
}
|
|
174
|
+
assertAllowedKeys(source, "Coverage generatedFrom entry", COVERAGE_SOURCE_KEYS);
|
|
175
|
+
const gateInputPath = safeRelativePath(source.gateInputPath, "Coverage gateInputPath");
|
|
176
|
+
if (!gateInputPath.startsWith(`${RUNTIME_ROOT}/`)) {
|
|
177
|
+
throw new Error("Coverage gateInputPath must reference a project RunKit artifact.");
|
|
178
|
+
}
|
|
179
|
+
sha256(source.gateInputSha256, "Coverage gateInputSha256");
|
|
180
|
+
sha256(source.activeReceiptSha256, "Coverage activeReceiptSha256");
|
|
181
|
+
if (!Array.isArray(source.commandBindings) || source.commandBindings.length === 0) {
|
|
182
|
+
throw new Error("Coverage generatedFrom entry requires commandBindings.");
|
|
183
|
+
}
|
|
184
|
+
for (const binding of source.commandBindings) {
|
|
185
|
+
assertAllowedKeys(binding, "Coverage command binding", COMMAND_BINDING_KEYS);
|
|
186
|
+
safeIdentifier(binding.receiptCommandId, "Coverage receiptCommandId");
|
|
187
|
+
safeIdentifier(binding.profileId, "Coverage profileId");
|
|
188
|
+
safeIdentifier(binding.commandId, "Coverage commandId");
|
|
189
|
+
}
|
|
190
|
+
if (!Array.isArray(source.dependencyBindings)) {
|
|
191
|
+
throw new Error("Coverage generatedFrom entry requires dependencyBindings.");
|
|
192
|
+
}
|
|
193
|
+
for (const binding of source.dependencyBindings) {
|
|
194
|
+
assertAllowedKeys(binding, "Coverage dependency binding", DEPENDENCY_BINDING_KEYS);
|
|
195
|
+
safeIdentifier(binding.dependencyId, "Coverage dependencyId");
|
|
196
|
+
if (!isRecord(binding.source)) throw new Error("Coverage dependency source must be an object.");
|
|
197
|
+
assertAllowedKeys(binding.source, "Coverage dependency source", DEPENDENCY_SOURCE_KEYS);
|
|
198
|
+
if (!new Set(["lockfile", "fixture", "material_input"]).has(binding.source.kind)
|
|
199
|
+
|| typeof binding.source.identity !== "string"
|
|
200
|
+
|| binding.source.identity.length === 0) {
|
|
201
|
+
throw new Error("Coverage dependency source is invalid.");
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (generatedReceiptHashes.has(source.activeReceiptSha256)) {
|
|
205
|
+
throw new Error(`Coverage generated receipt hashes must be unique: ${source.activeReceiptSha256}`);
|
|
206
|
+
}
|
|
207
|
+
generatedReceiptHashes.add(source.activeReceiptSha256);
|
|
208
|
+
}
|
|
209
|
+
const seen = new Set();
|
|
210
|
+
const entries = value.entries.map(entry => {
|
|
211
|
+
if (!isRecord(entry) || typeof entry.receiptId !== "string" || entry.receiptId.length === 0) {
|
|
212
|
+
throw new Error("Each coverage entry requires receiptId.");
|
|
213
|
+
}
|
|
214
|
+
assertAllowedKeys(entry, "Coverage entry", COVERAGE_ENTRY_KEYS);
|
|
215
|
+
if (seen.has(entry.receiptId)) throw new Error(`Coverage receipt ids must be unique: ${entry.receiptId}`);
|
|
216
|
+
seen.add(entry.receiptId);
|
|
217
|
+
if (!new Set(["passed", "ready_for_verification", "shadow_validated", "invalidated_by_concurrent_write"]).has(entry.status)) {
|
|
218
|
+
throw new Error("Coverage entry status is invalid.");
|
|
219
|
+
}
|
|
220
|
+
if (!isRecord(entry.dependencySha256)) throw new Error("dependencySha256 must be an object.");
|
|
221
|
+
const dependencySha256 = {};
|
|
222
|
+
for (const [id, hash] of Object.entries(entry.dependencySha256).sort(([left], [right]) => left.localeCompare(right))) {
|
|
223
|
+
if (!id) throw new Error("Coverage dependency id must be non-empty.");
|
|
224
|
+
dependencySha256[id] = sha256(hash, "Coverage dependency hash");
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
receiptId: entry.receiptId,
|
|
228
|
+
receiptSha256: sha256(entry.receiptSha256, "Coverage receiptSha256"),
|
|
229
|
+
status: entry.status,
|
|
230
|
+
sourceFiles: normalizeFileMap(entry.sourceFiles, "Coverage sourceFiles"),
|
|
231
|
+
dependencySha256,
|
|
232
|
+
verificationContextFingerprint: sha256(
|
|
233
|
+
entry.verificationContextFingerprint,
|
|
234
|
+
"Coverage verificationContextFingerprint",
|
|
235
|
+
),
|
|
236
|
+
profileIds: sortedUniqueStrings(entry.profileIds, "Coverage profileIds"),
|
|
237
|
+
commandIds: sortedUniqueStrings(entry.commandIds, "Coverage commandIds"),
|
|
238
|
+
};
|
|
239
|
+
}).sort((left, right) => left.receiptId.localeCompare(right.receiptId));
|
|
240
|
+
const entryReceiptHashes = new Set(entries.map(entry => entry.receiptSha256));
|
|
241
|
+
if (generatedReceiptHashes.size !== entryReceiptHashes.size
|
|
242
|
+
|| [...generatedReceiptHashes].some(hash => !entryReceiptHashes.has(hash))) {
|
|
243
|
+
throw new Error("Coverage generatedFrom hashes must match entry receipt hashes.");
|
|
244
|
+
}
|
|
245
|
+
return entries;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function normalizeCommands(profiles) {
|
|
249
|
+
const commands = new Map();
|
|
250
|
+
for (const profile of profiles) {
|
|
251
|
+
if (profile.commands === undefined) continue;
|
|
252
|
+
if (!Array.isArray(profile.commands)) throw new Error("Profile commands must be an array.");
|
|
253
|
+
for (const command of profile.commands) {
|
|
254
|
+
if (!isRecord(command) || typeof command.id !== "string" || command.id.length === 0) {
|
|
255
|
+
throw new Error("Each profile command requires an id.");
|
|
256
|
+
}
|
|
257
|
+
const normalized = {
|
|
258
|
+
id: command.id,
|
|
259
|
+
cwd: command.cwd,
|
|
260
|
+
executable: command.executable,
|
|
261
|
+
argv: command.argv,
|
|
262
|
+
};
|
|
263
|
+
if (command.cwd !== ".") safePath(command.cwd, "Profile command cwd");
|
|
264
|
+
if (typeof command.executable !== "string" || command.executable.length === 0) {
|
|
265
|
+
throw new Error("Profile command executable is required.");
|
|
266
|
+
}
|
|
267
|
+
sortedUniqueStrings(command.argv, "Profile command argv");
|
|
268
|
+
const previous = commands.get(command.id);
|
|
269
|
+
if (previous && JSON.stringify(previous.definition) !== JSON.stringify(normalized)) {
|
|
270
|
+
throw new Error(`Profile command id has conflicting definitions: ${command.id}`);
|
|
271
|
+
}
|
|
272
|
+
if (previous) previous.profileIds.add(profile.id);
|
|
273
|
+
else commands.set(command.id, { definition: normalized, profileIds: new Set([profile.id]) });
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return commands;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function matchesOwnedPath(filePath, ownedPath) {
|
|
280
|
+
if (ownedPath.endsWith("/**")) return filePath.startsWith(`${ownedPath.slice(0, -3)}/`);
|
|
281
|
+
return filePath === ownedPath;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function buildVerificationPlan(input) {
|
|
285
|
+
if (!isRecord(input) || input.schemaVersion !== "OwlCodaRunKitVerifyPlanRequestV1") {
|
|
286
|
+
throw new Error("Unsupported verify-plan request schemaVersion.");
|
|
287
|
+
}
|
|
288
|
+
if (typeof input.runId !== "string" || input.runId.length === 0) throw new Error("runId is required.");
|
|
289
|
+
if (typeof input.planId !== "string" || input.planId.length === 0) throw new Error("planId is required.");
|
|
290
|
+
|
|
291
|
+
const ownedPaths = sortedUniqueStrings(input.ownedPaths, "ownedPaths");
|
|
292
|
+
for (const ownedPath of ownedPaths) {
|
|
293
|
+
const prefix = ownedPath.endsWith("/**") ? ownedPath.slice(0, -3) : ownedPath;
|
|
294
|
+
safePath(prefix, "ownedPaths entry");
|
|
295
|
+
}
|
|
296
|
+
const changedPaths = sortedUniqueStrings(input.changedPaths, "changedPaths");
|
|
297
|
+
for (const changedPath of changedPaths) safePath(changedPath, "changedPaths entry");
|
|
298
|
+
const currentSourceFiles = normalizeFileMap(input.currentSourceFiles, "currentSourceFiles");
|
|
299
|
+
const dependencies = normalizeDependencies(input.dependencies);
|
|
300
|
+
const dependencyByPath = new Map(dependencies.map(dependency => [dependency.path, dependency]));
|
|
301
|
+
const dependencyById = new Map(dependencies.map(dependency => [dependency.id, dependency]));
|
|
302
|
+
const contextFingerprint = sha256(
|
|
303
|
+
input.verificationContextFingerprint,
|
|
304
|
+
"verificationContextFingerprint",
|
|
305
|
+
);
|
|
306
|
+
if (!Array.isArray(input.profiles)) throw new Error("profiles must be an array.");
|
|
307
|
+
const commandById = normalizeCommands(input.profiles);
|
|
308
|
+
const coverage = normalizeCoverageIndex(input.coverageIndex);
|
|
309
|
+
if (input.coverageIndex.runId !== input.runId) {
|
|
310
|
+
throw new Error("Coverage index runId does not match the verification plan run.");
|
|
311
|
+
}
|
|
312
|
+
const profileIds = new Set(input.profiles.map(profile => profile.id));
|
|
313
|
+
for (const entry of coverage) {
|
|
314
|
+
for (const profileId of entry.profileIds) {
|
|
315
|
+
if (!profileIds.has(profileId)) throw new Error(`Coverage profile does not exist: ${profileId}`);
|
|
316
|
+
}
|
|
317
|
+
for (const commandId of entry.commandIds) {
|
|
318
|
+
const command = commandById.get(commandId);
|
|
319
|
+
if (!command) throw new Error(`Coverage command does not exist: ${commandId}`);
|
|
320
|
+
if (!entry.profileIds.some(profileId => command.profileIds.has(profileId))) {
|
|
321
|
+
throw new Error(`Coverage command ${commandId} is not owned by its declared profiles.`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
const globalGates = normalizeGlobalGates(input.globalGates);
|
|
326
|
+
|
|
327
|
+
const leasedSourceDrift = [];
|
|
328
|
+
const unrelatedDirtyTreeDrift = [];
|
|
329
|
+
for (const changedPath of changedPaths) {
|
|
330
|
+
if (dependencyByPath.has(changedPath)) continue;
|
|
331
|
+
if (ownedPaths.some(ownedPath => matchesOwnedPath(changedPath, ownedPath))) leasedSourceDrift.push(changedPath);
|
|
332
|
+
else unrelatedDirtyTreeDrift.push(changedPath);
|
|
333
|
+
}
|
|
334
|
+
const declaredDependencyDrift = dependencies
|
|
335
|
+
.filter(dependency => dependency.baselineSha256 !== dependency.currentSha256)
|
|
336
|
+
.map(dependency => dependency.id);
|
|
337
|
+
const globalGateFailures = globalGates
|
|
338
|
+
.filter(gate => gate.status === "failed")
|
|
339
|
+
.map(gate => gate.id);
|
|
340
|
+
|
|
341
|
+
const affectedPaths = [
|
|
342
|
+
...leasedSourceDrift,
|
|
343
|
+
...dependencies
|
|
344
|
+
.filter(dependency => declaredDependencyDrift.includes(dependency.id))
|
|
345
|
+
.map(dependency => dependency.path),
|
|
346
|
+
];
|
|
347
|
+
const profileImpact = resolveProfileImpactDetailed({
|
|
348
|
+
changedPaths: affectedPaths,
|
|
349
|
+
profiles: input.profiles,
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
const reusableReceiptIds = [];
|
|
353
|
+
const invalidatedReceipts = [];
|
|
354
|
+
const reusableCommandIds = new Set();
|
|
355
|
+
for (const entry of coverage) {
|
|
356
|
+
const reasons = [];
|
|
357
|
+
if (entry.status !== "passed") reasons.push(`receipt_status:${entry.status}`);
|
|
358
|
+
for (const [filePath, expectedHash] of Object.entries(entry.sourceFiles)) {
|
|
359
|
+
if (currentSourceFiles[filePath] !== expectedHash) reasons.push(`leased_source_drift:${filePath}`);
|
|
360
|
+
}
|
|
361
|
+
for (const [dependencyId, expectedHash] of Object.entries(entry.dependencySha256)) {
|
|
362
|
+
const current = dependencyById.get(dependencyId)?.currentSha256;
|
|
363
|
+
if (current !== expectedHash) reasons.push(`declared_dependency_drift:${dependencyId}`);
|
|
364
|
+
}
|
|
365
|
+
if (entry.verificationContextFingerprint !== contextFingerprint) reasons.push("verification_context_drift");
|
|
366
|
+
const normalizedReasons = [...new Set(reasons)].sort();
|
|
367
|
+
if (normalizedReasons.length > 0) {
|
|
368
|
+
invalidatedReceipts.push({ receiptId: entry.receiptId, reasons: normalizedReasons });
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
reusableReceiptIds.push(entry.receiptId);
|
|
372
|
+
for (const commandId of entry.commandIds) reusableCommandIds.add(commandId);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const requiredCommandIds = [];
|
|
376
|
+
const unmappedProfileIds = [];
|
|
377
|
+
for (const profileId of profileImpact.selectedProfileIds) {
|
|
378
|
+
const profile = input.profiles.find(candidate => candidate.id === profileId);
|
|
379
|
+
if (!Array.isArray(profile?.commands) || profile.commands.length === 0) {
|
|
380
|
+
unmappedProfileIds.push(profileId);
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
for (const command of profile?.commands ?? []) requiredCommandIds.push(command.id);
|
|
384
|
+
}
|
|
385
|
+
const required = [...new Set(requiredCommandIds)].sort();
|
|
386
|
+
for (const commandId of required) {
|
|
387
|
+
if (!commandById.has(commandId)) throw new Error(`Selected profile command is not defined: ${commandId}`);
|
|
388
|
+
}
|
|
389
|
+
const reused = required.filter(commandId => reusableCommandIds.has(commandId));
|
|
390
|
+
const pending = required.filter(commandId => !reusableCommandIds.has(commandId));
|
|
391
|
+
const pendingCommands = pending.map(commandId => {
|
|
392
|
+
const command = commandById.get(commandId);
|
|
393
|
+
return {
|
|
394
|
+
...command.definition,
|
|
395
|
+
argv: [...command.definition.argv],
|
|
396
|
+
profileIds: [...command.profileIds].sort(),
|
|
397
|
+
};
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
const acceptanceReasons = [
|
|
401
|
+
...globalGateFailures.map(gateId => `global_gate_failure:${gateId}`),
|
|
402
|
+
...(profileImpact.decision === "full_profile_required" ? ["full_profile_required"] : []),
|
|
403
|
+
...unmappedProfileIds.map(profileId => `verification_command_mapping_missing:${profileId}`),
|
|
404
|
+
].sort();
|
|
405
|
+
const status = profileImpact.decision === "full_profile_required"
|
|
406
|
+
? "full_profile_required"
|
|
407
|
+
: globalGateFailures.length > 0
|
|
408
|
+
? "blocked_by_global_gate"
|
|
409
|
+
: unmappedProfileIds.length > 0
|
|
410
|
+
? "verification_mapping_required"
|
|
411
|
+
: pending.length > 0
|
|
412
|
+
? "verification_required"
|
|
413
|
+
: "ready_to_finalize";
|
|
414
|
+
|
|
415
|
+
return {
|
|
416
|
+
schemaVersion: "OwlCodaRunKitVerificationPlanV1",
|
|
417
|
+
runId: input.runId,
|
|
418
|
+
planId: input.planId,
|
|
419
|
+
verificationContextFingerprint: contextFingerprint,
|
|
420
|
+
status,
|
|
421
|
+
drift: {
|
|
422
|
+
leasedSourceDrift,
|
|
423
|
+
declaredDependencyDrift,
|
|
424
|
+
unrelatedDirtyTreeDrift,
|
|
425
|
+
globalGateFailures,
|
|
426
|
+
},
|
|
427
|
+
profileImpact,
|
|
428
|
+
evidence: {
|
|
429
|
+
...(input.coverageIndexPath === undefined ? {} : {
|
|
430
|
+
coverageIndexPath: safeRelativePath(input.coverageIndexPath, "coverageIndexPath"),
|
|
431
|
+
coverageIndexSha256: sha256(input.coverageIndexSha256, "coverageIndexSha256"),
|
|
432
|
+
}),
|
|
433
|
+
reusableReceiptIds,
|
|
434
|
+
invalidatedReceipts,
|
|
435
|
+
},
|
|
436
|
+
commands: {
|
|
437
|
+
requiredCommandIds: required,
|
|
438
|
+
reusedCommandIds: reused,
|
|
439
|
+
pendingCommandIds: pending,
|
|
440
|
+
unmappedProfileIds,
|
|
441
|
+
pendingCommands,
|
|
442
|
+
},
|
|
443
|
+
acceptance: {
|
|
444
|
+
blocked: acceptanceReasons.length > 0,
|
|
445
|
+
reasons: acceptanceReasons,
|
|
446
|
+
},
|
|
447
|
+
authorizationGranted: false,
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function gitStatusPaths(workspaceRoot) {
|
|
452
|
+
const completed = spawnSync("git", [
|
|
453
|
+
"-C",
|
|
454
|
+
workspaceRoot,
|
|
455
|
+
"status",
|
|
456
|
+
"--porcelain=v1",
|
|
457
|
+
"-z",
|
|
458
|
+
"--untracked-files=all",
|
|
459
|
+
"--",
|
|
460
|
+
".",
|
|
461
|
+
":(exclude).owlcoda/runkit/**",
|
|
462
|
+
], { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
|
|
463
|
+
if (completed.error) throw completed.error;
|
|
464
|
+
if (completed.status !== 0) throw new Error(`Git status failed: ${completed.stderr.trim()}`);
|
|
465
|
+
const entries = completed.stdout.split("\0");
|
|
466
|
+
const paths = [];
|
|
467
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
468
|
+
const entry = entries[index];
|
|
469
|
+
if (!entry) continue;
|
|
470
|
+
if (entry.length < 4 || entry[2] !== " ") throw new Error("Git status emitted a malformed porcelain entry.");
|
|
471
|
+
const status = entry.slice(0, 2);
|
|
472
|
+
paths.push(safePath(entry.slice(3), "Git status path"));
|
|
473
|
+
if (status.includes("R") || status.includes("C")) {
|
|
474
|
+
const priorPath = entries[index + 1];
|
|
475
|
+
if (!priorPath) throw new Error("Git status rename entry is incomplete.");
|
|
476
|
+
paths.push(safePath(priorPath, "Git status prior path"));
|
|
477
|
+
index += 1;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return [...new Set(paths)].sort();
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function deliveryFiles(packet) {
|
|
484
|
+
const files = packet?.changedFiles?.files ?? packet?.changedFiles?.wholeFileSha256;
|
|
485
|
+
if (!isRecord(files) || Object.keys(files).length === 0) {
|
|
486
|
+
throw new Error("Delivery packet must declare changed files.");
|
|
487
|
+
}
|
|
488
|
+
return normalizeFileMap(files, "Delivery packet changed files");
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function workspaceFileSha256(workspaceRoot, filePath, label) {
|
|
492
|
+
const safe = safeRelativePath(filePath, label);
|
|
493
|
+
const absolutePath = path.resolve(workspaceRoot, safe);
|
|
494
|
+
if (!existsSync(absolutePath)) return null;
|
|
495
|
+
const stat = lstatSync(absolutePath);
|
|
496
|
+
if (stat.isSymbolicLink() || !stat.isFile()) throw new Error(`${label} must be a regular non-symlink file.`);
|
|
497
|
+
return hashBytes(readFileSync(realpathSync(absolutePath)));
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function loadOwnedPaths(executionRoot) {
|
|
501
|
+
const leasesRoot = path.join(executionRoot, "leases");
|
|
502
|
+
if (!existsSync(leasesRoot)) return [];
|
|
503
|
+
const ownedPaths = [];
|
|
504
|
+
for (const entry of readdirSync(leasesRoot, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
|
|
505
|
+
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
506
|
+
const lease = readJson(path.join(leasesRoot, entry.name));
|
|
507
|
+
if (!Array.isArray(lease.ownedPaths)) throw new Error(`Lease ownedPaths are invalid: ${entry.name}`);
|
|
508
|
+
for (const ownedPath of lease.ownedPaths) ownedPaths.push(ownedPath);
|
|
509
|
+
}
|
|
510
|
+
return sortedUniqueStrings(ownedPaths, "Lease ownedPaths");
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function validateRunRequest(request) {
|
|
514
|
+
assertAllowedKeys(request, "Verify-plan request", REQUEST_KEYS);
|
|
515
|
+
if (request.schemaVersion !== "OwlCodaRunKitVerifyPlanRequestV1") {
|
|
516
|
+
throw new Error("Unsupported verify-plan request schemaVersion.");
|
|
517
|
+
}
|
|
518
|
+
safeIdentifier(request.planId, "planId");
|
|
519
|
+
if (typeof request.deliveryPacketPath !== "string" || request.deliveryPacketPath.length === 0) {
|
|
520
|
+
throw new Error("Verify-plan request requires deliveryPacketPath.");
|
|
521
|
+
}
|
|
522
|
+
if (request.statusMode !== STATUS_MODE) {
|
|
523
|
+
throw new Error(`Verify-plan statusMode must be ${STATUS_MODE}.`);
|
|
524
|
+
}
|
|
525
|
+
const context = validateVerificationContext(request.verificationContext);
|
|
526
|
+
if (!context.valid) throw new Error(`Verify-plan verification context is invalid: ${context.issues.join("; ")}`);
|
|
527
|
+
if (typeof request.coverageIndexPath !== "string" || request.coverageIndexPath.length === 0) {
|
|
528
|
+
throw new Error("Verify-plan request requires coverageIndexPath.");
|
|
529
|
+
}
|
|
530
|
+
sha256(request.coverageIndexSha256, "coverageIndexSha256");
|
|
531
|
+
normalizeGlobalGates(request.globalGates);
|
|
532
|
+
if (!Array.isArray(request.dependencies)) throw new Error("Verify-plan dependencies must be an array.");
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
export function runVerifyPlan({ workspaceRoot, runId, request }) {
|
|
536
|
+
validateRunRequest(request);
|
|
537
|
+
const { executionRoot, pinGate } = loadActiveExecution(workspaceRoot, runId);
|
|
538
|
+
if (pinGate.status !== "valid") return pinGate;
|
|
539
|
+
const packetPath = resolveExistingArtifact(workspaceRoot, request.deliveryPacketPath, "deliveryPacketPath");
|
|
540
|
+
const packet = readJson(packetPath);
|
|
541
|
+
if (packet.runId !== runId) throw new Error("Delivery packet runId does not match the execution.");
|
|
542
|
+
const packetFiles = deliveryFiles(packet);
|
|
543
|
+
const coveragePath = resolveExistingArtifact(
|
|
544
|
+
workspaceRoot,
|
|
545
|
+
request.coverageIndexPath,
|
|
546
|
+
"coverageIndexPath",
|
|
547
|
+
);
|
|
548
|
+
const coverageRoot = path.join(executionRoot, "coverage-indexes");
|
|
549
|
+
const coverageRemainder = path.relative(coverageRoot, coveragePath);
|
|
550
|
+
if (coverageRemainder === ""
|
|
551
|
+
|| coverageRemainder === ".."
|
|
552
|
+
|| coverageRemainder.startsWith(`..${path.sep}`)
|
|
553
|
+
|| path.isAbsolute(coverageRemainder)) {
|
|
554
|
+
throw new Error("Coverage index must be inside the active execution coverage-indexes directory.");
|
|
555
|
+
}
|
|
556
|
+
const normalizedCoveragePath = relativeToWorkspace(workspaceRoot, coveragePath);
|
|
557
|
+
const coverageBytes = readFileSync(coveragePath);
|
|
558
|
+
const actualCoverageSha256 = hashBytes(coverageBytes);
|
|
559
|
+
if (actualCoverageSha256 !== request.coverageIndexSha256) {
|
|
560
|
+
throw new Error("Coverage index hash mismatch.");
|
|
561
|
+
}
|
|
562
|
+
const coverageIndex = JSON.parse(coverageBytes.toString("utf8"));
|
|
563
|
+
const coverage = normalizeCoverageIndex(coverageIndex);
|
|
564
|
+
if (coverageIndex.runId !== runId) throw new Error("Coverage index runId does not match the execution.");
|
|
565
|
+
validateCoverageIndexArtifact({ workspaceRoot, runId, coverageIndex });
|
|
566
|
+
const sourcePaths = new Set(Object.keys(packetFiles));
|
|
567
|
+
for (const entry of coverage) {
|
|
568
|
+
for (const filePath of Object.keys(entry.sourceFiles)) sourcePaths.add(filePath);
|
|
569
|
+
}
|
|
570
|
+
const currentSourceFiles = {};
|
|
571
|
+
for (const filePath of [...sourcePaths].sort()) {
|
|
572
|
+
const current = workspaceFileSha256(workspaceRoot, filePath, "Verification source path");
|
|
573
|
+
if (current !== null) currentSourceFiles[filePath] = current;
|
|
574
|
+
}
|
|
575
|
+
const dependencies = request.dependencies.map(dependency => ({
|
|
576
|
+
...dependency,
|
|
577
|
+
currentSha256: workspaceFileSha256(workspaceRoot, dependency.path, "Dependency path"),
|
|
578
|
+
}));
|
|
579
|
+
const profiles = readJson(path.join(workspaceRoot, RUNTIME_ROOT, "profiles.json"));
|
|
580
|
+
if (profiles.schemaVersion !== "OwlCodaRunKitProfilesV1" || !Array.isArray(profiles.profiles)) {
|
|
581
|
+
throw new Error("Project profiles artifact is invalid.");
|
|
582
|
+
}
|
|
583
|
+
const contextFingerprint = verificationContextFingerprint(request.verificationContext);
|
|
584
|
+
const plan = buildVerificationPlan({
|
|
585
|
+
schemaVersion: request.schemaVersion,
|
|
586
|
+
runId,
|
|
587
|
+
planId: request.planId,
|
|
588
|
+
ownedPaths: loadOwnedPaths(executionRoot),
|
|
589
|
+
changedPaths: gitStatusPaths(workspaceRoot),
|
|
590
|
+
currentSourceFiles,
|
|
591
|
+
dependencies,
|
|
592
|
+
verificationContextFingerprint: contextFingerprint,
|
|
593
|
+
profiles: profiles.profiles,
|
|
594
|
+
coverageIndex: coverageIndex,
|
|
595
|
+
coverageIndexPath: normalizedCoveragePath,
|
|
596
|
+
coverageIndexSha256: actualCoverageSha256,
|
|
597
|
+
globalGates: request.globalGates,
|
|
598
|
+
});
|
|
599
|
+
const planPath = path.join(executionRoot, "verification-plans", `${request.planId}.json`);
|
|
600
|
+
writeJsonExclusive(planPath, plan);
|
|
601
|
+
return {
|
|
602
|
+
status: "verification_plan_written",
|
|
603
|
+
exitCode: 0,
|
|
604
|
+
runId,
|
|
605
|
+
planPath: relativeToWorkspace(workspaceRoot, planPath),
|
|
606
|
+
authorizationGranted: false,
|
|
607
|
+
};
|
|
608
|
+
}
|