@agent-teams/engineering-foundation 0.17.0 → 0.18.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 (142) hide show
  1. package/dist/capabilities/contract-json-schema-releases/module.d.ts.map +1 -1
  2. package/dist/capabilities/contract-json-schema-releases/module.js +4 -17
  3. package/dist/capabilities/contract-json-schema-releases/module.js.map +1 -1
  4. package/dist/capabilities/contract-protobuf-evolution/module.d.ts.map +1 -1
  5. package/dist/capabilities/contract-protobuf-evolution/module.js +4 -17
  6. package/dist/capabilities/contract-protobuf-evolution/module.js.map +1 -1
  7. package/dist/capabilities/documentation-local-references/module.d.ts.map +1 -1
  8. package/dist/capabilities/documentation-local-references/module.js +4 -17
  9. package/dist/capabilities/documentation-local-references/module.js.map +1 -1
  10. package/dist/capabilities/executable-specifications/module.d.ts.map +1 -1
  11. package/dist/capabilities/executable-specifications/module.js +4 -17
  12. package/dist/capabilities/executable-specifications/module.js.map +1 -1
  13. package/dist/capabilities/governance-architecture-decisions/module.d.ts.map +1 -1
  14. package/dist/capabilities/governance-architecture-decisions/module.js +4 -17
  15. package/dist/capabilities/governance-architecture-decisions/module.js.map +1 -1
  16. package/dist/capabilities/public-api-compatibility/module.d.ts.map +1 -1
  17. package/dist/capabilities/public-api-compatibility/module.js +4 -19
  18. package/dist/capabilities/public-api-compatibility/module.js.map +1 -1
  19. package/dist/capabilities/quality-gate-runner/module.d.ts.map +1 -1
  20. package/dist/capabilities/quality-gate-runner/module.js +4 -17
  21. package/dist/capabilities/quality-gate-runner/module.js.map +1 -1
  22. package/dist/capabilities/repository-agent-workflow/module.d.ts.map +1 -1
  23. package/dist/capabilities/repository-agent-workflow/module.js +4 -17
  24. package/dist/capabilities/repository-agent-workflow/module.js.map +1 -1
  25. package/dist/capabilities/repository-security-baseline/adapters/outbound/filesystem/workflow-evidence-parser.d.ts.map +1 -1
  26. package/dist/capabilities/repository-security-baseline/adapters/outbound/filesystem/workflow-evidence-parser.js +33 -0
  27. package/dist/capabilities/repository-security-baseline/adapters/outbound/filesystem/workflow-evidence-parser.js.map +1 -1
  28. package/dist/capabilities/repository-security-baseline/application/model/repository-security.d.ts +1 -0
  29. package/dist/capabilities/repository-security-baseline/application/model/repository-security.d.ts.map +1 -1
  30. package/dist/capabilities/repository-security-baseline/application/model/repository-security.js.map +1 -1
  31. package/dist/capabilities/repository-security-baseline/application/policies/dependency-review-ordering.js +1 -1
  32. package/dist/capabilities/repository-security-baseline/application/policies/dependency-review-ordering.js.map +1 -1
  33. package/dist/capabilities/repository-security-baseline/application/policies/evaluate-repository-security-tools.js +1 -1
  34. package/dist/capabilities/repository-security-baseline/application/policies/evaluate-repository-security-tools.js.map +1 -1
  35. package/dist/capabilities/repository-security-baseline/application/policies/evaluate-repository-security.js +1 -1
  36. package/dist/capabilities/repository-security-baseline/application/policies/evaluate-repository-security.js.map +1 -1
  37. package/dist/capabilities/repository-security-baseline/module.d.ts.map +1 -1
  38. package/dist/capabilities/repository-security-baseline/module.js +4 -17
  39. package/dist/capabilities/repository-security-baseline/module.js.map +1 -1
  40. package/dist/capabilities/source-dependencies/module.d.ts.map +1 -1
  41. package/dist/capabilities/source-dependencies/module.js +4 -19
  42. package/dist/capabilities/source-dependencies/module.js.map +1 -1
  43. package/dist/capabilities/suppression-governance/module.d.ts.map +1 -1
  44. package/dist/capabilities/suppression-governance/module.js +4 -19
  45. package/dist/capabilities/suppression-governance/module.js.map +1 -1
  46. package/dist/capabilities/workspace-dependency-declarations/module.d.ts.map +1 -1
  47. package/dist/capabilities/workspace-dependency-declarations/module.js +4 -18
  48. package/dist/capabilities/workspace-dependency-declarations/module.js.map +1 -1
  49. package/dist/capability-runtime.d.ts +8 -0
  50. package/dist/capability-runtime.d.ts.map +1 -1
  51. package/dist/capability-runtime.js +66 -0
  52. package/dist/capability-runtime.js.map +1 -1
  53. package/dist/check-runner.d.ts.map +1 -1
  54. package/dist/check-runner.js +16 -10
  55. package/dist/check-runner.js.map +1 -1
  56. package/dist/document-authoring/index.d.ts +6 -1
  57. package/dist/document-authoring/index.d.ts.map +1 -1
  58. package/dist/document-authoring/index.js.map +1 -1
  59. package/dist/index.d.ts +2 -0
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +2 -0
  62. package/dist/index.js.map +1 -1
  63. package/dist/local-mode/index.d.ts +5 -0
  64. package/dist/local-mode/index.d.ts.map +1 -1
  65. package/dist/local-mode/index.js +5 -0
  66. package/dist/local-mode/index.js.map +1 -1
  67. package/dist/mutation/index.d.ts +8 -0
  68. package/dist/mutation/index.d.ts.map +1 -1
  69. package/dist/mutation/index.js.map +1 -1
  70. package/dist/mutation/qualification/index.d.ts +10 -0
  71. package/dist/mutation/qualification/index.d.ts.map +1 -0
  72. package/dist/mutation/qualification/index.js +6 -0
  73. package/dist/mutation/qualification/index.js.map +1 -0
  74. package/dist/repository-mutation/adapters/node/node-known-file-apply-effects.d.ts +84 -0
  75. package/dist/repository-mutation/adapters/node/node-known-file-apply-effects.d.ts.map +1 -0
  76. package/dist/repository-mutation/adapters/node/node-known-file-apply-effects.js +232 -0
  77. package/dist/repository-mutation/adapters/node/node-known-file-apply-effects.js.map +1 -0
  78. package/dist/repository-mutation/adapters/node/node-known-file-apply-faults.d.ts +9 -0
  79. package/dist/repository-mutation/adapters/node/node-known-file-apply-faults.d.ts.map +1 -0
  80. package/dist/repository-mutation/adapters/node/node-known-file-apply-faults.js +2 -0
  81. package/dist/repository-mutation/adapters/node/node-known-file-apply-faults.js.map +1 -0
  82. package/dist/repository-mutation/adapters/node/node-known-file-apply-observation.d.ts +6 -0
  83. package/dist/repository-mutation/adapters/node/node-known-file-apply-observation.d.ts.map +1 -0
  84. package/dist/repository-mutation/adapters/node/node-known-file-apply-observation.js +30 -0
  85. package/dist/repository-mutation/adapters/node/node-known-file-apply-observation.js.map +1 -0
  86. package/dist/repository-mutation/adapters/node/node-known-file-apply-operation.d.ts +11 -0
  87. package/dist/repository-mutation/adapters/node/node-known-file-apply-operation.d.ts.map +1 -0
  88. package/dist/repository-mutation/adapters/node/node-known-file-apply-operation.js +159 -0
  89. package/dist/repository-mutation/adapters/node/node-known-file-apply-operation.js.map +1 -0
  90. package/dist/repository-mutation/adapters/node/node-known-file-apply-state.d.ts +9 -0
  91. package/dist/repository-mutation/adapters/node/node-known-file-apply-state.d.ts.map +1 -0
  92. package/dist/repository-mutation/adapters/node/node-known-file-apply-state.js +21 -0
  93. package/dist/repository-mutation/adapters/node/node-known-file-apply-state.js.map +1 -0
  94. package/dist/repository-mutation/adapters/node/node-known-file-recovery-cleanup.d.ts +9 -0
  95. package/dist/repository-mutation/adapters/node/node-known-file-recovery-cleanup.d.ts.map +1 -0
  96. package/dist/repository-mutation/adapters/node/node-known-file-recovery-cleanup.js +203 -0
  97. package/dist/repository-mutation/adapters/node/node-known-file-recovery-cleanup.js.map +1 -0
  98. package/dist/repository-mutation/adapters/node/node-known-file-recovery-executor.d.ts +17 -0
  99. package/dist/repository-mutation/adapters/node/node-known-file-recovery-executor.d.ts.map +1 -0
  100. package/dist/repository-mutation/adapters/node/node-known-file-recovery-executor.js +22 -0
  101. package/dist/repository-mutation/adapters/node/node-known-file-recovery-executor.js.map +1 -0
  102. package/dist/repository-mutation/adapters/node/node-known-file-recovery-observation.d.ts +15 -0
  103. package/dist/repository-mutation/adapters/node/node-known-file-recovery-observation.d.ts.map +1 -0
  104. package/dist/repository-mutation/adapters/node/node-known-file-recovery-observation.js +47 -0
  105. package/dist/repository-mutation/adapters/node/node-known-file-recovery-observation.js.map +1 -0
  106. package/dist/repository-mutation/adapters/node/node-known-file-recovery-preimage-captured-cleanup.d.ts +29 -0
  107. package/dist/repository-mutation/adapters/node/node-known-file-recovery-preimage-captured-cleanup.d.ts.map +1 -0
  108. package/dist/repository-mutation/adapters/node/node-known-file-recovery-preimage-captured-cleanup.js +72 -0
  109. package/dist/repository-mutation/adapters/node/node-known-file-recovery-preimage-captured-cleanup.js.map +1 -0
  110. package/dist/repository-mutation/adapters/node/node-known-file-recovery-preimage-captured.d.ts +31 -0
  111. package/dist/repository-mutation/adapters/node/node-known-file-recovery-preimage-captured.d.ts.map +1 -0
  112. package/dist/repository-mutation/adapters/node/node-known-file-recovery-preimage-captured.js +254 -0
  113. package/dist/repository-mutation/adapters/node/node-known-file-recovery-preimage-captured.js.map +1 -0
  114. package/dist/repository-mutation/adapters/node/node-known-file-recovery-preimage.d.ts +13 -0
  115. package/dist/repository-mutation/adapters/node/node-known-file-recovery-preimage.d.ts.map +1 -0
  116. package/dist/repository-mutation/adapters/node/node-known-file-recovery-preimage.js +210 -0
  117. package/dist/repository-mutation/adapters/node/node-known-file-recovery-preimage.js.map +1 -0
  118. package/dist/repository-mutation/adapters/node/node-known-file-recovery-retirement.d.ts +15 -0
  119. package/dist/repository-mutation/adapters/node/node-known-file-recovery-retirement.d.ts.map +1 -0
  120. package/dist/repository-mutation/adapters/node/node-known-file-recovery-retirement.js +172 -0
  121. package/dist/repository-mutation/adapters/node/node-known-file-recovery-retirement.js.map +1 -0
  122. package/dist/repository-mutation/adapters/node/node-known-file-recovery-state.d.ts +12 -0
  123. package/dist/repository-mutation/adapters/node/node-known-file-recovery-state.d.ts.map +1 -0
  124. package/dist/repository-mutation/adapters/node/node-known-file-recovery-state.js +17 -0
  125. package/dist/repository-mutation/adapters/node/node-known-file-recovery-state.js.map +1 -0
  126. package/dist/repository-mutation/adapters/node/node-known-file-transaction-recovery.d.ts +2 -5
  127. package/dist/repository-mutation/adapters/node/node-known-file-transaction-recovery.d.ts.map +1 -1
  128. package/dist/repository-mutation/adapters/node/node-known-file-transaction-recovery.js +18 -860
  129. package/dist/repository-mutation/adapters/node/node-known-file-transaction-recovery.js.map +1 -1
  130. package/dist/repository-mutation/adapters/node/node-known-file-transaction.d.ts +3 -9
  131. package/dist/repository-mutation/adapters/node/node-known-file-transaction.d.ts.map +1 -1
  132. package/dist/repository-mutation/adapters/node/node-known-file-transaction.js +35 -451
  133. package/dist/repository-mutation/adapters/node/node-known-file-transaction.js.map +1 -1
  134. package/dist/repository-mutation/application/policies/classify-known-file-recovery-transition.d.ts +25 -0
  135. package/dist/repository-mutation/application/policies/classify-known-file-recovery-transition.d.ts.map +1 -0
  136. package/dist/repository-mutation/application/policies/classify-known-file-recovery-transition.js +19 -0
  137. package/dist/repository-mutation/application/policies/classify-known-file-recovery-transition.js.map +1 -0
  138. package/dist/unexpected-failure.d.ts +9 -0
  139. package/dist/unexpected-failure.d.ts.map +1 -0
  140. package/dist/unexpected-failure.js +105 -0
  141. package/dist/unexpected-failure.js.map +1 -0
  142. package/package.json +5 -1
@@ -1,835 +1,9 @@
1
- import { link, lstat, mkdir, readdir, rename, rmdir, unlink } from "node:fs/promises";
2
- import { basename, dirname, join } from "node:path";
3
- import { LOCAL_STATE_DIRECTORY } from "../../../foundation-state-contract.js";
4
- import { installedFoundationVersion } from "../../../package-version.js";
5
1
  import { createNodeFoundationTransactionCoordinator } from "../../../transaction-coordination/adapters/node/node-foundation-transaction-coordinator.js";
6
- import { installedFoundationBuildIdentity } from "../../../transaction-coordination/adapters/node/installed-foundation-build-identity.js";
7
2
  import { pruneFoundationStateDirectory } from "../../../transaction-coordination/adapters/node/node-foundation-state-directory.js";
8
- import { deserializeKnownFileIdentity, serializeKnownFileIdentity } from "../../application/model/known-file-transaction-journal.js";
9
- import { compileKnownFileTransactionEnvelope } from "../../application/policies/known-file-transaction-envelope.js";
10
- import { pathMatchesRegularFileIdentity, readBoundedRegularFile } from "./node-bounded-regular-file.js";
11
- import { syncDirectoryStrictly } from "./node-directory-durability.js";
12
- import { canonicalKnownFileRoot, knownFileErrorCode, knownFileTemporaryName, KnownFileTransactionError, matchesKnownFileImage, maximumKnownFileEvidenceBytes, observeKnownFile, sameKnownFileIdentity } from "./node-known-file-transaction-filesystem.js";
13
- import { cleanupCommittedKnownFileCaptures, knownFileCapturePaths, observeRecoveryFile, prepareRollbackTemporary } from "./node-known-file-recovery-filesystem.js";
14
- import { NodeKnownFileTransactionJournalStore } from "./node-known-file-transaction-journal-store.js";
15
- import { compileKnownFileTransactionReceipt, verifyCommittedKnownFilePostimages } from "./node-known-file-transaction.js";
16
- import { compileRolledBackReceipt } from "./node-known-file-transaction-receipt.js";
17
- async function persistRecoveryJournal(store, stored, journal) {
18
- const envelope = compileKnownFileTransactionEnvelope({
19
- foundation: stored.envelope.foundation,
20
- journal,
21
- state: stored.envelope.state
22
- });
23
- stored.authority = await store.replace(stored.authority, envelope);
24
- stored.envelope = envelope;
25
- }
26
- function replaceRecoveryOperation(journal, index, operation) {
27
- return Object.freeze({
28
- ...journal,
29
- operations: Object.freeze(journal.operations.with(index, operation))
30
- });
31
- }
32
- function clearOperationRetirement(operation) {
33
- if (operation.retirement === undefined) {
34
- return operation;
35
- }
36
- const { retirement: _retirement, ...cleared } = operation;
37
- return Object.freeze(cleared);
38
- }
39
- function retirementPath(options) {
40
- const leaf = basename(options.operation.path);
41
- const directory = join(options.parent, `.${leaf}.agent-teams.retire.${options.planDigest.slice(7, 23)}.${options.operationIndex}.${options.kind}`);
42
- const source = options.kind === "destination"
43
- ? join(options.parent, leaf)
44
- : options.kind === "temporary"
45
- ? join(options.parent, knownFileTemporaryName(options.operation.path, options.planDigest, options.operationIndex))
46
- : join(options.parent, `.${leaf}.agent-teams.rollback.${options.operationIndex}.tmp`);
47
- return { captured: join(directory, "captured"), directory, source };
48
- }
49
- // oxlint-disable-next-line complexity, max-lines-per-function
50
- async function retireJournalBoundPath(options) {
51
- const operation = options.stored.envelope.journal.plan.operations[options.operationIndex];
52
- const parent = dirname(join(options.root, ...operation.path.split("/")));
53
- const paths = retirementPath({
54
- kind: options.kind,
55
- operation,
56
- operationIndex: options.operationIndex,
57
- parent,
58
- planDigest: options.stored.envelope.journal.plan.planDigest
59
- });
60
- const initialJournalOperation = options.stored.envelope.journal.operations[options.operationIndex];
61
- if (!("temporaryIdentity" in initialJournalOperation)) {
62
- throw new KnownFileTransactionError("KNOWN_FILE_JOURNAL_INVALID", `Retirement requires durable operation identity: ${operation.path}.`);
63
- }
64
- let journalOperation = initialJournalOperation;
65
- let retirement = journalOperation.retirement;
66
- if (retirement === undefined) {
67
- if (await pathMatchesRegularFileIdentity(paths.source, options.expectedIdentity) !== "match") {
68
- throw new KnownFileTransactionError("KNOWN_FILE_RECOVERY_CONFLICT", `Identity-bound recovery path changed before retirement: ${operation.path}.`);
69
- }
70
- const directory = await mkdir(paths.directory, { mode: 0o700 }).then(() => lstat(paths.directory, { bigint: true })).catch((error) => {
71
- throw new KnownFileTransactionError("KNOWN_FILE_RECOVERY_CONFLICT", `Retirement directory could not be exclusively created: ${operation.path}.`, { cause: error });
72
- });
73
- await syncDirectoryStrictly(parent);
74
- const durableDirectory = await lstat(paths.directory, { bigint: true }).catch((error) => {
75
- throw new KnownFileTransactionError("KNOWN_FILE_RECOVERY_CONFLICT", `Retirement directory disappeared before durable identity binding: ${operation.path}.`, { cause: error });
76
- });
77
- if (!durableDirectory.isDirectory() || durableDirectory.isSymbolicLink() ||
78
- durableDirectory.birthtimeNs !== directory.birthtimeNs ||
79
- durableDirectory.dev !== directory.dev || durableDirectory.ino !== directory.ino) {
80
- throw new KnownFileTransactionError("KNOWN_FILE_RECOVERY_CONFLICT", `Retirement directory changed before durable identity binding: ${operation.path}.`);
81
- }
82
- retirement = Object.freeze({
83
- kind: options.kind,
84
- state: "ready",
85
- directoryIdentity: serializeKnownFileIdentity(directory),
86
- pathIdentity: serializeKnownFileIdentity(options.expectedIdentity)
87
- });
88
- journalOperation = Object.freeze({ ...journalOperation, retirement });
89
- await persistRecoveryJournal(options.store, options.stored, replaceRecoveryOperation(options.stored.envelope.journal, options.operationIndex, journalOperation));
90
- await options.faultInjector?.({
91
- phase: "after-retirement-directory-bound",
92
- operationIndex: options.operationIndex,
93
- path: operation.path
94
- });
95
- }
96
- if (retirement.kind !== options.kind ||
97
- !sameKnownFileIdentity(deserializeKnownFileIdentity(retirement.pathIdentity), options.expectedIdentity)) {
98
- throw new KnownFileTransactionError("KNOWN_FILE_RECOVERY_CONFLICT", `Retirement authority does not match the requested path: ${operation.path}.`);
99
- }
100
- const expectedDirectory = deserializeKnownFileIdentity(retirement.directoryIdentity);
101
- const directory = await lstat(paths.directory, { bigint: true }).catch((error) => {
102
- if (knownFileErrorCode(error) === "ENOENT") {
103
- return null;
104
- }
105
- throw error;
106
- });
107
- if (directory === null) {
108
- if (retirement.state !== "unlink-authorized" ||
109
- await pathMatchesRegularFileIdentity(paths.source, options.expectedIdentity) === "match") {
110
- throw new KnownFileTransactionError("KNOWN_FILE_RECOVERY_CONFLICT", `Journal-bound retirement directory disappeared: ${operation.path}.`);
111
- }
112
- const cleared = clearOperationRetirement(journalOperation);
113
- await persistRecoveryJournal(options.store, options.stored, replaceRecoveryOperation(options.stored.envelope.journal, options.operationIndex, cleared));
114
- return;
115
- }
116
- if (!directory.isDirectory() || directory.isSymbolicLink() ||
117
- directory.birthtimeNs !== expectedDirectory.birthtimeNs ||
118
- directory.dev !== expectedDirectory.dev || directory.ino !== expectedDirectory.ino) {
119
- throw new KnownFileTransactionError("KNOWN_FILE_RECOVERY_CONFLICT", `Journal-bound retirement directory changed: ${operation.path}.`);
120
- }
121
- const capturedMatch = await pathMatchesRegularFileIdentity(paths.captured, options.expectedIdentity);
122
- const sourceMatch = await pathMatchesRegularFileIdentity(paths.source, options.expectedIdentity);
123
- if (retirement.state === "ready") {
124
- if (capturedMatch === "missing" && sourceMatch === "match") {
125
- await rename(paths.source, paths.captured);
126
- await syncDirectoryStrictly(paths.directory);
127
- await syncDirectoryStrictly(parent);
128
- }
129
- else if (capturedMatch !== "match" || sourceMatch === "match") {
130
- throw new KnownFileTransactionError("KNOWN_FILE_RECOVERY_CONFLICT", `Journal-bound retirement capture is ambiguous: ${operation.path}.`);
131
- }
132
- retirement = Object.freeze({ ...retirement, state: "captured" });
133
- journalOperation = Object.freeze({ ...journalOperation, retirement });
134
- await persistRecoveryJournal(options.store, options.stored, replaceRecoveryOperation(options.stored.envelope.journal, options.operationIndex, journalOperation));
135
- await options.faultInjector?.({
136
- phase: "after-retirement-captured",
137
- operationIndex: options.operationIndex,
138
- path: operation.path
139
- });
140
- }
141
- if (retirement.state === "captured") {
142
- if (await pathMatchesRegularFileIdentity(paths.captured, options.expectedIdentity) !== "match") {
143
- throw new KnownFileTransactionError("KNOWN_FILE_RECOVERY_CONFLICT", `Journal-bound retired bytes changed: ${operation.path}.`);
144
- }
145
- retirement = Object.freeze({ ...retirement, state: "unlink-authorized" });
146
- journalOperation = Object.freeze({ ...journalOperation, retirement });
147
- await persistRecoveryJournal(options.store, options.stored, replaceRecoveryOperation(options.stored.envelope.journal, options.operationIndex, journalOperation));
148
- await options.faultInjector?.({
149
- phase: "after-retirement-unlink-authorized",
150
- operationIndex: options.operationIndex,
151
- path: operation.path
152
- });
153
- }
154
- const capturedAfterAuthorization = await pathMatchesRegularFileIdentity(paths.captured, options.expectedIdentity);
155
- if (capturedAfterAuthorization === "match") {
156
- await unlink(paths.captured);
157
- await syncDirectoryStrictly(paths.directory);
158
- }
159
- else if (capturedAfterAuthorization !== "missing") {
160
- throw new KnownFileTransactionError("KNOWN_FILE_RECOVERY_CONFLICT", `Authorized retirement capture became foreign: ${operation.path}.`);
161
- }
162
- if ((await readdir(paths.directory)).length !== 0) {
163
- throw new KnownFileTransactionError("KNOWN_FILE_RECOVERY_CONFLICT", `Retirement directory contains foreign evidence: ${operation.path}.`);
164
- }
165
- await rmdir(paths.directory);
166
- await syncDirectoryStrictly(parent);
167
- const cleared = clearOperationRetirement(journalOperation);
168
- await persistRecoveryJournal(options.store, options.stored, replaceRecoveryOperation(options.stored.envelope.journal, options.operationIndex, cleared));
169
- }
170
- async function restoreAbsentPreimage(options) {
171
- if (options.observed.state === "absent") {
172
- return;
173
- }
174
- if (!matchesKnownFileImage(options.observed, options.operation.postimage) ||
175
- options.observed.identity === undefined ||
176
- !("temporaryIdentity" in options.journalOperation) ||
177
- !sameKnownFileIdentity(options.observed.identity, deserializeKnownFileIdentity(options.journalOperation.temporaryIdentity))) {
178
- throw new KnownFileTransactionError("KNOWN_FILE_RECOVERY_CONFLICT", `Created destination changed after publication: ${options.operation.path}.`);
179
- }
180
- await retireJournalBoundPath({
181
- expectedIdentity: options.observed.identity,
182
- ...(options.faultInjector === undefined ? {} : { faultInjector: options.faultInjector }),
183
- kind: "destination",
184
- operationIndex: options.operationIndex,
185
- root: options.root,
186
- store: options.store,
187
- stored: options.stored
188
- });
189
- }
190
- async function acceptRestoredPreimage(options) {
191
- if (!matchesKnownFileImage(options.observed, options.preimage)) {
192
- return false;
193
- }
194
- if (options.journalOperation.rollbackTemporaryIdentity !== undefined &&
195
- options.observed.identity !== undefined &&
196
- sameKnownFileIdentity(options.observed.identity, deserializeKnownFileIdentity(options.journalOperation.rollbackTemporaryIdentity))) {
197
- await retireJournalBoundPath({
198
- expectedIdentity: options.observed.identity,
199
- kind: "rollback-temporary",
200
- operationIndex: options.operationIndex,
201
- root: options.root,
202
- store: options.store,
203
- stored: options.stored
204
- });
205
- }
206
- return true;
207
- }
208
- function assertPublishedIdentity(options) {
209
- if (options.observed.identity === undefined ||
210
- !sameKnownFileIdentity(options.observed.identity, deserializeKnownFileIdentity(options.journalOperation.temporaryIdentity))) {
211
- throw new KnownFileTransactionError("KNOWN_FILE_RECOVERY_CONFLICT", `Published destination identity changed before rollback: ${options.operationPath}.`);
212
- }
213
- }
214
- // oxlint-disable-next-line complexity, max-lines-per-function
215
- async function restorePreimage(options) {
216
- const operation = options.operation;
217
- let journalOperation = options.stored.envelope.journal.operations[options.operationIndex];
218
- const destination = join(options.root, ...operation.path.split("/"));
219
- const parent = dirname(destination);
220
- if (operation.precondition.state === "absent") {
221
- const observed = await observeRecoveryFile(destination, maximumKnownFileEvidenceBytes(operation));
222
- await restoreAbsentPreimage({
223
- ...(options.faultInjector === undefined ? {} : { faultInjector: options.faultInjector }),
224
- journalOperation,
225
- observed,
226
- operation,
227
- operationIndex: options.operationIndex,
228
- root: options.root,
229
- store: options.store,
230
- stored: options.stored
231
- });
232
- return;
233
- }
234
- if (journalOperation.captureDirectoryIdentity !== undefined) {
235
- await restoreCapturedPreimage({
236
- ...(options.faultInjector === undefined ? {} : { faultInjector: options.faultInjector }),
237
- journalOperation: journalOperation,
238
- operation: { ...operation, precondition: operation.precondition },
239
- operationIndex: options.operationIndex,
240
- planDigest: options.stored.envelope.journal.plan.planDigest,
241
- root: options.root,
242
- store: options.store,
243
- stored: options.stored
244
- });
245
- return;
246
- }
247
- const observed = await observeRecoveryFile(destination, maximumKnownFileEvidenceBytes(operation));
248
- const postMatches = matchesKnownFileImage(observed, operation.postimage);
249
- if (!("temporaryIdentity" in journalOperation)) {
250
- throw new KnownFileTransactionError("KNOWN_FILE_JOURNAL_INVALID", `Published operation lacks temporary identity authority: ${operation.path}.`);
251
- }
252
- const matched = journalOperation.matchedPreimage;
253
- if (matched === undefined) {
254
- throw new KnownFileTransactionError("KNOWN_FILE_JOURNAL_INVALID", `Replacement preimage binding is absent: ${operation.path}.`);
255
- }
256
- const preimage = operation.precondition.acceptedPreimages[matched];
257
- if (await acceptRestoredPreimage({
258
- journalOperation,
259
- observed,
260
- operationIndex: options.operationIndex,
261
- operationPath: operation.path,
262
- preimage,
263
- root: options.root,
264
- store: options.store,
265
- stored: options.stored
266
- })) {
267
- return;
268
- }
269
- const resumingAfterRetirement = observed.state === "absent" &&
270
- journalOperation.rollbackTemporaryIdentity !== undefined;
271
- if (!postMatches && !resumingAfterRetirement) {
272
- throw new KnownFileTransactionError("KNOWN_FILE_RECOVERY_CONFLICT", `Published destination changed before rollback: ${operation.path}.`);
273
- }
274
- if (postMatches) {
275
- assertPublishedIdentity({ journalOperation, observed, operationPath: operation.path });
276
- }
277
- const rollbackPath = join(parent, `.${basename(operation.path)}.agent-teams.rollback.${options.operationIndex}.tmp`);
278
- const rollbackIdentity = await prepareRollbackTemporary({
279
- ...(journalOperation.rollbackTemporaryIdentity === undefined
280
- ? {}
281
- : {
282
- expectedIdentity: deserializeKnownFileIdentity(journalOperation.rollbackTemporaryIdentity)
283
- }),
284
- operationPath: operation.path,
285
- path: rollbackPath,
286
- preimage
287
- });
288
- await syncDirectoryStrictly(parent);
289
- if (await pathMatchesRegularFileIdentity(rollbackPath, rollbackIdentity) !== "match") {
290
- throw new KnownFileTransactionError("KNOWN_FILE_RECOVERY_CONFLICT", `Rollback temporary changed before durable identity binding: ${operation.path}.`);
291
- }
292
- if (journalOperation.rollbackTemporaryIdentity === undefined) {
293
- journalOperation = Object.freeze({
294
- ...journalOperation,
295
- rollbackTemporaryIdentity: serializeKnownFileIdentity(rollbackIdentity)
296
- });
297
- const journal = Object.freeze({
298
- ...options.stored.envelope.journal,
299
- operations: Object.freeze(options.stored.envelope.journal.operations.with(options.operationIndex, journalOperation))
300
- });
301
- await persistRecoveryJournal(options.store, options.stored, journal);
302
- }
303
- if (!resumingAfterRetirement) {
304
- await retireJournalBoundPath({
305
- expectedIdentity: observed.identity,
306
- ...(options.faultInjector === undefined ? {} : { faultInjector: options.faultInjector }),
307
- kind: "destination",
308
- operationIndex: options.operationIndex,
309
- root: options.root,
310
- store: options.store,
311
- stored: options.stored
312
- });
313
- await options.faultInjector?.({
314
- phase: "after-destination-retired",
315
- operationIndex: options.operationIndex,
316
- path: operation.path
317
- });
318
- }
319
- const prepared = await readBoundedRegularFile(rollbackPath, preimage.size);
320
- if (prepared.outcome !== "read" ||
321
- !sameKnownFileIdentity(prepared.identity, rollbackIdentity) ||
322
- !matchesKnownFileImage({
323
- state: "file", bytes: prepared.bytes, identity: prepared.identity,
324
- mode: prepared.mode & 0o777
325
- }, preimage)) {
326
- throw new KnownFileTransactionError("KNOWN_FILE_RECOVERY_CONFLICT", `Rollback temporary changed before copy: ${operation.path}.`);
327
- }
328
- await link(rollbackPath, destination).catch((error) => {
329
- throw new KnownFileTransactionError("KNOWN_FILE_RECOVERY_CONFLICT", `Destination appeared during rollback CAS: ${operation.path}.`, { cause: error });
330
- });
331
- await options.faultInjector?.({
332
- phase: "after-rollback-linked",
333
- operationIndex: options.operationIndex,
334
- path: operation.path
335
- });
336
- await syncDirectoryStrictly(parent);
337
- const restored = await observeRecoveryFile(destination, preimage.size);
338
- if (!matchesKnownFileImage(restored, preimage) || restored.identity === undefined ||
339
- !sameKnownFileIdentity(restored.identity, rollbackIdentity)) {
340
- throw new KnownFileTransactionError("KNOWN_FILE_RECOVERY_FAILED", `Preimage restoration failed: ${operation.path}.`);
341
- }
342
- await retireJournalBoundPath({
343
- expectedIdentity: rollbackIdentity,
344
- ...(options.faultInjector === undefined ? {} : { faultInjector: options.faultInjector }),
345
- kind: "rollback-temporary",
346
- operationIndex: options.operationIndex,
347
- root: options.root,
348
- store: options.store,
349
- stored: options.stored
350
- });
351
- }
352
- // oxlint-disable-next-line complexity, max-lines-per-function
353
- async function restoreCapturedPreimage(options) {
354
- const destination = join(options.root, ...options.operation.path.split("/"));
355
- const parent = dirname(destination);
356
- const paths = knownFileCapturePaths({
357
- operationIndex: options.operationIndex,
358
- operationPath: options.operation.path,
359
- parent,
360
- planDigest: options.planDigest
361
- });
362
- const recoveryMaximumBytes = Math.max(maximumKnownFileEvidenceBytes(options.operation), 8 * 1024 * 1024);
363
- if (options.journalOperation.state === "rollback-restored") {
364
- const matched = options.journalOperation.matchedPreimage;
365
- if (matched === undefined) {
366
- throw new KnownFileTransactionError("KNOWN_FILE_JOURNAL_INVALID", `Restored replacement lacks exact preimage authority: ${options.operation.path}.`);
367
- }
368
- const preimage = options.operation.precondition.acceptedPreimages[matched];
369
- const destinationObserved = await observeRecoveryFile(destination, recoveryMaximumBytes);
370
- if (!matchesKnownFileImage(destinationObserved, preimage)) {
371
- throw new KnownFileTransactionError("KNOWN_FILE_RECOVERY_CONFLICT", `Restored destination changed during cleanup: ${options.operation.path}.`);
372
- }
373
- const rollbackPath = join(parent, `.${basename(options.operation.path)}.agent-teams.rollback.${options.operationIndex}.tmp`);
374
- if (options.journalOperation.rollbackTemporaryIdentity !== undefined) {
375
- const rollbackObserved = await observeRecoveryFile(rollbackPath, preimage.size);
376
- if (rollbackObserved.state === "file") {
377
- const expectedRollback = deserializeKnownFileIdentity(options.journalOperation.rollbackTemporaryIdentity);
378
- if (rollbackObserved.identity === undefined ||
379
- !sameKnownFileIdentity(rollbackObserved.identity, expectedRollback) ||
380
- !matchesKnownFileImage(rollbackObserved, preimage)) {
381
- throw new KnownFileTransactionError("KNOWN_FILE_RECOVERY_CONFLICT", `Rollback copy changed during cleanup: ${options.operation.path}.`);
382
- }
383
- await unlink(rollbackPath);
384
- await syncDirectoryStrictly(parent);
385
- }
386
- }
387
- const directory = await lstat(paths.directory, { bigint: true }).catch((error) => {
388
- if (knownFileErrorCode(error) === "ENOENT") {
389
- return null;
390
- }
391
- throw error;
392
- });
393
- if (directory === null) {
394
- return;
395
- }
396
- const expectedDirectory = deserializeKnownFileIdentity(options.journalOperation.captureDirectoryIdentity);
397
- if (!directory.isDirectory() || directory.isSymbolicLink() ||
398
- directory.birthtimeNs !== expectedDirectory.birthtimeNs ||
399
- directory.dev !== expectedDirectory.dev || directory.ino !== expectedDirectory.ino) {
400
- throw new KnownFileTransactionError("KNOWN_FILE_RECOVERY_CONFLICT", `Rollback capture directory changed during cleanup: ${options.operation.path}.`);
401
- }
402
- const capturedObserved = await observeRecoveryFile(paths.captured, recoveryMaximumBytes);
403
- if (capturedObserved.state === "file") {
404
- const expectedCaptured = options.journalOperation.capturedPreimageIdentity;
405
- if (expectedCaptured === undefined || capturedObserved.identity === undefined ||
406
- !sameKnownFileIdentity(capturedObserved.identity, deserializeKnownFileIdentity(expectedCaptured)) || !matchesKnownFileImage(capturedObserved, preimage)) {
407
- throw new KnownFileTransactionError("KNOWN_FILE_RECOVERY_CONFLICT", `Rollback capture changed during cleanup: ${options.operation.path}.`);
408
- }
409
- await unlink(paths.captured);
410
- await syncDirectoryStrictly(paths.directory);
411
- await options.faultInjector?.({
412
- phase: "after-rollback-capture-unlinked",
413
- operationIndex: options.operationIndex,
414
- path: options.operation.path
415
- });
416
- }
417
- const retiredObserved = await observeRecoveryFile(paths.retired, recoveryMaximumBytes);
418
- if (retiredObserved.state !== "absent") {
419
- throw new KnownFileTransactionError("KNOWN_FILE_RECOVERY_CONFLICT", `Rollback capture retains foreign retired evidence: ${options.operation.path}.`);
420
- }
421
- await rmdir(paths.directory);
422
- await syncDirectoryStrictly(parent);
423
- return;
424
- }
425
- const expectedDirectory = deserializeKnownFileIdentity(options.journalOperation.captureDirectoryIdentity);
426
- const directory = await lstat(paths.directory, { bigint: true }).catch((error) => {
427
- if (knownFileErrorCode(error) === "ENOENT") {
428
- return null;
429
- }
430
- throw error;
431
- });
432
- if (directory === null || !directory.isDirectory() || directory.isSymbolicLink() ||
433
- directory.birthtimeNs !== expectedDirectory.birthtimeNs ||
434
- directory.dev !== expectedDirectory.dev || directory.ino !== expectedDirectory.ino) {
435
- throw new KnownFileTransactionError("KNOWN_FILE_RECOVERY_CONFLICT", `Replacement capture directory changed: ${options.operation.path}.`);
436
- }
437
- const captured = await observeRecoveryFile(paths.captured, recoveryMaximumBytes);
438
- const destinationBefore = await observeRecoveryFile(destination, recoveryMaximumBytes);
439
- const retired = await observeRecoveryFile(paths.retired, recoveryMaximumBytes);
440
- const matched = options.journalOperation.matchedPreimage;
441
- if (matched === undefined) {
442
- throw new KnownFileTransactionError("KNOWN_FILE_JOURNAL_INVALID", `Captured replacement lacks its exact preimage binding: ${options.operation.path}.`);
443
- }
444
- const preimage = options.operation.precondition.acceptedPreimages[matched];
445
- if (options.journalOperation.capturedPreimageIdentity === undefined) {
446
- if (retired.state !== "absent" || captured.state !== "absent") {
447
- throw new KnownFileTransactionError("KNOWN_FILE_RECOVERY_CONFLICT", `Replacement capture contains bytes without durable identity authority: ${options.operation.path}.`);
448
- }
449
- const stableDestination = await readBoundedRegularFile(destination, preimage.size);
450
- if (stableDestination.outcome !== "read" || stableDestination.linkCount !== 1n ||
451
- !matchesKnownFileImage({
452
- state: "file",
453
- bytes: stableDestination.bytes,
454
- identity: stableDestination.identity,
455
- mode: stableDestination.mode & 0o777
456
- }, preimage)) {
457
- throw new KnownFileTransactionError("KNOWN_FILE_RECOVERY_CONFLICT", `Uncaptured replacement preimage is not an exclusive exact file: ${options.operation.path}.`);
458
- }
459
- const restoredOperation = Object.freeze({
460
- ...options.journalOperation,
461
- state: "rollback-restored"
462
- });
463
- await persistRecoveryJournal(options.store, options.stored, replaceRecoveryOperation(options.stored.envelope.journal, options.operationIndex, restoredOperation));
464
- await rmdir(paths.directory);
465
- await syncDirectoryStrictly(parent);
466
- return;
467
- }
468
- const capturedIdentity = deserializeKnownFileIdentity(options.journalOperation.capturedPreimageIdentity);
469
- const captureIdentityMatches = captured.state === "file" && captured.identity !== undefined &&
470
- sameKnownFileIdentity(captured.identity, capturedIdentity);
471
- if (!captureIdentityMatches) {
472
- throw new KnownFileTransactionError("KNOWN_FILE_RECOVERY_CONFLICT", `Captured replacement identity changed: ${options.operation.path}.`);
473
- }
474
- const captureBytesStable = matchesKnownFileImage(captured, preimage);
475
- const rollbackPath = join(parent, `.${basename(options.operation.path)}.agent-teams.rollback.${options.operationIndex}.tmp`);
476
- const rollbackIdentity = options.journalOperation.rollbackTemporaryIdentity === undefined
477
- ? undefined
478
- : deserializeKnownFileIdentity(options.journalOperation.rollbackTemporaryIdentity);
479
- const rollbackObserved = rollbackIdentity === undefined
480
- ? { state: "absent" }
481
- : await observeRecoveryFile(rollbackPath, preimage.size);
482
- if (rollbackIdentity !== undefined &&
483
- (rollbackObserved.state !== "file" || rollbackObserved.identity === undefined ||
484
- !sameKnownFileIdentity(rollbackObserved.identity, rollbackIdentity) ||
485
- !matchesKnownFileImage(rollbackObserved, preimage))) {
486
- throw new KnownFileTransactionError("KNOWN_FILE_RECOVERY_CONFLICT", `Rollback preimage copy changed: ${options.operation.path}.`);
487
- }
488
- if (rollbackIdentity === undefined && !captureBytesStable) {
489
- throw new KnownFileTransactionError("KNOWN_FILE_RECOVERY_CONFLICT", `Captured preimage changed before a stable rollback copy existed: ${options.operation.path}.`);
490
- }
491
- const rollbackSource = rollbackIdentity === undefined ? paths.captured : rollbackPath;
492
- const rollbackSourceIdentity = rollbackIdentity ?? capturedIdentity;
493
- if (retired.state === "file") {
494
- if (retired.identity !== undefined &&
495
- sameKnownFileIdentity(retired.identity, capturedIdentity)) {
496
- await unlink(paths.retired);
497
- await syncDirectoryStrictly(paths.directory);
498
- }
499
- else {
500
- if (destinationBefore.state === "absent") {
501
- await link(paths.retired, destination);
502
- await syncDirectoryStrictly(parent);
503
- await unlink(paths.retired);
504
- await syncDirectoryStrictly(paths.directory);
505
- }
506
- throw new KnownFileTransactionError("KNOWN_FILE_RECOVERY_CONFLICT", `Foreign destination was preserved from an interrupted capture: ${options.operation.path}.`);
507
- }
508
- }
509
- let observed = await observeRecoveryFile(destination, recoveryMaximumBytes);
510
- if (matchesKnownFileImage(observed, preimage)) {
511
- // A foreign exact preimage is already the correct rollback result. Never
512
- // replace it merely to recover the original inode.
513
- if (observed.identity === undefined ||
514
- !sameKnownFileIdentity(observed.identity, rollbackSourceIdentity)) {
515
- const restoredJournalOperation = Object.freeze({
516
- ...options.journalOperation,
517
- state: "rollback-restored"
518
- });
519
- await persistRecoveryJournal(options.store, options.stored, Object.freeze({
520
- ...options.stored.envelope.journal,
521
- operations: Object.freeze(options.stored.envelope.journal.operations.with(options.operationIndex, restoredJournalOperation))
522
- }));
523
- if (rollbackIdentity !== undefined) {
524
- await unlink(rollbackPath);
525
- }
526
- if (captureBytesStable) {
527
- await unlink(paths.captured);
528
- }
529
- await syncDirectoryStrictly(paths.directory);
530
- if (captureBytesStable) {
531
- await options.faultInjector?.({
532
- phase: "after-rollback-capture-unlinked",
533
- operationIndex: options.operationIndex,
534
- path: options.operation.path
535
- });
536
- }
537
- if (captureBytesStable) {
538
- await rmdir(paths.directory);
539
- }
540
- await syncDirectoryStrictly(parent);
541
- if (!captureBytesStable) {
542
- throw new KnownFileTransactionError("KNOWN_FILE_RECOVERY_CONFLICT", `Edited retired inode was preserved after exact rollback: ${options.operation.path}.`);
543
- }
544
- return;
545
- }
546
- }
547
- else if (observed.state === "file") {
548
- const temporaryIdentity = deserializeKnownFileIdentity(options.journalOperation.temporaryIdentity);
549
- if (!matchesKnownFileImage(observed, options.operation.postimage) ||
550
- observed.identity === undefined ||
551
- !sameKnownFileIdentity(observed.identity, temporaryIdentity)) {
552
- throw new KnownFileTransactionError("KNOWN_FILE_RECOVERY_CONFLICT", `Destination changed before captured-preimage rollback: ${options.operation.path}.`);
553
- }
554
- await retireJournalBoundPath({
555
- expectedIdentity: temporaryIdentity,
556
- ...(options.faultInjector === undefined ? {} : { faultInjector: options.faultInjector }),
557
- kind: "destination",
558
- operationIndex: options.operationIndex,
559
- root: options.root,
560
- store: options.store,
561
- stored: options.stored
562
- });
563
- await options.faultInjector?.({
564
- phase: "after-destination-retired",
565
- operationIndex: options.operationIndex,
566
- path: options.operation.path
567
- });
568
- observed = { state: "absent" };
569
- }
570
- if (observed.state === "absent") {
571
- await link(rollbackSource, destination).catch((error) => {
572
- throw new KnownFileTransactionError("KNOWN_FILE_RECOVERY_CONFLICT", `Destination appeared during captured-preimage rollback: ${options.operation.path}.`, { cause: error });
573
- });
574
- await options.faultInjector?.({
575
- phase: "after-rollback-linked",
576
- operationIndex: options.operationIndex,
577
- path: options.operation.path
578
- });
579
- await syncDirectoryStrictly(parent);
580
- }
581
- const restored = await observeRecoveryFile(destination, preimage.size);
582
- if (!matchesKnownFileImage(restored, preimage) || restored.identity === undefined ||
583
- !sameKnownFileIdentity(restored.identity, rollbackSourceIdentity)) {
584
- throw new KnownFileTransactionError("KNOWN_FILE_RECOVERY_FAILED", `Captured preimage restoration failed: ${options.operation.path}.`);
585
- }
586
- const restoredJournalOperation = Object.freeze({
587
- ...options.journalOperation,
588
- state: "rollback-restored"
589
- });
590
- const restoredJournal = Object.freeze({
591
- ...options.stored.envelope.journal,
592
- operations: Object.freeze(options.stored.envelope.journal.operations.with(options.operationIndex, restoredJournalOperation))
593
- });
594
- await persistRecoveryJournal(options.store, options.stored, restoredJournal);
595
- if (rollbackIdentity !== undefined) {
596
- await unlink(rollbackPath);
597
- }
598
- if (captureBytesStable) {
599
- await unlink(paths.captured);
600
- }
601
- await syncDirectoryStrictly(paths.directory);
602
- if (captureBytesStable) {
603
- await options.faultInjector?.({
604
- phase: "after-rollback-capture-unlinked",
605
- operationIndex: options.operationIndex,
606
- path: options.operation.path
607
- });
608
- }
609
- if (captureBytesStable) {
610
- await rmdir(paths.directory);
611
- }
612
- await syncDirectoryStrictly(parent);
613
- if (!captureBytesStable) {
614
- throw new KnownFileTransactionError("KNOWN_FILE_RECOVERY_CONFLICT", `Edited retired inode was preserved after exact rollback: ${options.operation.path}.`);
615
- }
616
- }
617
- async function cleanupOperationTemporaries(options) {
618
- const operation = options.stored.envelope.journal.plan.operations[options.operationIndex];
619
- const journalOperation = options.stored.envelope.journal.operations[options.operationIndex];
620
- const parent = dirname(join(options.root, ...operation.path.split("/")));
621
- const candidates = [
622
- {
623
- identity: "temporaryIdentity" in journalOperation
624
- ? deserializeKnownFileIdentity(journalOperation.temporaryIdentity)
625
- : undefined,
626
- image: operation.postimage,
627
- kind: "temporary",
628
- authorizedWithoutIdentity: journalOperation.state === "temporary-authorized",
629
- path: join(parent, knownFileTemporaryName(operation.path, options.stored.envelope.journal.plan.planDigest, options.operationIndex))
630
- },
631
- ...(operation.precondition.state === "known-file" &&
632
- journalOperation.matchedPreimage !== undefined
633
- ? [{
634
- identity: journalOperation.rollbackTemporaryIdentity === undefined
635
- ? undefined
636
- : deserializeKnownFileIdentity(journalOperation.rollbackTemporaryIdentity),
637
- image: operation.precondition.acceptedPreimages[journalOperation.matchedPreimage],
638
- kind: "rollback-temporary",
639
- path: join(parent, `.${basename(operation.path)}.agent-teams.rollback.${options.operationIndex}.tmp`)
640
- }]
641
- : [])
642
- ];
643
- for (const candidate of candidates) {
644
- let observed;
645
- try {
646
- observed = await readBoundedRegularFile(candidate.path, candidate.image.size);
647
- }
648
- catch (error) {
649
- if (knownFileErrorCode(error) === "ENOENT") {
650
- continue;
651
- }
652
- throw error;
653
- }
654
- if (observed.outcome !== "read" || !matchesKnownFileImage({
655
- state: "file", bytes: observed.bytes, identity: observed.identity,
656
- mode: observed.mode & 0o777
657
- }, candidate.image)) {
658
- throw new KnownFileTransactionError("KNOWN_FILE_RECOVERY_CONFLICT", `Transaction temporary is foreign or modified: ${operation.path}.`);
659
- }
660
- if (candidate.identity === undefined && candidate.authorizedWithoutIdentity === true) {
661
- throw new KnownFileTransactionError("KNOWN_FILE_RECOVERY_CONFLICT", `Authorized temporary exists without durable ownership identity: ${operation.path}.`);
662
- }
663
- if (candidate.identity === undefined ||
664
- !sameKnownFileIdentity(observed.identity, candidate.identity)) {
665
- throw new KnownFileTransactionError("KNOWN_FILE_RECOVERY_CONFLICT", `Transaction temporary identity is absent or changed: ${operation.path}.`);
666
- }
667
- await retireJournalBoundPath({
668
- expectedIdentity: candidate.identity,
669
- kind: candidate.kind,
670
- operationIndex: options.operationIndex,
671
- root: options.root,
672
- store: options.store,
673
- stored: options.stored
674
- });
675
- }
676
- }
677
- async function cleanupAuthorizedCapture(options) {
678
- const operation = options.journal.plan.operations[options.operationIndex];
679
- const parent = dirname(join(options.root, ...operation.path.split("/")));
680
- const paths = knownFileCapturePaths({
681
- operationIndex: options.operationIndex,
682
- operationPath: operation.path,
683
- parent,
684
- planDigest: options.journal.plan.planDigest
685
- });
686
- const metadata = await lstat(paths.directory).catch((error) => {
687
- if (knownFileErrorCode(error) === "ENOENT") {
688
- return null;
689
- }
690
- throw error;
691
- });
692
- if (metadata === null) {
693
- return;
694
- }
695
- if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
696
- throw new KnownFileTransactionError("KNOWN_FILE_RECOVERY_CONFLICT", `Authorized capture path is foreign: ${operation.path}.`);
697
- }
698
- throw new KnownFileTransactionError("KNOWN_FILE_RECOVERY_CONFLICT", `Authorized capture exists without durable ownership identity: ${operation.path}.`);
699
- }
700
- async function removeAuthorizedDirectories(root, directories) {
701
- for (const repositoryPath of directories.toReversed()) {
702
- const path = join(root, ...repositoryPath.split("/"));
703
- const metadata = await lstat(path).catch((error) => {
704
- if (knownFileErrorCode(error) === "ENOENT") {
705
- return null;
706
- }
707
- throw error;
708
- });
709
- if (metadata === null) {
710
- continue;
711
- }
712
- if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
713
- throw new KnownFileTransactionError("KNOWN_FILE_RECOVERY_CONFLICT", `Authorized directory path is foreign: ${repositoryPath}.`);
714
- }
715
- throw new KnownFileTransactionError("KNOWN_FILE_RECOVERY_CONFLICT", `Authorized directory exists without durable ownership identity: ${repositoryPath}.`);
716
- }
717
- }
718
- async function removeCreatedDirectories(root, directories) {
719
- for (const directory of directories.toReversed()) {
720
- const path = join(root, ...directory.path.split("/"));
721
- let metadata;
722
- try {
723
- metadata = await lstat(path, { bigint: true });
724
- }
725
- catch (error) {
726
- if (knownFileErrorCode(error) === "ENOENT") {
727
- continue;
728
- }
729
- throw error;
730
- }
731
- const expected = deserializeKnownFileIdentity(directory.identity);
732
- if (!metadata.isDirectory() || metadata.isSymbolicLink() ||
733
- metadata.dev !== expected.dev || metadata.ino !== expected.ino ||
734
- metadata.birthtimeNs !== expected.birthtimeNs) {
735
- throw new KnownFileTransactionError("KNOWN_FILE_RECOVERY_CONFLICT", `Transaction-created directory changed before cleanup: ${directory.path}.`);
736
- }
737
- try {
738
- await rmdir(path);
739
- await syncDirectoryStrictly(dirname(path));
740
- }
741
- catch (error) {
742
- if (knownFileErrorCode(error) !== "ENOTEMPTY") {
743
- throw error;
744
- }
745
- }
746
- }
747
- }
748
- async function rollback(options) {
749
- for (let index = options.stored.envelope.journal.operations.length - 1; index >= 0; index -= 1) {
750
- let journalOperation = options.stored.envelope.journal.operations[index];
751
- if (journalOperation.retirement !== undefined) {
752
- await retireJournalBoundPath({
753
- expectedIdentity: deserializeKnownFileIdentity(journalOperation.retirement.pathIdentity),
754
- ...(options.faultInjector === undefined ? {} : { faultInjector: options.faultInjector }),
755
- kind: journalOperation.retirement.kind,
756
- operationIndex: index,
757
- root: options.root,
758
- store: options.store,
759
- stored: options.stored
760
- });
761
- journalOperation = options.stored.envelope.journal.operations[index];
762
- }
763
- if (journalOperation.state === "capture-authorized") {
764
- await cleanupAuthorizedCapture({
765
- journal: options.stored.envelope.journal,
766
- operationIndex: index,
767
- root: options.root
768
- });
769
- continue;
770
- }
771
- if (![
772
- "capture-ready", "preimage-captured", "destination-retired",
773
- "publishing", "published", "rollback-restored"
774
- ].includes(journalOperation.state)) {
775
- continue;
776
- }
777
- await restorePreimage({
778
- ...(options.faultInjector === undefined ? {} : { faultInjector: options.faultInjector }),
779
- operation: options.stored.envelope.journal.plan.operations[index],
780
- operationIndex: index,
781
- root: options.root,
782
- store: options.store,
783
- stored: options.stored
784
- });
785
- await cleanupOperationTemporaries({
786
- operationIndex: index,
787
- root: options.root,
788
- store: options.store,
789
- stored: options.stored
790
- });
791
- }
792
- for (let index = options.stored.envelope.journal.operations.length - 1; index >= 0; index -= 1) {
793
- const journal = options.stored.envelope.journal;
794
- if (![
795
- "capture-ready", "preimage-captured", "destination-retired",
796
- "publishing", "published", "rollback-restored"
797
- ].includes(journal.operations[index].state)) {
798
- await cleanupOperationTemporaries({
799
- operationIndex: index,
800
- root: options.root,
801
- store: options.store,
802
- stored: options.stored
803
- });
804
- }
805
- }
806
- const journal = options.stored.envelope.journal;
807
- await removeCreatedDirectories(options.root, journal.createdDirectories);
808
- await removeAuthorizedDirectories(options.root, journal.authorizedDirectories);
809
- }
810
- async function verifyRolledBackKnownFileState(root, journal) {
811
- for (const [index, operation] of journal.plan.operations.entries()) {
812
- const journalOperation = journal.operations[index];
813
- const observed = await observeKnownFile(root, operation.path, maximumKnownFileEvidenceBytes(operation));
814
- if (journalOperation.state === "already-satisfied") {
815
- if (!matchesKnownFileImage(observed, operation.postimage)) {
816
- throw new KnownFileTransactionError("KNOWN_FILE_RECOVERY_CONFLICT", `Unchanged guard drifted during rollback: ${operation.path}.`);
817
- }
818
- continue;
819
- }
820
- if (operation.precondition.state === "absent") {
821
- if (observed.state !== "absent") {
822
- throw new KnownFileTransactionError("KNOWN_FILE_RECOVERY_CONFLICT", `Absent preimage was not restored: ${operation.path}.`);
823
- }
824
- continue;
825
- }
826
- const matched = journalOperation.matchedPreimage;
827
- if (matched === undefined ||
828
- !matchesKnownFileImage(observed, operation.precondition.acceptedPreimages[matched])) {
829
- throw new KnownFileTransactionError("KNOWN_FILE_RECOVERY_CONFLICT", `Exact preimage was not restored: ${operation.path}.`);
830
- }
831
- }
832
- }
3
+ import { classifyKnownFileRecoveryTransition } from "../../application/policies/classify-known-file-recovery-transition.js";
4
+ import { executeApplyingKnownFileRollback, executeCommittedKnownFileRecovery } from "./node-known-file-recovery-executor.js";
5
+ import { observeKnownFileRecoveryEvidence } from "./node-known-file-recovery-observation.js";
6
+ import { canonicalKnownFileRoot, KnownFileTransactionError } from "./node-known-file-transaction-filesystem.js";
833
7
  export async function recoverKnownFileTransaction(options) {
834
8
  if (process.platform === "win32") {
835
9
  throw new KnownFileTransactionError("KNOWN_FILE_APPLY_UNSUPPORTED", "Known-file recovery requires strict directory durability and is not qualified on Windows.");
@@ -842,39 +16,23 @@ export async function recoverKnownFileTransaction(options) {
842
16
  });
843
17
  let retainBarrier = true;
844
18
  try {
845
- const store = new NodeKnownFileTransactionJournalStore(join(root, LOCAL_STATE_DIRECTORY));
846
- await store.canonicalizeTemporary();
847
- const observed = await store.read();
848
- if (observed === undefined) {
849
- throw new KnownFileTransactionError("KNOWN_FILE_JOURNAL_MISSING", "Known-file recovery journal disappeared.");
850
- }
851
- const stored = observed;
852
- const [version, buildIdentity] = await Promise.all([
853
- installedFoundationVersion(),
854
- installedFoundationBuildIdentity()
855
- ]);
856
- if (stored.envelope.foundation.version !== version ||
857
- stored.envelope.foundation.buildIdentity !== buildIdentity) {
858
- throw new KnownFileTransactionError("KNOWN_FILE_EXACT_BUILD_REQUIRED", "The exact Foundation build that created this journal must recover it.");
859
- }
860
- if (stored.envelope.state === "COMMITTED") {
861
- await verifyCommittedKnownFilePostimages(root, stored.envelope.journal);
862
- await cleanupCommittedKnownFileCaptures(root, stored.envelope.journal, options.faultInjector);
863
- await verifyCommittedKnownFilePostimages(root, stored.envelope.journal);
864
- const result = compileKnownFileTransactionReceipt(stored.envelope.journal, "applied");
865
- await store.remove(stored.authority);
866
- retainBarrier = false;
867
- return result;
19
+ const evidence = await observeKnownFileRecoveryEvidence(root);
20
+ const transition = classifyKnownFileRecoveryTransition({
21
+ envelope: evidence.stored.envelope,
22
+ installedBuild: evidence.installedBuild
23
+ });
24
+ if (transition.action === "reject") {
25
+ throw new KnownFileTransactionError(transition.code, transition.message);
868
26
  }
869
- await rollback({
27
+ const execution = {
870
28
  ...(options.faultInjector === undefined ? {} : { faultInjector: options.faultInjector }),
871
29
  root,
872
- store,
873
- stored
874
- });
875
- await verifyRolledBackKnownFileState(root, stored.envelope.journal);
876
- const result = compileRolledBackReceipt(stored.envelope.journal);
877
- await store.remove(stored.authority);
30
+ store: evidence.store,
31
+ stored: evidence.stored
32
+ };
33
+ const result = transition.action === "resume-committed-cleanup"
34
+ ? await executeCommittedKnownFileRecovery(execution)
35
+ : await executeApplyingKnownFileRollback(execution);
878
36
  retainBarrier = false;
879
37
  return result;
880
38
  }