owlrunkit 0.15.1 → 0.16.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 (76) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/README.md +274 -66
  3. package/integrations/codex/skills/owlcoda-runkit/SKILL.md +222 -116
  4. package/integrations/codex/skills/owlcoda-runkit/assets/templates/config.json +3 -3
  5. package/integrations/codex/skills/owlcoda-runkit/assets/templates/ready-for-commit-request.json +2 -2
  6. package/package.json +2 -2
  7. package/packages/attest/distribution-manifest-v1.json +5 -0
  8. package/packages/attest/src/quick.mjs +5 -0
  9. package/registry-release-policy-v1.json +8 -2
  10. package/sbom.spdx.json +5 -5
  11. package/scripts/runkit-contract/assurance-router.mjs +119 -0
  12. package/scripts/runkit-contract/core-contract.mjs +203 -206
  13. package/scripts/runkit-contract/core-successor.mjs +2043 -0
  14. package/scripts/runkit-contract/deployment-workflow.mjs +2040 -0
  15. package/scripts/runkit-contract/finalize.mjs +242 -21
  16. package/scripts/runkit-contract/fleet-discovery.mjs +456 -0
  17. package/scripts/runkit-contract/formal-workflow.mjs +850 -0
  18. package/scripts/runkit-contract/human-status.mjs +553 -0
  19. package/scripts/runkit-contract/inspect-presentation.mjs +25 -8
  20. package/scripts/runkit-contract/install-codex-skill.mjs +73 -30
  21. package/scripts/runkit-contract/lease-lifecycle.mjs +8 -2
  22. package/scripts/runkit-contract/lifecycle-orchestration.mjs +135 -12
  23. package/scripts/runkit-contract/onboarding-doctor.mjs +43 -90
  24. package/scripts/runkit-contract/owner-authority-trust.mjs +230 -0
  25. package/scripts/runkit-contract/owner-deployment-decision.mjs +385 -0
  26. package/scripts/runkit-contract/profile-onboarding.mjs +841 -13
  27. package/scripts/runkit-contract/project-cli-resolver.mjs +701 -0
  28. package/scripts/runkit-contract/project-control-parser.mjs +830 -0
  29. package/scripts/runkit-contract/project-control-state.mjs +28 -0
  30. package/scripts/runkit-contract/provenance-common.mjs +19 -2
  31. package/scripts/runkit-contract/ready-for-commit.mjs +330 -18
  32. package/scripts/runkit-contract/remote-deployment.mjs +1596 -0
  33. package/scripts/runkit-contract/remote-process-adapter.mjs +177 -0
  34. package/scripts/runkit-contract/runkit-bootstrap.mjs +131 -0
  35. package/scripts/runkit-contract/runkit-cli.mjs +1097 -21
  36. package/scripts/runkit-contract/schemas/assurance-route-v1.schema.json +26 -0
  37. package/scripts/runkit-contract/schemas/core-successor-batch-receipt-v1.schema.json +126 -0
  38. package/scripts/runkit-contract/schemas/core-successor-plan-v1.schema.json +155 -0
  39. package/scripts/runkit-contract/schemas/core-successor-project-success-receipt-v1.schema.json +72 -0
  40. package/scripts/runkit-contract/schemas/deployment-execute-request-v1.schema.json +63 -0
  41. package/scripts/runkit-contract/schemas/deployment-lineage-v1.schema.json +292 -0
  42. package/scripts/runkit-contract/schemas/deployment-prepare-receipt-v1.schema.json +140 -0
  43. package/scripts/runkit-contract/schemas/fleet-discovery-v1.schema.json +31 -0
  44. package/scripts/runkit-contract/schemas/fleet-manifest-v1.schema.json +20 -0
  45. package/scripts/runkit-contract/schemas/fleet-root-registry-v1.schema.json +34 -0
  46. package/scripts/runkit-contract/schemas/human-status-v1.schema.json +102 -0
  47. package/scripts/runkit-contract/schemas/owner-authority-trust-v1.schema.json +66 -0
  48. package/scripts/runkit-contract/schemas/owner-deployment-authority-v1.schema.json +46 -0
  49. package/scripts/runkit-contract/schemas/owner-deployment-authority-v2.schema.json +94 -0
  50. package/scripts/runkit-contract/schemas/owner-deployment-decision-v1.schema.json +102 -0
  51. package/scripts/runkit-contract/schemas/owner-migration-authority-v1.schema.json +33 -0
  52. package/scripts/runkit-contract/schemas/owner-migration-authority-v2.schema.json +67 -0
  53. package/scripts/runkit-contract/schemas/profile-apply-receipt-v1.schema.json +32 -0
  54. package/scripts/runkit-contract/schemas/profile-detection-v2.schema.json +145 -0
  55. package/scripts/runkit-contract/schemas/ready-for-commit-v2.schema.json +37 -0
  56. package/scripts/runkit-contract/schemas/remote-deployment-manifest-v1.schema.json +379 -0
  57. package/scripts/runkit-contract/schemas/remote-deployment-result-v1.schema.json +153 -0
  58. package/scripts/runkit-contract/schemas/remote-deployment-stage-journal-v1.schema.json +183 -0
  59. package/scripts/runkit-contract/schemas/remote-target-v1.schema.json +32 -0
  60. package/scripts/runkit-contract/schemas/source-candidate-v2.schema.json +152 -0
  61. package/scripts/runkit-contract/schemas/verification-receipt-v2.schema.json +13 -0
  62. package/scripts/runkit-contract/snapshot.mjs +1 -1
  63. package/scripts/runkit-contract/source-candidate.mjs +1382 -0
  64. package/scripts/runkit-contract/ssh-remote-adapter.mjs +1184 -0
  65. package/scripts/runkit-contract/sync-core016-release-identity.mjs +161 -0
  66. package/scripts/runkit-contract/templates/assurance-request.json +13 -0
  67. package/scripts/runkit-contract/templates/core-successor-owner-authority.json +15 -0
  68. package/scripts/runkit-contract/templates/deployment-execute-request.json +39 -0
  69. package/scripts/runkit-contract/templates/deployment-owner-authority.json +23 -0
  70. package/scripts/runkit-contract/templates/deployment-owner-decision.json +26 -0
  71. package/scripts/runkit-contract/templates/fleet-manifest.json +8 -0
  72. package/scripts/runkit-contract/templates/fleet-root-registry.json +8 -0
  73. package/scripts/runkit-contract/templates/owner-authority-trust.json +16 -0
  74. package/scripts/runkit-contract/templates/ready-for-commit-request.json +2 -2
  75. package/scripts/runkit-contract/templates/remote-deployment-manifest.json +108 -0
  76. package/scripts/runkit-contract/verification-receipt-gate.mjs +28 -0
@@ -20,11 +20,12 @@ import {
20
20
  safeIdentifier,
21
21
  sha256,
22
22
  writeJsonAtomically,
23
- writeJsonExclusive,
23
+ writeJsonExclusiveAtomically,
24
24
  } from "./provenance-common.mjs";
25
25
  import { receiptSha256, validateReceiptLineage } from "./receipt-lineage.mjs";
26
26
  import { verifySnapshotEvidence, verifySnapshotSourceBinding } from "./snapshot.mjs";
27
27
  import { verifyDeliveryPacket } from "./source-fingerprint.mjs";
28
+ import { verifySourceCandidateV2 } from "./source-candidate.mjs";
28
29
  import {
29
30
  validateVerificationContext,
30
31
  verificationContextFingerprint,
@@ -36,6 +37,7 @@ const REQUEST_KEYS = [
36
37
  "schemaVersion",
37
38
  "receiptId",
38
39
  "deliveryPacketPath",
40
+ "sourceCandidatePath",
39
41
  "verificationContext",
40
42
  "snapshotPaths",
41
43
  "supersedesReceiptSha256",
@@ -56,6 +58,75 @@ const REPAIRABLE_INVALID_REASONS = new Set([
56
58
  "source_mutated_during_verification",
57
59
  ]);
58
60
 
61
+ function canonicalJson(value) {
62
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
63
+ if (value && typeof value === "object") {
64
+ return `{${Object.keys(value).sort()
65
+ .map(key => `${JSON.stringify(key)}:${canonicalJson(value[key])}`)
66
+ .join(",")}}`;
67
+ }
68
+ const encoded = JSON.stringify(value);
69
+ if (encoded === undefined) {
70
+ throw new Error("Finalize artifact is not canonical JSON.");
71
+ }
72
+ return encoded;
73
+ }
74
+
75
+ function writeFinalizeArtifactExact(filePath, value, label) {
76
+ if (!existsSync(filePath)) {
77
+ writeJsonExclusiveAtomically(filePath, value);
78
+ return;
79
+ }
80
+ const stat = lstatSync(filePath);
81
+ if (stat.isSymbolicLink() || !stat.isFile()) {
82
+ throw new Error(`Existing finalize artifact differs: ${label}.`);
83
+ }
84
+ const existing = readJson(realpathSync(filePath));
85
+ if (canonicalJson(existing) !== canonicalJson(value)) {
86
+ throw new Error(`Existing finalize artifact differs: ${label}.`);
87
+ }
88
+ }
89
+
90
+ function writeFinalizeLineageExact(
91
+ filePath,
92
+ { expectedEntries, nextEntries },
93
+ ) {
94
+ const validation = validateReceiptLineage(nextEntries);
95
+ if (!validation.valid || !validation.active) {
96
+ throw new Error("Replacement receipt lineage is invalid.");
97
+ }
98
+ if (!existsSync(filePath)) {
99
+ if (expectedEntries.length !== 0 || nextEntries.length !== 1) {
100
+ throw new Error("Receipt lineage changed before activation.");
101
+ }
102
+ writeJsonExclusiveAtomically(filePath, nextEntries);
103
+ return;
104
+ }
105
+ const stat = lstatSync(filePath);
106
+ if (stat.isSymbolicLink() || !stat.isFile()) {
107
+ throw new Error("Receipt lineage changed before activation.");
108
+ }
109
+ const document = readJson(realpathSync(filePath));
110
+ const currentEntries = Array.isArray(document)
111
+ ? document
112
+ : document?.receipts;
113
+ if (!Array.isArray(currentEntries)) {
114
+ throw new Error("Receipt lineage changed before activation.");
115
+ }
116
+ if (canonicalJson(currentEntries) === canonicalJson(nextEntries)) {
117
+ return;
118
+ }
119
+ if (
120
+ canonicalJson(currentEntries) !== canonicalJson(expectedEntries)
121
+ || nextEntries.length !== expectedEntries.length + 1
122
+ || canonicalJson(nextEntries.slice(0, -1))
123
+ !== canonicalJson(expectedEntries)
124
+ ) {
125
+ throw new Error("Receipt lineage changed before activation.");
126
+ }
127
+ writeJsonAtomically(filePath, nextEntries);
128
+ }
129
+
59
130
  function changedPaths(packet) {
60
131
  const files = packet?.changedFiles?.files ?? packet?.changedFiles?.wholeFileSha256;
61
132
  if (!isRecord(files) || Object.keys(files).length === 0) {
@@ -64,14 +135,106 @@ function changedPaths(packet) {
64
135
  return Object.keys(files).sort();
65
136
  }
66
137
 
138
+ function candidateChangedPaths(candidate) {
139
+ return [...new Set(candidate.sourceManifest.entries.flatMap((entry) => (
140
+ entry.previousPath ? [entry.path, entry.previousPath] : [entry.path]
141
+ )))].sort();
142
+ }
143
+
144
+ function candidatePresentFiles(candidate) {
145
+ return Object.fromEntries(candidate.sourceManifest.entries
146
+ .filter((entry) => entry.operation !== "deleted")
147
+ .map((entry) => [entry.path, entry.sha256])
148
+ .sort(([left], [right]) => left.localeCompare(right)));
149
+ }
150
+
151
+ function loadFinalizeSource({ workspaceRoot, runId, request }) {
152
+ if (request.deliveryPacketPath !== undefined) {
153
+ const sourcePath = resolveExistingArtifact(
154
+ workspaceRoot,
155
+ request.deliveryPacketPath,
156
+ "deliveryPacketPath",
157
+ );
158
+ const source = readJson(sourcePath);
159
+ if (source.runId !== runId) {
160
+ throw new Error("Delivery packet runId does not match the execution.");
161
+ }
162
+ const sourceGate = verifyDeliveryPacket({ workspaceRoot, packet: source });
163
+ return {
164
+ kind: "delivery_packet_v1",
165
+ sourcePath,
166
+ source,
167
+ sourceGate,
168
+ changedPaths: changedPaths(source),
169
+ expectedFiles:
170
+ source.changedFiles?.files ?? source.changedFiles?.wholeFileSha256,
171
+ verifyCurrent: () => verifyDeliveryPacket({
172
+ workspaceRoot,
173
+ packet: source,
174
+ }),
175
+ };
176
+ }
177
+
178
+ const sourcePath = resolveExistingArtifact(
179
+ workspaceRoot,
180
+ request.sourceCandidatePath,
181
+ "sourceCandidatePath",
182
+ );
183
+ const source = readJson(sourcePath);
184
+ if (source.runId !== runId) {
185
+ throw new Error("Source candidate runId does not match the execution.");
186
+ }
187
+ const candidateGate = verifySourceCandidateV2({
188
+ workspaceRoot,
189
+ candidatePath: request.sourceCandidatePath,
190
+ });
191
+ const fingerprint = source.sourceFingerprint?.sha256;
192
+ const sourceGate = {
193
+ status: candidateGate.status,
194
+ exitCode: candidateGate.exitCode,
195
+ declaredFingerprint: fingerprint,
196
+ recomputedFingerprint: candidateGate.status === "valid"
197
+ ? candidateGate.sourceFingerprint
198
+ : fingerprint,
199
+ issues: candidateGate.issueCodes ?? [],
200
+ };
201
+ return {
202
+ kind: "source_candidate_v2",
203
+ sourcePath,
204
+ source,
205
+ sourceGate,
206
+ changedPaths: candidateChangedPaths(source),
207
+ expectedFiles: candidatePresentFiles(source),
208
+ verifyCurrent: () => {
209
+ const gate = verifySourceCandidateV2({
210
+ workspaceRoot,
211
+ candidatePath: request.sourceCandidatePath,
212
+ });
213
+ return {
214
+ status: gate.status,
215
+ exitCode: gate.exitCode,
216
+ declaredFingerprint: fingerprint,
217
+ recomputedFingerprint: gate.status === "valid"
218
+ ? gate.sourceFingerprint
219
+ : fingerprint,
220
+ issues: gate.issueCodes ?? [],
221
+ };
222
+ },
223
+ };
224
+ }
225
+
67
226
  function validateRequest(request) {
68
227
  assertAllowedKeys(request, "Finalize request", REQUEST_KEYS);
69
228
  if (request.schemaVersion !== "OwlCodaRunKitFinalizeRequestV1") {
70
229
  throw new Error("Unsupported finalize request schemaVersion.");
71
230
  }
72
231
  safeIdentifier(request.receiptId, "receiptId");
73
- if (typeof request.deliveryPacketPath !== "string" || request.deliveryPacketPath.length === 0) {
74
- throw new Error("Finalize request requires deliveryPacketPath.");
232
+ const hasDeliveryPacket = typeof request.deliveryPacketPath === "string"
233
+ && request.deliveryPacketPath.length > 0;
234
+ const hasSourceCandidate = typeof request.sourceCandidatePath === "string"
235
+ && request.sourceCandidatePath.length > 0;
236
+ if (hasDeliveryPacket === hasSourceCandidate) {
237
+ throw new Error("Finalize requires exactly one deliveryPacketPath or sourceCandidatePath.");
75
238
  }
76
239
  if (!Array.isArray(request.snapshotPaths)
77
240
  || request.snapshotPaths.some((value) => typeof value !== "string" || value.length === 0)) {
@@ -100,6 +263,9 @@ function validateRequest(request) {
100
263
  if (request.supersedesReceiptSha256 === undefined && request.repairControl !== undefined) {
101
264
  throw new Error("repairControl is only valid for repair finalize.");
102
265
  }
266
+ if (hasSourceCandidate && request.supersedesReceiptSha256 !== undefined) {
267
+ throw new Error("SourceCandidate V2 finalize does not support receipt repair.");
268
+ }
103
269
  if (request.repairControl !== undefined) validateRepairControl(request.repairControl);
104
270
  const context = validateVerificationContext(request.verificationContext);
105
271
  if (!context.valid) throw new Error(`Finalize verification context is invalid: ${context.issues.join("; ")}`);
@@ -280,6 +446,7 @@ function snapshotCommandReceipt({
280
446
  expectedFiles,
281
447
  expectedFingerprint,
282
448
  partialSourceBinding = false,
449
+ sourceCandidate = null,
283
450
  }) {
284
451
  const absolutePath = resolveExistingArtifact(workspaceRoot, snapshotPath, "snapshotPaths entry");
285
452
  const snapshot = readJson(absolutePath);
@@ -288,7 +455,29 @@ function snapshotCommandReceipt({
288
455
  }
289
456
  const validation = verifySnapshotEvidence({ workspaceRoot, runId, snapshot });
290
457
  if (!validation.valid) throw new Error(validation.issues.join("; "));
291
- if (!partialSourceBinding) {
458
+ if (sourceCandidate !== null) {
459
+ const expected = JSON.stringify(sortedFileMap(expectedFiles));
460
+ const before = JSON.stringify(sortedFileMap(
461
+ snapshot.repositoryBefore?.selectedFiles ?? {},
462
+ ));
463
+ const after = JSON.stringify(sortedFileMap(
464
+ snapshot.repositoryAfter?.selectedFiles ?? {},
465
+ ));
466
+ const materialInputs = Object.entries(sortedFileMap(expectedFiles))
467
+ .map(([id, hash]) => ({ id, sha256: hash }));
468
+ const actualMaterialInputs = [
469
+ ...(snapshot.command?.evidence?.materialInputs ?? []),
470
+ ].sort((left, right) => String(left?.id).localeCompare(String(right?.id)));
471
+ if (
472
+ snapshot.repositoryBefore?.head !== sourceCandidate.baseline.head
473
+ || snapshot.repositoryAfter?.head !== sourceCandidate.baseline.head
474
+ || before !== expected
475
+ || after !== expected
476
+ || JSON.stringify(actualMaterialInputs) !== JSON.stringify(materialInputs)
477
+ ) {
478
+ throw new Error("Snapshot source does not match SourceCandidate V2.");
479
+ }
480
+ } else if (!partialSourceBinding) {
292
481
  const sourceBinding = verifySnapshotSourceBinding({ snapshot, expectedFiles, expectedFingerprint });
293
482
  if (!sourceBinding.valid) throw new Error(sourceBinding.issues.join("; "));
294
483
  }
@@ -323,6 +512,13 @@ function nextLineage({ lineagePath, receipt, supersedesReceiptSha256 }) {
323
512
  }
324
513
  return [{ receiptSha256: nextHash, receipt }];
325
514
  }
515
+ if (
516
+ supersedesReceiptSha256 === undefined
517
+ && existing.active.receiptSha256 === nextHash
518
+ && canonicalJson(existing.active.receipt) === canonicalJson(receipt)
519
+ ) {
520
+ return existing.entries;
521
+ }
326
522
  if (supersedesReceiptSha256 !== undefined) {
327
523
  if (existing.active.receiptSha256 !== supersedesReceiptSha256) {
328
524
  throw new Error("Repair supersedesReceiptSha256 does not match the active receipt.");
@@ -603,14 +799,10 @@ export function runFinalize({ workspaceRoot, runId, request, hooks = {} }) {
603
799
  const { executionRoot, pinGate } = loadActiveExecution(workspaceRoot, runId);
604
800
  if (pinGate.status !== "valid") return pinGate;
605
801
  assertExecutionUnclosed(executionRoot, runId);
606
- const packetPath = resolveExistingArtifact(
607
- workspaceRoot,
608
- request.deliveryPacketPath,
609
- "deliveryPacketPath",
610
- );
611
- const packet = readJson(packetPath);
612
- if (packet.runId !== runId) throw new Error("Delivery packet runId does not match the execution.");
613
- const sourceGate = verifyDeliveryPacket({ workspaceRoot, packet });
802
+ const sourceBinding = loadFinalizeSource({ workspaceRoot, runId, request });
803
+ const packetPath = sourceBinding.sourcePath;
804
+ const packet = sourceBinding.source;
805
+ const sourceGate = sourceBinding.sourceGate;
614
806
  if (sourceGate.status !== "valid") {
615
807
  return {
616
808
  status: sourceGate.status,
@@ -622,7 +814,7 @@ export function runFinalize({ workspaceRoot, runId, request, hooks = {} }) {
622
814
  }
623
815
  const profilesDocument = readJson(path.join(workspaceRoot, ".owlcoda/runkit/profiles.json"));
624
816
  const profileImpact = resolveProfileImpactProjection({
625
- changedPaths: changedPaths(packet),
817
+ changedPaths: sourceBinding.changedPaths,
626
818
  profiles: profilesDocument.profiles,
627
819
  detailed: request.supersedesReceiptSha256 !== undefined,
628
820
  });
@@ -636,7 +828,7 @@ export function runFinalize({ workspaceRoot, runId, request, hooks = {} }) {
636
828
  };
637
829
  }
638
830
  const contextFingerprint = verificationContextFingerprint(request.verificationContext);
639
- const expectedFiles = packet.changedFiles?.files ?? packet.changedFiles?.wholeFileSha256;
831
+ const expectedFiles = sourceBinding.expectedFiles;
640
832
  const receiptsRoot = path.join(executionRoot, "verification-receipts");
641
833
  const lineagePath = path.join(receiptsRoot, "receipt-lineage.json");
642
834
  const existing = activeLineage(lineagePath);
@@ -679,6 +871,9 @@ export function runFinalize({ workspaceRoot, runId, request, hooks = {} }) {
679
871
  expectedFiles,
680
872
  expectedFingerprint: sourceGate.recomputedFingerprint,
681
873
  partialSourceBinding: request.supersedesReceiptSha256 !== undefined,
874
+ sourceCandidate: sourceBinding.kind === "source_candidate_v2"
875
+ ? sourceBinding.source
876
+ : null,
682
877
  }));
683
878
  const commandReceipts = [...reusedCommandReceipts, ...replayedCommandReceipts];
684
879
  const commandIds = commandReceipts.map(receipt => receipt.id);
@@ -714,6 +909,13 @@ export function runFinalize({ workspaceRoot, runId, request, hooks = {} }) {
714
909
  receiptId: request.receiptId,
715
910
  status: "passed",
716
911
  sourceFingerprint: sourceGate.recomputedFingerprint,
912
+ sourceArtifact: {
913
+ kind: sourceBinding.kind,
914
+ runId,
915
+ path: relativeToWorkspace(workspaceRoot, sourceBinding.sourcePath),
916
+ sha256: sha256(readFileSync(sourceBinding.sourcePath)),
917
+ sourceFingerprint: sourceGate.recomputedFingerprint,
918
+ },
717
919
  verificationContextFingerprint: contextFingerprint,
718
920
  goalContractSha256: currentGoal.goalContractSha256,
719
921
  risk: structuredClone(currentGoal.risk),
@@ -758,7 +960,7 @@ export function runFinalize({ workspaceRoot, runId, request, hooks = {} }) {
758
960
  selectedProfileIds: profileImpact.profileIds,
759
961
  activeParent: existing.active,
760
962
  });
761
- } else if (verifyDeliveryPacket({ workspaceRoot, packet }).status !== "valid") {
963
+ } else if (sourceBinding.verifyCurrent().status !== "valid") {
762
964
  throw new Error("Source changed before receipt persistence.");
763
965
  }
764
966
  };
@@ -768,14 +970,33 @@ export function runFinalize({ workspaceRoot, runId, request, hooks = {} }) {
768
970
  const profileImpactPath = path.join(outputRoot, "profile-impact.json");
769
971
  const gateInputPath = path.join(outputRoot, "verification-gate-input.json");
770
972
  const gateOutputPath = path.join(outputRoot, "verification-gate-output.json");
771
- writeJsonExclusive(receiptPath, receipt);
772
- writeJsonExclusive(sourceGatePath, sourceGate);
773
- writeJsonExclusive(profileImpactPath, profileImpact);
774
- writeJsonExclusive(gateInputPath, gateInput);
775
- writeJsonExclusive(gateOutputPath, gate);
973
+ writeFinalizeArtifactExact(
974
+ receiptPath,
975
+ receipt,
976
+ "verification receipt",
977
+ );
978
+ writeFinalizeArtifactExact(sourceGatePath, sourceGate, "source gate");
979
+ writeFinalizeArtifactExact(
980
+ profileImpactPath,
981
+ profileImpact,
982
+ "profile impact",
983
+ );
984
+ writeFinalizeArtifactExact(
985
+ gateInputPath,
986
+ gateInput,
987
+ "verification gate input",
988
+ );
989
+ writeFinalizeArtifactExact(
990
+ gateOutputPath,
991
+ gate,
992
+ "verification gate output",
993
+ );
776
994
  if (typeof hooks.beforeLineageActivate === "function") hooks.beforeLineageActivate();
777
995
  assertCurrentBindings();
778
- writeJsonAtomically(lineagePath, lineage);
996
+ writeFinalizeLineageExact(lineagePath, {
997
+ expectedEntries: existing.entries,
998
+ nextEntries: lineage,
999
+ });
779
1000
  return {
780
1001
  status: gate.decision,
781
1002
  exitCode: 0,