owlrunkit 0.18.3 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/README.md +223 -52
  3. package/docs/architecture/OWLCODA_RUNKIT_0184_EXTERNAL_DELIVERY_INTAKE_PRODUCT_DECISION_20260812.md +209 -0
  4. package/docs/architecture/OWLCODA_RUNKIT_0190_CONSOLIDATED_DAILY_USE_PRODUCT_DECISION_20260812.md +97 -0
  5. package/docs/architecture/OWLCODA_RUNKIT_USAGE_MODE_SELECTION_PRODUCT_DECISION_20260811.md +8 -7
  6. package/docs/architecture/OWLCODA_RUN_KIT_CONTRACT_V0_2.md +57 -0
  7. package/integrations/codex/skills/owlcoda-runkit/SKILL.md +133 -77
  8. package/integrations/codex/skills/owlcoda-runkit/assets/templates/config.json +3 -3
  9. package/integrations/codex/skills/owlcoda-runkit/references/contract-v0.2.md +57 -0
  10. package/package.json +1 -1
  11. package/packages/attest/README.md +9 -5
  12. package/packages/attest/consumer-surface-v2.json +263 -0
  13. package/packages/attest/distribution-manifest-v1.json +14 -1
  14. package/packages/attest/schemas/quick-verification-receipt-v3.schema.json +244 -0
  15. package/packages/attest/schemas/workspace-snapshot-v2.schema.json +114 -0
  16. package/packages/attest/src/dependency-environment.mjs +128 -0
  17. package/packages/attest/src/index.mjs +1 -0
  18. package/packages/attest/src/quick-receipt-contract.mjs +205 -12
  19. package/packages/attest/src/quick.mjs +45 -4
  20. package/packages/attest/src/workspace-snapshot.mjs +143 -7
  21. package/registry-release-policy-v1.json +5 -5
  22. package/sbom.spdx.json +5 -5
  23. package/scripts/runkit-contract/assurance-router.mjs +25 -6
  24. package/scripts/runkit-contract/core-contract.mjs +29 -1
  25. package/scripts/runkit-contract/efficiency-metrics.mjs +559 -0
  26. package/scripts/runkit-contract/evidence-store.mjs +130 -0
  27. package/scripts/runkit-contract/onboarding-bootstrap.mjs +685 -0
  28. package/scripts/runkit-contract/onboarding-doctor.mjs +3 -2
  29. package/scripts/runkit-contract/outcome-presentation.mjs +171 -0
  30. package/scripts/runkit-contract/profile-onboarding.mjs +285 -17
  31. package/scripts/runkit-contract/project-cli-resolver.mjs +29 -5
  32. package/scripts/runkit-contract/project-external-delivery.mjs +707 -0
  33. package/scripts/runkit-contract/quick-evidence-reuse.mjs +590 -0
  34. package/scripts/runkit-contract/quick-isolation.mjs +314 -0
  35. package/scripts/runkit-contract/quick-metrics.mjs +51 -3
  36. package/scripts/runkit-contract/quick-receipt.mjs +31 -1
  37. package/scripts/runkit-contract/quick-verify.mjs +217 -26
  38. package/scripts/runkit-contract/quick-workspace-snapshot.mjs +1 -0
  39. package/scripts/runkit-contract/runkit-bootstrap.mjs +7 -1
  40. package/scripts/runkit-contract/runkit-cli.mjs +602 -152
  41. package/scripts/runkit-contract/runtime-transition.mjs +478 -0
  42. package/scripts/runkit-contract/schemas/assurance-route-v2.schema.json +26 -0
  43. package/scripts/runkit-contract/schemas/efficiency-event-v1.schema.json +56 -0
  44. package/scripts/runkit-contract/schemas/efficiency-metrics-v1.schema.json +220 -0
  45. package/scripts/runkit-contract/schemas/external-runtime-report-v1.schema.json +125 -0
  46. package/scripts/runkit-contract/schemas/onboarding-bootstrap-plan-v1.schema.json +168 -0
  47. package/scripts/runkit-contract/schemas/onboarding-bootstrap-receipt-v1.schema.json +130 -0
  48. package/scripts/runkit-contract/schemas/outcome-summary-v1.schema.json +83 -0
  49. package/scripts/runkit-contract/schemas/profile-detection-v3.schema.json +170 -0
  50. package/scripts/runkit-contract/schemas/quick-evidence-reuse-plan-v1.schema.json +59 -0
  51. package/scripts/runkit-contract/schemas/quick-evidence-reuse-receipt-v1.schema.json +79 -0
  52. package/scripts/runkit-contract/schemas/runtime-transition-receipt-v1.schema.json +185 -0
  53. package/scripts/runkit-contract/schemas/team-project-event-v3.schema.json +146 -0
  54. package/scripts/runkit-contract/schemas/team-project-event-v4.schema.json +142 -0
  55. package/scripts/runkit-contract/schemas/team-project-status-v2.schema.json +85 -0
  56. package/scripts/runkit-contract/schemas/usage-mode-recommendation-v2.schema.json +136 -0
  57. package/scripts/runkit-contract/team-project.mjs +1172 -22
  58. package/scripts/runkit-contract/usage-mode.mjs +28 -7
  59. package/scripts/runkit-contract/verification-envelope.mjs +15 -18
@@ -0,0 +1,128 @@
1
+ import {
2
+ existsSync,
3
+ lstatSync,
4
+ readFileSync,
5
+ readdirSync,
6
+ realpathSync,
7
+ } from "node:fs";
8
+ import path from "node:path";
9
+
10
+ import { sha256Bytes, sha256Canonical } from "./formal.mjs";
11
+
12
+ const MAX_DEPENDENCY_MATERIAL_BYTES = 128 * 1024 * 1024;
13
+ const DEPENDENCY_MATERIALS = Object.freeze([
14
+ "package.json",
15
+ "package-lock.json",
16
+ "npm-shrinkwrap.json",
17
+ "pnpm-lock.yaml",
18
+ "yarn.lock",
19
+ "bun.lock",
20
+ "bun.lockb",
21
+ ".pnp.cjs",
22
+ ".pnp.loader.mjs",
23
+ "node_modules/.package-lock.json",
24
+ "node_modules/.modules.yaml",
25
+ ]);
26
+
27
+ function normalizePath(value) {
28
+ return value.split(path.sep).join("/");
29
+ }
30
+
31
+ function withinRoot(root, candidate) {
32
+ const relative = path.relative(root, candidate);
33
+ return relative === ""
34
+ || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
35
+ }
36
+
37
+ function comparePaths(left, right) {
38
+ return left.localeCompare(right, "en");
39
+ }
40
+
41
+ function canonicalRealDirectory(requested, label) {
42
+ const absolute = path.resolve(requested);
43
+ const stat = lstatSync(absolute);
44
+ if (stat.isSymbolicLink() || !stat.isDirectory() || realpathSync(absolute) !== absolute) {
45
+ throw new Error(`${label} must be a real directory without symlink traversal.`);
46
+ }
47
+ return absolute;
48
+ }
49
+
50
+ function dependencyMaterial(root, relativePath, budget) {
51
+ const absolute = path.resolve(root, ...relativePath.split("/"));
52
+ if (!withinRoot(root, absolute) || !existsSync(absolute)) return null;
53
+ const stat = lstatSync(absolute);
54
+ if (stat.isSymbolicLink() || !stat.isFile() || realpathSync(absolute) !== absolute) {
55
+ throw new Error(`Dependency binding material must be a regular non-symlink file: ${relativePath}`);
56
+ }
57
+ budget.bytes += stat.size;
58
+ if (budget.bytes > MAX_DEPENDENCY_MATERIAL_BYTES) {
59
+ throw new Error("Dependency binding material exceeds the byte limit.");
60
+ }
61
+ return { path: relativePath, sha256: sha256Bytes(readFileSync(absolute)) };
62
+ }
63
+
64
+ function installedDependencyMaterialPaths(root) {
65
+ const nodeModules = path.join(root, "node_modules");
66
+ const discovered = new Set();
67
+ const addRealFile = (candidate) => {
68
+ let resolved;
69
+ try {
70
+ resolved = realpathSync(candidate);
71
+ } catch {
72
+ return;
73
+ }
74
+ if (!withinRoot(root, resolved) || !lstatSync(resolved).isFile()) {
75
+ throw new Error("Dependency resolution metadata escapes its declared root.");
76
+ }
77
+ discovered.add(normalizePath(path.relative(root, resolved)));
78
+ };
79
+ const addPackage = (candidate) => addRealFile(path.join(realpathSync(candidate), "package.json"));
80
+ for (const name of readdirSync(nodeModules).sort(comparePaths)) {
81
+ if (name === ".bin") {
82
+ for (const binName of readdirSync(path.join(nodeModules, name)).sort(comparePaths)) {
83
+ addRealFile(path.join(nodeModules, name, binName));
84
+ }
85
+ continue;
86
+ }
87
+ if (name.startsWith(".")) continue;
88
+ const entry = path.join(nodeModules, name);
89
+ const stat = lstatSync(entry);
90
+ if (name.startsWith("@") && stat.isDirectory() && !stat.isSymbolicLink()) {
91
+ for (const child of readdirSync(entry).sort(comparePaths)) addPackage(path.join(entry, child));
92
+ } else {
93
+ addPackage(entry);
94
+ }
95
+ if (discovered.size > 20_000) {
96
+ throw new Error("Dependency resolution metadata exceeds the file limit.");
97
+ }
98
+ }
99
+ return [...discovered].sort(comparePaths);
100
+ }
101
+
102
+ export function captureDependencyEnvironment(dependencyRoot) {
103
+ if (dependencyRoot === undefined || dependencyRoot === null) return null;
104
+ const root = canonicalRealDirectory(dependencyRoot, "Dependency root");
105
+ const nodeModules = path.join(root, "node_modules");
106
+ const nodeModulesStat = lstatSync(nodeModules);
107
+ if (nodeModulesStat.isSymbolicLink()
108
+ || !nodeModulesStat.isDirectory()
109
+ || realpathSync(nodeModules) !== nodeModules) {
110
+ throw new Error("Dependency root must contain a real node_modules directory.");
111
+ }
112
+ const budget = { bytes: 0 };
113
+ const materialPaths = [...new Set([
114
+ ...DEPENDENCY_MATERIALS,
115
+ ...installedDependencyMaterialPaths(root),
116
+ ])].sort(comparePaths);
117
+ const materials = materialPaths
118
+ .map(relativePath => dependencyMaterial(root, relativePath, budget))
119
+ .filter(Boolean);
120
+ if (!materials.some(material => material.path === "package.json") || materials.length < 2) {
121
+ throw new Error("Dependency root must bind package.json and lock or install metadata.");
122
+ }
123
+ const identity = { root, materials };
124
+ return {
125
+ ...identity,
126
+ fingerprint: sha256Canonical(identity),
127
+ };
128
+ }
@@ -44,6 +44,7 @@ export function attestFile({ subjectPath, workspaceRoot }) {
44
44
  if (new Set([
45
45
  "OwlCodaQuickVerificationReceiptV1",
46
46
  "OwlCodaQuickVerificationReceiptV2",
47
+ "OwlCodaQuickVerificationReceiptV3",
47
48
  ]).has(subject.value.schemaVersion)) {
48
49
  const details = attestQuickReceiptDetailsFromBytes({
49
50
  receiptPath: subject.absolutePath,
@@ -34,10 +34,33 @@ const SNAPSHOT_REQUIRED_KEYS = [
34
34
  "policyVersion",
35
35
  "sourceFingerprint",
36
36
  ];
37
+ const SNAPSHOT_V2_REQUIRED_KEYS = [
38
+ ...SNAPSHOT_REQUIRED_KEYS.filter(key => key !== "sourceFingerprint"),
39
+ "ignoredPathBindings",
40
+ "ignoredPathsFingerprint",
41
+ "sourceStateFingerprint",
42
+ "sourceFingerprint",
43
+ ];
37
44
  const KNOWN_RECEIPT_ISSUES = new Set([
38
45
  "quick_ignored_artifact_unbound",
39
46
  "source_mutated_during_verification",
40
47
  ]);
48
+ const V3_RECEIPT_ISSUES = new Set([
49
+ ...KNOWN_RECEIPT_ISSUES,
50
+ "bound_ignored_path_mutated_during_verification",
51
+ "dependency_environment_mutated_during_verification",
52
+ "consumer_source_mutated_during_verification",
53
+ "quick_isolation_cleanup_failed",
54
+ ]);
55
+ const V3_MUTATION_DECISIONS = new Set([
56
+ "source_unchanged",
57
+ "invalidated_by_command_source_mutation",
58
+ "invalidated_by_bound_ignored_mutation",
59
+ "invalidated_by_dependency_environment_mutation",
60
+ "invalidated_by_consumer_source_mutation",
61
+ "invalidated_by_isolation_cleanup_failure",
62
+ "invalidated_by_multiple_mutations",
63
+ ]);
41
64
 
42
65
  function validDateTime(value) {
43
66
  return typeof value === "string"
@@ -54,16 +77,30 @@ function validCoreIdentity(value) {
54
77
  && isSha256Ref(value.coreManifestSha256);
55
78
  }
56
79
 
57
- function validSnapshot(value) {
58
- if (!hasExactKeys(value, SNAPSHOT_REQUIRED_KEYS)) return false;
80
+ function sourceStateFingerprint(value) {
81
+ return sha256Canonical({
82
+ repositoryIdentity: value.repositoryIdentity,
83
+ headCommit: value.headCommit,
84
+ trackedTreeIdentity: value.trackedTreeIdentity,
85
+ submodules: value.submodules,
86
+ dirtyOverlay: value.dirtyOverlay,
87
+ dependencyLockfiles: value.dependencyLockfiles,
88
+ excludedRoots: value.excludedRoots,
89
+ });
90
+ }
91
+
92
+ export function validWorkspaceSnapshotShape(value) {
93
+ const snapshotV1 = value?.schemaVersion === "OwlCodaWorkspaceSnapshotV1";
94
+ const snapshotV2 = value?.schemaVersion === "OwlCodaWorkspaceSnapshotV2";
95
+ if (!(snapshotV1 || snapshotV2)) return false;
96
+ if (!hasExactKeys(value, snapshotV2 ? SNAPSHOT_V2_REQUIRED_KEYS : SNAPSHOT_REQUIRED_KEYS)) return false;
59
97
  if (
60
- value.schemaVersion !== "OwlCodaWorkspaceSnapshotV1"
61
- || typeof value.repositoryIdentity !== "string"
98
+ typeof value.repositoryIdentity !== "string"
62
99
  || value.repositoryIdentity.length === 0
63
100
  || !(value.headCommit === null || /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/.test(value.headCommit))
64
101
  || !isSha256Ref(value.trackedTreeIdentity)
65
- || value.ignoredPathsBound !== false
66
- || value.policyVersion !== "workspace-snapshot-v1"
102
+ || value.ignoredPathsBound !== snapshotV2
103
+ || value.policyVersion !== (snapshotV2 ? "workspace-snapshot-v2" : "workspace-snapshot-v1")
67
104
  || !isSha256Ref(value.sourceFingerprint)
68
105
  || !Array.isArray(value.submodules)
69
106
  || !Array.isArray(value.dirtyOverlay)
@@ -92,6 +129,22 @@ function validSnapshot(value) {
92
129
  && isSha256Ref(entry.sha256))) {
93
130
  return false;
94
131
  }
132
+ if (snapshotV2 && (
133
+ !Array.isArray(value.ignoredPathBindings)
134
+ || value.ignoredPathBindings.length === 0
135
+ || !isSha256Ref(value.ignoredPathsFingerprint)
136
+ || !isSha256Ref(value.sourceStateFingerprint)
137
+ || !value.ignoredPathBindings.every(entry =>
138
+ hasExactKeys(entry, ["path", "state", "sha256"])
139
+ && typeof entry.path === "string"
140
+ && entry.path.length > 0
141
+ && ["missing", "file", "directory"].includes(entry.state)
142
+ && (entry.sha256 === null || isSha256Ref(entry.sha256)))
143
+ || sha256Canonical(value.ignoredPathBindings) !== value.ignoredPathsFingerprint
144
+ || sourceStateFingerprint(value) !== value.sourceStateFingerprint
145
+ )) {
146
+ return false;
147
+ }
95
148
  return true;
96
149
  }
97
150
 
@@ -109,21 +162,146 @@ function validOutputArtifact(value) {
109
162
  && value.sizeBytes >= 0;
110
163
  }
111
164
 
165
+ function validDependencyEnvironment(value) {
166
+ if (value === null) return true;
167
+ if (!hasExactKeys(value, ["root", "materials", "fingerprint"])
168
+ || typeof value.root !== "string"
169
+ || value.root.length === 0
170
+ || !Array.isArray(value.materials)
171
+ || value.materials.length < 2
172
+ || !isSha256Ref(value.fingerprint)
173
+ || !value.materials.every(material =>
174
+ hasExactKeys(material, ["path", "sha256"])
175
+ && typeof material.path === "string"
176
+ && material.path.length > 0
177
+ && isSha256Ref(material.sha256))) {
178
+ return false;
179
+ }
180
+ return sha256Canonical({ root: value.root, materials: value.materials }) === value.fingerprint;
181
+ }
182
+
183
+ function validExecutionIsolation(value, receipt) {
184
+ if (value === null) {
185
+ return receipt.mutationClasses.consumerSource === "not_isolated"
186
+ && receipt.mutationClasses.consumerIgnoredArtifacts === "not_isolated";
187
+ }
188
+ return hasExactKeys(value, [
189
+ "schemaVersion",
190
+ "mode",
191
+ "consumerRoot",
192
+ "sourceCopy",
193
+ "cacheEnvironmentVariables",
194
+ "dependencyEnvironment",
195
+ "dependencyEnvironmentAfter",
196
+ "consumerDelta",
197
+ "cleanupStatus",
198
+ ])
199
+ && value.schemaVersion === "OwlCodaRunKitQuickIsolationV1"
200
+ && value.mode === "temporary_consumer"
201
+ && typeof value.consumerRoot === "string"
202
+ && value.consumerRoot.length > 0
203
+ && value.consumerRoot === receipt.exactCommand.cwd
204
+ && hasExactKeys(value.sourceCopy, ["fileCount", "totalBytes", "fingerprint"])
205
+ && Number.isInteger(value.sourceCopy.fileCount)
206
+ && value.sourceCopy.fileCount >= 0
207
+ && Number.isInteger(value.sourceCopy.totalBytes)
208
+ && value.sourceCopy.totalBytes >= 0
209
+ && isSha256Ref(value.sourceCopy.fingerprint)
210
+ && Array.isArray(value.cacheEnvironmentVariables)
211
+ && value.cacheEnvironmentVariables.length > 0
212
+ && new Set(value.cacheEnvironmentVariables).size === value.cacheEnvironmentVariables.length
213
+ && value.cacheEnvironmentVariables.every(name => typeof name === "string" && name.length > 0)
214
+ && validDependencyEnvironment(value.dependencyEnvironment)
215
+ && validDependencyEnvironment(value.dependencyEnvironmentAfter)
216
+ && (value.dependencyEnvironment !== null || value.dependencyEnvironmentAfter === null)
217
+ && hasExactKeys(value.consumerDelta, ["sourcePaths", "ignoredPaths"])
218
+ && [value.consumerDelta.sourcePaths, value.consumerDelta.ignoredPaths].every(paths =>
219
+ Array.isArray(paths)
220
+ && new Set(paths).size === paths.length
221
+ && paths.every(entry => typeof entry === "string" && entry.length > 0))
222
+ && ["removed", "cleanup_failed"].includes(value.cleanupStatus);
223
+ }
224
+
225
+ function validMutationClasses(value) {
226
+ return hasExactKeys(value, [
227
+ "workspaceSource",
228
+ "boundIgnoredPaths",
229
+ "dependencyEnvironment",
230
+ "consumerSource",
231
+ "consumerIgnoredArtifacts",
232
+ ])
233
+ && ["unchanged", "changed"].includes(value.workspaceSource)
234
+ && ["not_bound", "unchanged", "changed"].includes(value.boundIgnoredPaths)
235
+ && ["not_bound", "unchanged", "changed"].includes(value.dependencyEnvironment)
236
+ && ["not_isolated", "unchanged", "changed"].includes(value.consumerSource)
237
+ && ["not_isolated", "none", "changed_and_discarded"].includes(value.consumerIgnoredArtifacts);
238
+ }
239
+
240
+ function validV3MutationTruth(receipt) {
241
+ const workspaceSourceChanged = sourceStateFingerprint(receipt.workspaceBefore)
242
+ !== sourceStateFingerprint(receipt.workspaceAfter);
243
+ const boundIgnored = receipt.workspaceBefore.ignoredPathsBound === true;
244
+ const boundIgnoredChanged = boundIgnored
245
+ && receipt.workspaceBefore.ignoredPathsFingerprint
246
+ !== receipt.workspaceAfter.ignoredPathsFingerprint;
247
+ const isolation = receipt.executionIsolation;
248
+ const consumerSourceChanged = (isolation?.consumerDelta.sourcePaths.length ?? 0) > 0;
249
+ const consumerIgnoredChanged = (isolation?.consumerDelta.ignoredPaths.length ?? 0) > 0;
250
+ const dependencyBound = isolation !== null && isolation?.dependencyEnvironment !== null;
251
+ const dependencyChanged = dependencyBound
252
+ && (isolation.dependencyEnvironmentAfter === null
253
+ || isolation.dependencyEnvironmentAfter.fingerprint
254
+ !== isolation.dependencyEnvironment.fingerprint);
255
+ const cleanupFailed = isolation?.cleanupStatus === "cleanup_failed";
256
+ if (receipt.mutationClasses.workspaceSource !== (workspaceSourceChanged ? "changed" : "unchanged")
257
+ || receipt.mutationClasses.boundIgnoredPaths !== (boundIgnored
258
+ ? boundIgnoredChanged ? "changed" : "unchanged"
259
+ : "not_bound")
260
+ || receipt.mutationClasses.dependencyEnvironment !== (dependencyBound
261
+ ? dependencyChanged ? "changed" : "unchanged"
262
+ : "not_bound")
263
+ || receipt.mutationClasses.consumerSource !== (isolation === null
264
+ ? "not_isolated"
265
+ : consumerSourceChanged ? "changed" : "unchanged")
266
+ || receipt.mutationClasses.consumerIgnoredArtifacts !== (isolation === null
267
+ ? "not_isolated"
268
+ : consumerIgnoredChanged ? "changed_and_discarded" : "none")) {
269
+ return false;
270
+ }
271
+ const facts = [
272
+ [workspaceSourceChanged, "invalidated_by_command_source_mutation", "source_mutated_during_verification"],
273
+ [boundIgnoredChanged, "invalidated_by_bound_ignored_mutation", "bound_ignored_path_mutated_during_verification"],
274
+ [dependencyChanged, "invalidated_by_dependency_environment_mutation", "dependency_environment_mutated_during_verification"],
275
+ [consumerSourceChanged, "invalidated_by_consumer_source_mutation", "consumer_source_mutated_during_verification"],
276
+ [cleanupFailed, "invalidated_by_isolation_cleanup_failure", "quick_isolation_cleanup_failed"],
277
+ ].filter(([active]) => active);
278
+ const expectedDecision = facts.length === 0
279
+ ? "source_unchanged"
280
+ : facts.length === 1 ? facts[0][1] : "invalidated_by_multiple_mutations";
281
+ const expectedIssues = facts.map(([, , issue]) => issue).sort();
282
+ return receipt.mutationDecision === expectedDecision
283
+ && JSON.stringify([...receipt.issueCodes].sort()) === JSON.stringify(expectedIssues);
284
+ }
285
+
112
286
  export function validQuickReceiptShape(receipt) {
113
287
  const receiptV1 = receipt?.schemaVersion === "OwlCodaQuickVerificationReceiptV1";
114
288
  const receiptV2 = receipt?.schemaVersion === "OwlCodaQuickVerificationReceiptV2";
115
- const requiredKeys = receiptV2
289
+ const receiptV3 = receipt?.schemaVersion === "OwlCodaQuickVerificationReceiptV3";
290
+ const requiredKeys = receiptV3
291
+ ? [...RECEIPT_REQUIRED_KEYS, "sourceWorkspaceRoot", "executionIsolation", "mutationClasses"]
292
+ : receiptV2
116
293
  ? [...RECEIPT_REQUIRED_KEYS, "inputArtifacts"]
117
294
  : RECEIPT_REQUIRED_KEYS;
118
- if (!(receiptV1 || receiptV2) || !hasExactKeys(receipt, requiredKeys, ["signatureRef"])) return false;
295
+ const optionalKeys = receiptV3 ? ["inputArtifacts", "signatureRef"] : ["signatureRef"];
296
+ if (!(receiptV1 || receiptV2 || receiptV3) || !hasExactKeys(receipt, requiredKeys, optionalKeys)) return false;
119
297
  if (
120
298
  typeof receipt.receiptId !== "string"
121
299
  || receipt.receiptId.length === 0
122
300
  || receipt.assurance !== "captured_verification"
123
301
  || receipt.authorizationGranted !== false
124
302
  || !validCoreIdentity(receipt.coreIdentity)
125
- || !validSnapshot(receipt.workspaceBefore)
126
- || !validSnapshot(receipt.workspaceAfter)
303
+ || !validWorkspaceSnapshotShape(receipt.workspaceBefore)
304
+ || !validWorkspaceSnapshotShape(receipt.workspaceAfter)
127
305
  || !hasExactKeys(receipt.exactCommand, ["executable", "argv", "cwd"])
128
306
  || typeof receipt.exactCommand.executable !== "string"
129
307
  || receipt.exactCommand.executable.length === 0
@@ -142,10 +320,13 @@ export function validQuickReceiptShape(receipt) {
142
320
  || !hasExactKeys(receipt.outputArtifacts, ["stdout", "stderr"])
143
321
  || !validOutputArtifact(receipt.outputArtifacts.stdout)
144
322
  || !validOutputArtifact(receipt.outputArtifacts.stderr)
145
- || !["source_unchanged", "invalidated_by_command_source_mutation"].includes(receipt.mutationDecision)
323
+ || !(receiptV3
324
+ ? V3_MUTATION_DECISIONS.has(receipt.mutationDecision)
325
+ : ["source_unchanged", "invalidated_by_command_source_mutation"].includes(receipt.mutationDecision))
146
326
  || !Array.isArray(receipt.issueCodes)
147
327
  || new Set(receipt.issueCodes).size !== receipt.issueCodes.length
148
- || !receipt.issueCodes.every((entry) => typeof entry === "string" && KNOWN_RECEIPT_ISSUES.has(entry))
328
+ || !receipt.issueCodes.every((entry) => typeof entry === "string"
329
+ && (receiptV3 ? V3_RECEIPT_ISSUES : KNOWN_RECEIPT_ISSUES).has(entry))
149
330
  ) {
150
331
  return false;
151
332
  }
@@ -155,6 +336,18 @@ export function validQuickReceiptShape(receipt) {
155
336
  )) {
156
337
  return false;
157
338
  }
339
+ if (receiptV3 && (
340
+ typeof receipt.sourceWorkspaceRoot !== "string"
341
+ || receipt.sourceWorkspaceRoot.length === 0
342
+ || !validMutationClasses(receipt.mutationClasses)
343
+ || !validExecutionIsolation(receipt.executionIsolation, receipt)
344
+ || receipt.inputArtifacts !== undefined
345
+ && (!hasExactKeys(receipt.inputArtifacts, ["stdin"])
346
+ || !validOutputArtifact(receipt.inputArtifacts.stdin))
347
+ || !validV3MutationTruth(receipt)
348
+ )) {
349
+ return false;
350
+ }
158
351
  if (receipt.signatureRef !== undefined) {
159
352
  if (!hasExactKeys(receipt.signatureRef, ["path", "sha256"])
160
353
  || typeof receipt.signatureRef.path !== "string"
@@ -13,11 +13,22 @@ import {
13
13
  quickSnapshotFingerprintValid,
14
14
  validQuickReceiptShape,
15
15
  } from "./quick-receipt-contract.mjs";
16
+ import { captureDependencyEnvironment } from "./dependency-environment.mjs";
16
17
  import { captureWorkspaceSnapshot } from "./workspace-snapshot.mjs";
17
18
 
18
19
  export { validQuickReceiptShape } from "./quick-receipt-contract.mjs";
19
20
 
20
21
  export const SUPPORTED_QUICK_CORE_IDENTITIES = Object.freeze([
22
+ {
23
+ contractVersion: "0.2",
24
+ coreVersion: "0.19.0",
25
+ coreManifestSha256: "sha256:803fb7de76a6ccffd1ec5e0164eeff15a580209fa6dfbef9e561345ab616b403",
26
+ },
27
+ {
28
+ contractVersion: "0.2",
29
+ coreVersion: "0.18.4",
30
+ coreManifestSha256: "sha256:372d4c247e4b40b0fd2f3fe0f5bf928548095bfb7e2e92e9ac66790e2717f77d",
31
+ },
21
32
  {
22
33
  contractVersion: "0.2",
23
34
  coreVersion: "0.18.3",
@@ -120,6 +131,23 @@ export const SUPPORTED_QUICK_CORE_IDENTITIES = Object.freeze([
120
131
  },
121
132
  ]);
122
133
 
134
+ function dependencyEnvironmentCurrent(binding, materials) {
135
+ if (binding === null || binding === undefined) return true;
136
+ try {
137
+ const current = captureDependencyEnvironment(binding.root);
138
+ if (current.root !== binding.root || current.fingerprint !== binding.fingerprint) return false;
139
+ for (const material of current.materials) {
140
+ materials.push({
141
+ path: resolveBoundMaterial(current.root, material.path),
142
+ sha256: material.sha256,
143
+ });
144
+ }
145
+ return true;
146
+ } catch {
147
+ return false;
148
+ }
149
+ }
150
+
123
151
  function resolveBoundMaterial(root, relativePath) {
124
152
  if (path.isAbsolute(relativePath) || relativePath.includes("\0")) {
125
153
  throw new Error("material path must be workspace-relative");
@@ -194,7 +222,7 @@ export function attestQuickReceiptDetailsFromBytes({
194
222
  throw error;
195
223
  }
196
224
  if (!isPlainJsonObject(receipt) || !validQuickReceiptShape(receipt)) {
197
- const error = new Error("Quick receipt does not satisfy the strict Wave 1 shape");
225
+ const error = new Error("Quick receipt does not satisfy a supported strict shape");
198
226
  error.code = "receipt_schema_invalid";
199
227
  throw error;
200
228
  }
@@ -234,7 +262,11 @@ export function attestQuickReceiptDetailsFromBytes({
234
262
  let receiptWorkspaceRoot;
235
263
  if (selectedWorkspaceRoot !== undefined) {
236
264
  try {
237
- receiptWorkspaceRoot = realpathSync(receipt.exactCommand.cwd);
265
+ receiptWorkspaceRoot = realpathSync(
266
+ receipt.schemaVersion === "OwlCodaQuickVerificationReceiptV3"
267
+ ? receipt.sourceWorkspaceRoot
268
+ : receipt.exactCommand.cwd,
269
+ );
238
270
  } catch {
239
271
  receiptWorkspaceRoot = undefined;
240
272
  }
@@ -267,11 +299,19 @@ export function attestQuickReceiptDetailsFromBytes({
267
299
  }
268
300
  if (materialMissing) issues.push("attestation_material_missing");
269
301
  if (materialMismatch) issues.push("receipt_material_hash_mismatch");
302
+ const ignoredPaths = receipt.workspaceAfter.ignoredPathsBound === true
303
+ ? receipt.workspaceAfter.ignoredPathBindings.map(entry => entry.path)
304
+ : [];
270
305
  const currentSourceValid = selectedWorkspaceRoot === undefined
271
306
  || commandWorkspaceMatches
272
- && captureWorkspaceSnapshot(selectedWorkspaceRoot).sourceFingerprint
307
+ && captureWorkspaceSnapshot(selectedWorkspaceRoot, { ignoredPaths }).sourceFingerprint
273
308
  === receipt.workspaceAfter.sourceFingerprint;
274
- const sourceValid = receiptSourceValid && currentSourceValid;
309
+ const dependencyEnvironmentValid = dependencyEnvironmentCurrent(
310
+ receipt.executionIsolation?.dependencyEnvironment,
311
+ materials,
312
+ );
313
+ if (!dependencyEnvironmentValid) issues.push("receipt_dependency_environment_mismatch");
314
+ const sourceValid = receiptSourceValid && currentSourceValid && dependencyEnvironmentValid;
275
315
  if (!sourceValid || receipt.mutationDecision !== "source_unchanged") {
276
316
  issues.push("receipt_source_mismatch");
277
317
  }
@@ -280,6 +320,7 @@ export function attestQuickReceiptDetailsFromBytes({
280
320
  const deterministicFailure = !coreValid
281
321
  || !contextValid
282
322
  || materialMismatch
323
+ || !dependencyEnvironmentValid
283
324
  || !sourceValid
284
325
  || receipt.mutationDecision !== "source_unchanged"
285
326
  || !commandPassed;