@deksden-com/dd-flow-cli 0.9.0-beta.143 → 0.9.0-beta.145

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 (197) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +13 -8
  3. package/dist/build-info.json +7 -7
  4. package/dist/cli/command-inputs.js +9 -3
  5. package/dist/cli/command-metadata.js +33 -14
  6. package/dist/cli/help.js +32 -1
  7. package/dist/cli/hook-ingress.js +27 -3
  8. package/dist/cli/memory-flow-admission.js +7 -3
  9. package/dist/cli/memory-init-invocation.js +10 -3
  10. package/dist/cli/memory-init.js +74 -68
  11. package/dist/cli/memory-storage-admission.js +3 -2
  12. package/dist/cli/memory-storage-qualification.js +105 -0
  13. package/dist/cli/memory-storage-transition.js +123 -0
  14. package/dist/cli/memory-upgrade.js +189 -0
  15. package/dist/cli/native-hook-ingress.js +14 -4
  16. package/dist/cli/native-hook-preparation.js +55 -0
  17. package/dist/cli/response-file.js +2 -1
  18. package/dist/cli/run-cli.js +70 -32
  19. package/dist/cli/setup.js +2 -1
  20. package/dist/cli.js +12 -0
  21. package/dist/domain/stage-handler-identities.js +8 -0
  22. package/dist/harness-runtime/bin/dd-agy.mjs +3 -3
  23. package/dist/harness-runtime/bin/dd-grok.mjs +3 -3
  24. package/dist/harness-runtime/bin/dd-zcode.mjs +5 -5
  25. package/dist/harness-runtime/lib/agy-error-boundary.mjs +58 -0
  26. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +62 -20
  27. package/dist/harness-runtime/lib/dd-agy.mjs +17 -3
  28. package/dist/harness-runtime/lib/dd-droid.mjs +1 -1
  29. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +70 -13
  30. package/dist/harness-runtime/lib/dd-grok.mjs +6 -3
  31. package/dist/harness-runtime/lib/dd-zcode.mjs +44 -14
  32. package/dist/harness-runtime/lib/delegation-instructions.mjs +1 -1
  33. package/dist/harness-runtime/lib/entity-references.d.mts +2 -0
  34. package/dist/harness-runtime/lib/entity-references.mjs +9 -0
  35. package/dist/harness-runtime/lib/managed-daemon.mjs +13 -6
  36. package/dist/harness-runtime/lib/native-hook-command.mjs +4 -4
  37. package/dist/harness-runtime/lib/opencode-plugin.mjs +3 -3
  38. package/dist/harness-runtime/lib/operation-errors.mjs +1 -1
  39. package/dist/harness-runtime/lib/provider-overload-policy.d.mts +4 -2
  40. package/dist/harness-runtime/lib/provider-overload-policy.mjs +19 -6
  41. package/dist/harness-runtime/lib/runner-events.mjs +3 -1
  42. package/dist/harness-runtime/lib/setup-progress.mjs +8 -6
  43. package/dist/schemas/mb-upgrade-stage-result.schema.json +863 -0
  44. package/dist/schemas/memory-repair-work-result.schema.json +9 -2
  45. package/dist/schemas/release-impact.schema.json +3 -3
  46. package/dist/schemas/stage-start-response.schema.json +4 -0
  47. package/dist/services/accepted-intent.js +13 -1
  48. package/dist/services/canon.js +43 -7
  49. package/dist/services/cli-operation-classifier.js +1 -1
  50. package/dist/services/cli-upgrade-allowlist.js +1 -1
  51. package/dist/services/code-checks.js +11 -4
  52. package/dist/services/compatibility-preflight.js +5 -2
  53. package/dist/services/controller-fanout.js +21 -4
  54. package/dist/services/controller-memory-task.js +45 -0
  55. package/dist/services/controller-memory-worker.js +48 -0
  56. package/dist/services/dashboard.js +2 -2
  57. package/dist/services/descriptor-run-start.js +26 -19
  58. package/dist/services/descriptor-stage-coordinator.js +30 -10
  59. package/dist/services/descriptor-stage-start.js +40 -4
  60. package/dist/services/engines.js +64 -14
  61. package/dist/services/eval-snapshots.js +104 -93
  62. package/dist/services/external-work-launch.js +5 -3
  63. package/dist/services/flow-catalog.js +8 -4
  64. package/dist/services/frozen-engine.js +113 -11
  65. package/dist/services/hook-memory-routing.js +188 -0
  66. package/dist/services/hook-recovery-guard.js +14 -0
  67. package/dist/services/hook-session-observation.js +24 -0
  68. package/dist/services/hooks.js +27 -8
  69. package/dist/services/hybrid-contracts.js +2 -13
  70. package/dist/services/hybrid-digest.js +14 -0
  71. package/dist/services/installed-flow-source.js +25 -4
  72. package/dist/services/lifecycle-invocations.js +33 -5
  73. package/dist/services/memory-accepted-composition.js +5 -4
  74. package/dist/services/memory-completed-correction-wave.js +23 -15
  75. package/dist/services/memory-compose-handler.js +163 -21
  76. package/dist/services/memory-composition-acceptance.js +186 -10
  77. package/dist/services/memory-composition-graph.js +42 -11
  78. package/dist/services/memory-composition-registration.js +23 -8
  79. package/dist/services/memory-composition-work-custody.js +14 -9
  80. package/dist/services/memory-composition-work-results.js +15 -3
  81. package/dist/services/memory-corrected-review-batch.js +10 -3
  82. package/dist/services/memory-corrected-review-findings.js +4 -2
  83. package/dist/services/memory-corrected-review-inputs.js +10 -5
  84. package/dist/services/memory-corrected-review-receipt.js +17 -9
  85. package/dist/services/memory-corrected-review-registration.js +3 -2
  86. package/dist/services/memory-correction-check-receipt.js +20 -4
  87. package/dist/services/memory-custody-snapshot.js +36 -0
  88. package/dist/services/memory-finish-evidence.js +44 -22
  89. package/dist/services/memory-finish-handler.js +37 -23
  90. package/dist/services/memory-finish-integration.js +218 -48
  91. package/dist/services/memory-finish-readiness.js +66 -19
  92. package/dist/services/memory-git-integration.js +87 -18
  93. package/dist/services/memory-historical-corrected-review.js +21 -15
  94. package/dist/services/memory-historical-original-review.js +22 -9
  95. package/dist/services/memory-historical-repair-registration.js +14 -8
  96. package/dist/services/memory-historical-second-review.js +30 -11
  97. package/dist/services/memory-init-contexts.js +28 -2
  98. package/dist/services/memory-init-controller-start.js +19 -8
  99. package/dist/services/memory-init-git-policy.js +50 -21
  100. package/dist/services/memory-init-lint-capability.js +21 -3
  101. package/dist/services/memory-init-native-capacity.js +13 -0
  102. package/dist/services/memory-init-preflight.js +11 -1
  103. package/dist/services/memory-init-request-candidate.js +73 -17
  104. package/dist/services/memory-init-request-input.js +23 -7
  105. package/dist/services/memory-init-run-bundle.js +49 -12
  106. package/dist/services/memory-init-storage.js +17 -7
  107. package/dist/services/memory-layer.js +68 -4
  108. package/dist/services/memory-lint-config.js +111 -2
  109. package/dist/services/memory-native-review-copy.js +5 -67
  110. package/dist/services/memory-normal-flow-entry.js +12 -0
  111. package/dist/services/memory-ordinary-defaults.js +22 -5
  112. package/dist/services/memory-preflight-handler.js +15 -0
  113. package/dist/services/memory-repair-current-view.js +1 -1
  114. package/dist/services/memory-repair-document-delta.js +70 -33
  115. package/dist/services/memory-repair-history.js +12 -7
  116. package/dist/services/memory-repair-physical-binding.js +1 -119
  117. package/dist/services/memory-repair-publication-plan.js +5 -3
  118. package/dist/services/memory-repair-publication-source.js +1 -1
  119. package/dist/services/memory-repair-publication.js +3 -2
  120. package/dist/services/memory-repair-registration.js +2 -1
  121. package/dist/services/memory-repair-work-baseline.js +4 -2
  122. package/dist/services/memory-repair-work-completion.js +32 -12
  123. package/dist/services/memory-repair-work-results.js +21 -2
  124. package/dist/services/memory-repair-workspace.js +11 -2
  125. package/dist/services/memory-repair-writer-copy.js +2 -1
  126. package/dist/services/memory-result-binding.js +78 -12
  127. package/dist/services/memory-retained-review-authority.js +17 -8
  128. package/dist/services/memory-retained-second-baseline.js +1 -1
  129. package/dist/services/memory-retained-second-repairs.js +12 -6
  130. package/dist/services/memory-review-correction-plan.js +47 -12
  131. package/dist/services/memory-review-handler.js +17 -17
  132. package/dist/services/memory-review-inputs.js +55 -9
  133. package/dist/services/memory-review-limits.js +10 -0
  134. package/dist/services/memory-review-receipt.js +2 -2
  135. package/dist/services/memory-review-registration.js +13 -4
  136. package/dist/services/memory-review-work-custody.js +3 -1
  137. package/dist/services/memory-review-work-input.js +26 -7
  138. package/dist/services/memory-review-work-results.js +26 -4
  139. package/dist/services/memory-second-corrected-review-inputs.js +5 -3
  140. package/dist/services/memory-second-correction-plan.js +2 -1
  141. package/dist/services/memory-second-repair-launch.js +4 -4
  142. package/dist/services/memory-second-repair-source.js +1 -1
  143. package/dist/services/memory-stage-dispatch.js +38 -1
  144. package/dist/services/memory-stage-results.js +113 -22
  145. package/dist/services/memory-upgrade-analysis-handler.js +243 -0
  146. package/dist/services/memory-upgrade-bootstrap-readiness.js +251 -0
  147. package/dist/services/memory-upgrade-evidence.js +100 -0
  148. package/dist/services/memory-upgrade-inventory.js +442 -0
  149. package/dist/services/memory-upgrade-publication-custody.js +73 -0
  150. package/dist/services/memory-upgrade-publication.js +413 -0
  151. package/dist/services/memory-upgrade-selection.js +430 -0
  152. package/dist/services/memory-upgrade-start.js +60 -0
  153. package/dist/services/memory-upgrade-storage-readiness.js +340 -0
  154. package/dist/services/memory-verification-acceptance.js +38 -17
  155. package/dist/services/memory-verification-checks.js +77 -23
  156. package/dist/services/memory-verification-receipt.js +6 -5
  157. package/dist/services/memory-verify-handler.js +15 -13
  158. package/dist/services/memory-work-result-json.js +12 -0
  159. package/dist/services/memory-work-result-preparation.js +5 -4
  160. package/dist/services/merge-server.js +4 -2
  161. package/dist/services/migrations.js +127 -98
  162. package/dist/services/physical-startup.js +45 -0
  163. package/dist/services/project-registry.js +97 -0
  164. package/dist/services/project-summary.js +1 -1
  165. package/dist/services/projects.js +3 -93
  166. package/dist/services/prompt-delivery.js +71 -0
  167. package/dist/services/provider-capacity.js +74 -9
  168. package/dist/services/recovery-snapshot-database.js +32 -4
  169. package/dist/services/repair-intents.js +1 -12
  170. package/dist/services/repair-receipt.js +13 -0
  171. package/dist/services/run-control-worker.js +35 -18
  172. package/dist/services/run-controller-process.js +2 -6
  173. package/dist/services/run-controller-recovery.js +6 -1
  174. package/dist/services/run-controller.js +131 -80
  175. package/dist/services/run-recovery-guard.js +30 -0
  176. package/dist/services/run-recovery.js +11 -44
  177. package/dist/services/run-start-custody.js +17 -5
  178. package/dist/services/runtime-scope-worker.js +46 -19
  179. package/dist/services/sessions.js +3 -24
  180. package/dist/services/setup.js +63 -13
  181. package/dist/services/stage-handlers.js +9 -7
  182. package/dist/services/stage-pause.js +108 -44
  183. package/dist/services/version-status.js +1 -1
  184. package/dist/services/vnext-fanout.js +21 -9
  185. package/dist/services/vnext-plan-review.js +3 -5
  186. package/dist/services/work-registry.js +80 -33
  187. package/dist/services/workspace-files.js +1 -1
  188. package/dist/shared/entity-references.js +1 -8
  189. package/dist/storage/data-preservation.js +65 -0
  190. package/dist/storage/database.js +53 -19
  191. package/dist/storage/import-guard.js +84 -27
  192. package/dist/storage/memory-upgrade-schema-bootstrap.js +38 -0
  193. package/dist/storage/paired-writer-migration.js +424 -0
  194. package/dist/storage/writer-contract.js +11 -4
  195. package/dist/storage/writer-migration.js +313 -52
  196. package/dist/storage/writer-restore.js +217 -0
  197. package/package.json +8 -4
@@ -0,0 +1,413 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { isScalar, parseDocument } from "yaml";
5
+ import { AppError } from "../shared/errors.js";
6
+ import { publishImmutableInput } from "../storage/immutable-input.js";
7
+ import { acquireInstallationLock } from "./installation-lock.js";
8
+ import { stageRetainedMemoryRepairFile, verifyRetainedMemoryRepairFile } from "./memory-repair-publication-files.js";
9
+ import { PreparedMemoryLayer } from "./memory-layer.js";
10
+ import { hybridDigest } from "./hybrid-contracts.js";
11
+ import { readUpgradePreservationBaseline, upgradeAuthoringPaths } from "./memory-upgrade-inventory.js";
12
+ const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
13
+ function refuse(reason) {
14
+ throw new AppError("memory_upgrade_publication_invalid", "Canonical publication requires exact retained state", 1, { reason, effect: "no_effect" });
15
+ }
16
+ function logical(value) {
17
+ if (!value || path.isAbsolute(value) || value.includes("\\") || value.includes("\0")
18
+ || value.split("/").some(part => !part || part === "." || part === ".."))
19
+ refuse("unsafe_publication_path");
20
+ }
21
+ function directory(value) {
22
+ if (!fs.lstatSync(value).isDirectory())
23
+ refuse("linked_or_missing_publication_directory");
24
+ }
25
+ function parent(root, value, create = false, mode = 0o755) {
26
+ logical(value);
27
+ directory(root);
28
+ let current = root;
29
+ for (const segment of value.split("/").slice(0, -1)) {
30
+ current = path.join(current, segment);
31
+ if (create && !fs.lstatSync(current, { throwIfNoEntry: false }))
32
+ fs.mkdirSync(current, { mode });
33
+ directory(current);
34
+ }
35
+ return path.join(root, value);
36
+ }
37
+ function tree(root) {
38
+ directory(root);
39
+ const entries = [];
40
+ let total = 0;
41
+ const walk = (prefix) => {
42
+ for (const name of fs.readdirSync(path.join(root, prefix)).sort()) {
43
+ const relative = prefix + name;
44
+ logical(relative);
45
+ const file = path.join(root, relative), before = fs.lstatSync(file);
46
+ if (before.isDirectory()) {
47
+ if (relative.split("/").length > 64)
48
+ refuse("unbounded_tree");
49
+ walk(`${relative}/`);
50
+ continue;
51
+ }
52
+ if (!before.isFile() && !before.isSymbolicLink() || before.isFile() && before.nlink !== 1
53
+ || entries.length >= 20_000 || before.size > 64 * 1024 * 1024)
54
+ refuse("unsupported_tree_entry");
55
+ const bytes = before.isSymbolicLink() ? fs.readlinkSync(file, { encoding: "buffer" }) : fs.readFileSync(file);
56
+ const after = fs.lstatSync(file);
57
+ if ((total += bytes.length) > 64 * 1024 * 1024 || before.dev !== after.dev || before.ino !== after.ino
58
+ || before.mode !== after.mode || before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs)
59
+ refuse("tree_changed_during_read");
60
+ entries.push({ path: relative, kind: before.isSymbolicLink() ? "link" : "file", mode: before.mode & 0o777, sha256: hash(bytes) });
61
+ }
62
+ };
63
+ walk("");
64
+ return entries.sort((a, b) => a.path.localeCompare(b.path, "en"));
65
+ }
66
+ function syncDirectory(value) {
67
+ const fd = fs.openSync(value, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
68
+ try {
69
+ fs.fsyncSync(fd);
70
+ }
71
+ finally {
72
+ fs.closeSync(fd);
73
+ }
74
+ }
75
+ function syncParents(root, logical) {
76
+ let current = path.dirname(parent(root, logical));
77
+ while (current !== root) {
78
+ syncDirectory(current);
79
+ current = path.dirname(current);
80
+ }
81
+ syncDirectory(root);
82
+ }
83
+ /** Finite filesystem effects only, not Stage/Work/SQL authority. The caller
84
+ * must retain this exact plan under its accepted diff and settled Work before
85
+ * applying it. Restoring bytes does not admit a RUN or accept semantic edits.
86
+ */
87
+ export class PreparedMemoryUpgradePublication {
88
+ sha256;
89
+ #plan;
90
+ constructor(plan) { this.#plan = structuredClone(plan); this.sha256 = hybridDigest(plan); Object.freeze(this); }
91
+ static prepare(input) {
92
+ const { inventory: selected, layer, changes } = input;
93
+ if (!/^[a-f0-9]{64}$/.test(input.selectionSha256) || !(layer instanceof PreparedMemoryLayer)
94
+ || selected.inventory.target_layer_sha256 !== layer.sha256)
95
+ refuse("foreign_publication_selection");
96
+ const old = readUpgradePreservationBaseline(input.oldTree, selected);
97
+ const authoring = new Set([...upgradeAuthoringPaths(selected.inventory, changes).values()].flat().map(value => value.slice(13)));
98
+ const workspace = selected.inventory.workspace_root;
99
+ if (fs.realpathSync(workspace) !== workspace)
100
+ refuse("noncanonical_workspace");
101
+ const root = path.join(workspace, ".memory-bank"), before = tree(root), current = new Map(before.map(entry => [entry.path, entry]));
102
+ const frozen = new Map(selected.inventory.entries.map(entry => [entry.path, entry]));
103
+ for (const entry of before) {
104
+ const expected = frozen.get(entry.path)?.current;
105
+ if (!authoring.has(entry.path) && (!expected || entry.kind !== expected.kind || entry.mode !== expected.mode || entry.sha256 !== expected.sha256))
106
+ refuse("unassigned_project_change");
107
+ }
108
+ for (const [name, entry] of frozen)
109
+ if (entry.current && !current.has(name) && !authoring.has(name))
110
+ refuse("unassigned_project_removal");
111
+ const actions = [], archived = [];
112
+ const target = layer.files();
113
+ const add = (name, bytes, mode = 0o644) => {
114
+ logical(name);
115
+ const source = current.get(name) ?? null;
116
+ const after = bytes === null ? null : { path: name, kind: "file", mode, sha256: hash(bytes) };
117
+ if (hybridDigest(source) !== hybridDigest(after))
118
+ actions.push({ path: name, before: source, after, base64: bytes?.toString("base64") ?? null });
119
+ };
120
+ const content = [];
121
+ const archiveSource = (name, original, independent) => {
122
+ const bytes = old.get(name), entry = frozen.get(name);
123
+ if (!bytes || bytes.kind !== "file" || independent && bytes.sha256 !== entry?.baseline_sha256
124
+ || bytes.sha256 !== original.sha256)
125
+ refuse("unverified_archive_bytes");
126
+ // Historical documents are recovery data, not another active Markdown
127
+ // catalog. The record retains their original paths and raw byte hashes.
128
+ const archive = `archive/mb-upgrade/${input.selectionSha256}/files/${name}.bin`;
129
+ if (current.has(archive))
130
+ refuse("archive_destination_exists");
131
+ add(archive, bytes.bytes, bytes.mode);
132
+ archived.push({ path: name, archive, mode: bytes.mode, sha256: bytes.sha256 });
133
+ };
134
+ const mechanicalChanges = changes.flatMap(change => change.ownership === "canonical" && change.action === "move"
135
+ ? [{ ...change, to: null, action: "archive" }, { ...change, from: null, action: "add" }] : [change]);
136
+ for (const change of mechanicalChanges) {
137
+ if (change.action === "retain" || change.action === "semantic-work" || !["canonical", "generated"].includes(change.ownership))
138
+ continue;
139
+ const name = (change.to ?? change.from).slice(13), entry = frozen.get(name);
140
+ if (!entry || entry.ownership !== change.ownership || entry.action !== change.action && entry.ownership !== "generated")
141
+ refuse("changed_mechanical_action");
142
+ const original = current.get(name) ?? null;
143
+ if (original && (original.kind !== "file" || original.sha256 !== entry.current?.sha256 || original.mode !== entry.current.mode))
144
+ refuse("changed_canonical_source");
145
+ if (original)
146
+ archiveSource(name, original, entry.ownership === "canonical");
147
+ const bytes = change.action === "archive" ? null : target.get(name);
148
+ if (bytes === undefined)
149
+ refuse("missing_canonical_target");
150
+ const start = actions.length;
151
+ add(name, bytes, original?.mode ?? 0o644);
152
+ content.push(...actions.splice(start));
153
+ }
154
+ if (input.versionSource !== undefined) {
155
+ const name = input.versionSource;
156
+ if (!["index.md", "mbb/index.md"].includes(name) || content.some(action => action.path === name))
157
+ refuse("invalid_version_source");
158
+ const source = current.get(name), original = old.get(name);
159
+ const targetManifest = target.get("dd-flow/manifest.json"), oldManifest = old.get("dd-flow/manifest.json");
160
+ if (!source || source.kind !== "file" || !original || !targetManifest || !oldManifest)
161
+ refuse("missing_version_context");
162
+ const from = JSON.parse(oldManifest.bytes.toString("utf8")).canon_version_at_source_commit;
163
+ const to = JSON.parse(targetManifest.toString("utf8")).canon_version_at_source_commit;
164
+ if (typeof from !== "string" || typeof to !== "string" || !/^\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?$/.test(to))
165
+ refuse("invalid_target_version");
166
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(fs.readFileSync(parent(root, name)));
167
+ const frontmatter = text.match(/^---\n([\s\S]*?)\n---(?:\n|$)/);
168
+ if (!frontmatter)
169
+ refuse("version_frontmatter_missing");
170
+ const document = parseDocument(frontmatter[1]), scalar = document.get("memory_bank_version", true);
171
+ if (document.errors.length)
172
+ refuse("version_marker_changed");
173
+ const oldText = new TextDecoder("utf-8", { fatal: true }).decode(original.bytes);
174
+ const oldFrontmatter = oldText.match(/^---\n([\s\S]*?)\n---(?:\n|$)/);
175
+ const oldDocument = oldFrontmatter ? parseDocument(oldFrontmatter[1]) : null;
176
+ const oldMarker = oldDocument && !oldDocument.errors.length ? oldDocument.get("memory_bank_version", true) : null;
177
+ // The admitted project marker may legitimately lag its installed pack.
178
+ // Compare with its frozen document, not a different version context.
179
+ const markerFrom = isScalar(oldMarker) && typeof oldMarker.value === "string" ? oldMarker.value : from;
180
+ let updated;
181
+ if (!document.has("memory_bank_version")) {
182
+ if (!oldDocument || oldDocument.errors.length || oldDocument.has("memory_bank_version"))
183
+ refuse("version_marker_changed");
184
+ const offset = 4 + frontmatter[1].length;
185
+ updated = text.slice(0, offset) + "\nmemory_bank_version: " + JSON.stringify(to) + text.slice(offset);
186
+ }
187
+ else {
188
+ if (!isScalar(scalar) || !scalar.range || scalar.value !== markerFrom && scalar.value !== to)
189
+ refuse("version_marker_changed");
190
+ const offset = 4, start = offset + scalar.range[0], end = offset + scalar.range[1];
191
+ updated = scalar.value === to ? text : text.slice(0, start) + JSON.stringify(to) + text.slice(end);
192
+ }
193
+ const bytes = Buffer.from(updated);
194
+ if (hash(bytes) !== source.sha256) {
195
+ // Preserve the historical marker document, not merely its version.
196
+ // If semantic Work changed it, its old bytes remain in the frozen tree;
197
+ // this archive records that original rather than inventing a template.
198
+ const archive = `archive/mb-upgrade/${input.selectionSha256}/files/${name}.bin`;
199
+ if (current.has(archive))
200
+ refuse("archive_destination_exists");
201
+ add(archive, original.bytes, original.mode);
202
+ archived.push({ path: name, archive, mode: original.mode, sha256: original.sha256 });
203
+ const count = actions.length;
204
+ add(name, bytes, source.mode);
205
+ content.push(...actions.splice(count));
206
+ }
207
+ }
208
+ // Recovery material and its project-owned migration record precede every
209
+ // destructive effect. They survive worktree cleanup through integration.
210
+ const record = `archive/mb-upgrade/${input.selectionSha256}/migration.json`;
211
+ if (current.has(record))
212
+ refuse("migration_record_exists");
213
+ add(record, Buffer.from(`${JSON.stringify({ schema_id: "dd-flow/memory-upgrade-content-migration@1",
214
+ selection_sha256: input.selectionSha256, inventory_sha256: selected.sha256, target_layer_sha256: layer.sha256,
215
+ path_map: changes.map(({ unit_id, from, to, action, ownership }) => ({ unit_id, from, to, action, ownership })),
216
+ archived, changes: content.map(action => ({ path: action.path, before: action.before, after: action.after })) }, null, 2)}\n`));
217
+ actions.push(...content);
218
+ let finalBytes = before.reduce((size, entry) => size + fs.lstatSync(path.join(root, entry.path)).size, 0);
219
+ let finalCount = before.length;
220
+ for (const action of actions) {
221
+ if (!action.before && fs.lstatSync(path.join(root, action.path), { throwIfNoEntry: false }))
222
+ refuse("publication_destination_exists");
223
+ if (action.before) {
224
+ finalBytes -= fs.lstatSync(path.join(root, action.path)).size;
225
+ finalCount--;
226
+ }
227
+ if (action.after) {
228
+ finalBytes += Buffer.from(action.base64, "base64").length;
229
+ finalCount++;
230
+ }
231
+ }
232
+ if (finalBytes > 64 * 1024 * 1024 || finalCount > 20_000)
233
+ refuse("publication_exceeds_inventory_capacity");
234
+ const plan = { schema_id: "dd-flow/memory-upgrade-publication@1", workspace, selection_sha256: input.selectionSha256,
235
+ inventory_sha256: selected.sha256, layer_sha256: layer.sha256, before, actions };
236
+ return PreparedMemoryUpgradePublication.restore(Buffer.from(JSON.stringify(plan)), hybridDigest(plan));
237
+ }
238
+ static restore(bytes, expectedSha256) {
239
+ try {
240
+ if (!(bytes instanceof Uint8Array) || bytes.byteLength > 160 * 1024 * 1024 || !/^[a-f0-9]{64}$/.test(expectedSha256))
241
+ refuse("unbounded_plan");
242
+ const plan = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
243
+ if (!plan || Object.keys(plan).sort().join() !== "actions,before,inventory_sha256,layer_sha256,schema_id,selection_sha256,workspace"
244
+ || plan.schema_id !== "dd-flow/memory-upgrade-publication@1" || hybridDigest(plan) !== expectedSha256
245
+ || [plan.selection_sha256, plan.inventory_sha256, plan.layer_sha256].some(value => typeof value !== "string" || !/^[a-f0-9]{64}$/.test(value))
246
+ || !path.isAbsolute(plan.workspace) || !Array.isArray(plan.before) || !Array.isArray(plan.actions)
247
+ || plan.before.length > 20_000 || plan.actions.length > 40_001)
248
+ refuse("invalid_retained_plan");
249
+ const state = new Map();
250
+ let total = 0;
251
+ const entry = (value) => {
252
+ if (!value || Object.keys(value).sort().join() !== "kind,mode,path,sha256" || typeof value.path !== "string")
253
+ refuse("invalid_retained_entry");
254
+ logical(value.path);
255
+ if (!["file", "link"].includes(value.kind) || !Number.isInteger(value.mode) || value.mode < 0 || value.mode > 0o777
256
+ || !/^[a-f0-9]{64}$/.test(value.sha256))
257
+ refuse("invalid_retained_entry");
258
+ };
259
+ for (const value of plan.before) {
260
+ entry(value);
261
+ if (state.has(value.path))
262
+ refuse("duplicate_retained_entry");
263
+ state.set(value.path, value);
264
+ }
265
+ const paths = new Set();
266
+ for (const action of plan.actions) {
267
+ if (!action || Object.keys(action).sort().join() !== "after,base64,before,path" || typeof action.path !== "string")
268
+ refuse("invalid_retained_action");
269
+ logical(action.path);
270
+ if (paths.has(action.path) || hybridDigest(action.before) !== hybridDigest(state.get(action.path) ?? null))
271
+ refuse("invalid_retained_action");
272
+ if (hybridDigest(action.before) === hybridDigest(action.after))
273
+ refuse("unchanged_retained_action");
274
+ paths.add(action.path);
275
+ if (action.before && action.before.kind !== "file")
276
+ refuse("linked_publication_source");
277
+ if (action.after) {
278
+ entry(action.after);
279
+ if (action.after.path !== action.path || action.after.kind !== "file" || typeof action.base64 !== "string"
280
+ || action.base64.length > 90 * 1024 * 1024)
281
+ refuse("invalid_retained_output");
282
+ const content = Buffer.from(action.base64, "base64");
283
+ total += content.length;
284
+ if (total > 128 * 1024 * 1024 || content.toString("base64") !== action.base64 || hash(content) !== action.after.sha256)
285
+ refuse("retained_output_changed");
286
+ const physical = action.path.normalize("NFC").toLowerCase();
287
+ for (const name of state.keys()) {
288
+ const prior = name.normalize("NFC").toLowerCase();
289
+ if (name !== action.path && (prior === physical || prior.startsWith(`${physical}/`) || physical.startsWith(`${prior}/`)))
290
+ refuse("conflicting_publication_paths");
291
+ }
292
+ state.set(action.path, action.after);
293
+ }
294
+ else {
295
+ if (!action.before || action.base64 !== null)
296
+ refuse("invalid_retained_removal");
297
+ state.delete(action.path);
298
+ }
299
+ }
300
+ return new PreparedMemoryUpgradePublication(plan);
301
+ }
302
+ catch (error) {
303
+ if (error instanceof AppError)
304
+ throw error;
305
+ return refuse("invalid_retained_plan");
306
+ }
307
+ }
308
+ bytes() { return Buffer.from(JSON.stringify(this.#plan)); }
309
+ beforeFiles() { return structuredClone(this.#plan.before); }
310
+ actions() { return structuredClone(this.#plan.actions); }
311
+ outputs() { return this.#plan.actions.flatMap(action => action.after ? [structuredClone(action.after)] : []); }
312
+ classify() {
313
+ if (fs.realpathSync(this.#plan.workspace) !== this.#plan.workspace)
314
+ refuse("workspace_changed");
315
+ const actual = tree(path.join(this.#plan.workspace, ".memory-bank")), observed = new Map(actual.map(entry => [entry.path, entry]));
316
+ const state = new Map(this.#plan.before.map(entry => [entry.path, entry]));
317
+ let completed = 0, pending = false;
318
+ for (const action of this.#plan.actions) {
319
+ const value = observed.get(action.path) ?? null;
320
+ if (hybridDigest(value) === hybridDigest(action.after)) {
321
+ if (pending)
322
+ refuse("unknown_publication_prefix");
323
+ completed++;
324
+ if (action.after)
325
+ state.set(action.path, action.after);
326
+ else
327
+ state.delete(action.path);
328
+ }
329
+ else if (hybridDigest(value) === hybridDigest(action.before))
330
+ pending = true;
331
+ else
332
+ refuse("unknown_publication_state");
333
+ }
334
+ const expected = [...state.values()].sort((a, b) => a.path.localeCompare(b.path, "en"));
335
+ if (hybridDigest(actual) !== hybridDigest(expected))
336
+ refuse("unknown_publication_state");
337
+ for (const action of this.#plan.actions) {
338
+ const entry = state.get(action.path), stat = fs.lstatSync(path.join(this.#plan.workspace, ".memory-bank", action.path), { throwIfNoEntry: false });
339
+ if (entry ? !stat?.isFile() : stat !== undefined)
340
+ refuse("unexpected_publication_destination_kind");
341
+ }
342
+ return { completed, total: this.#plan.actions.length };
343
+ }
344
+ applyNextAction() {
345
+ const workspace = this.#plan.workspace;
346
+ if (fs.realpathSync(workspace) !== workspace)
347
+ refuse("workspace_changed");
348
+ const namespace = path.dirname(parent(workspace, ".dd-flow/memory-upgrade-publications/owner", true));
349
+ const lock = acquireInstallationLock(path.join(namespace, "workspace.lock"), this.sha256);
350
+ try {
351
+ lock.assertOwnership();
352
+ const journal = path.dirname(parent(namespace, `${this.sha256}/plan.json`, true, 0o700));
353
+ if ((fs.lstatSync(journal).mode & 0o7777) !== 0o700)
354
+ refuse("unsafe_journal_mode");
355
+ const planFile = path.join(journal, "plan.json");
356
+ if (!fs.lstatSync(planFile, { throwIfNoEntry: false }) && this.classify().completed !== 0)
357
+ refuse("publication_journal_missing");
358
+ publishImmutableInput(planFile, this.bytes(), "memory_upgrade_publication_invalid");
359
+ // A crash between link and unlink leaves two names for our complete
360
+ // staged addition. Remove only that exact journal alias, never user data.
361
+ for (const [index, item] of this.#plan.actions.entries())
362
+ if (item.after && !item.before) {
363
+ const slot = path.join(journal, `action-${index}.tmp`), destination = path.join(workspace, ".memory-bank", item.path);
364
+ const staged = fs.lstatSync(slot, { throwIfNoEntry: false }), published = fs.lstatSync(destination, { throwIfNoEntry: false });
365
+ if (staged && published && staged.dev === published.dev && staged.ino === published.ino) {
366
+ if (!staged.isFile() || staged.nlink !== 2 || (staged.mode & 0o777) !== item.after.mode
367
+ || hash(fs.readFileSync(slot)) !== item.after.sha256)
368
+ refuse("unknown_linked_staging_file");
369
+ fs.unlinkSync(slot);
370
+ syncDirectory(journal);
371
+ }
372
+ }
373
+ const view = this.classify();
374
+ for (const item of this.#plan.actions.slice(0, view.completed))
375
+ syncParents(path.join(workspace, ".memory-bank"), item.path);
376
+ if (view.completed === view.total)
377
+ return { ...view, status: "applied" };
378
+ const action = this.#plan.actions[view.completed], root = path.join(workspace, ".memory-bank");
379
+ const destination = parent(root, action.path, true);
380
+ let staged;
381
+ if (action.after) {
382
+ const bytes = Buffer.from(action.base64, "base64");
383
+ staged = stageRetainedMemoryRepairFile(journal, `action-${view.completed}.tmp`, bytes, action.after.mode).path;
384
+ verifyRetainedMemoryRepairFile(staged, bytes, action.after.mode);
385
+ }
386
+ if (this.classify().completed !== view.completed)
387
+ refuse("publication_cursor_changed");
388
+ lock.assertOwnership();
389
+ if (staged) {
390
+ if (action.before)
391
+ fs.renameSync(staged, destination);
392
+ else {
393
+ fs.linkSync(staged, destination);
394
+ fs.unlinkSync(staged);
395
+ }
396
+ }
397
+ else
398
+ fs.unlinkSync(destination);
399
+ syncParents(root, action.path);
400
+ syncDirectory(journal);
401
+ const after = this.classify();
402
+ if (after.completed !== view.completed + 1)
403
+ refuse("publication_readback_failed");
404
+ return { ...after, status: after.completed === after.total ? "applied" : "pending" };
405
+ }
406
+ catch (error) {
407
+ throw new AppError("memory_upgrade_publication_pending", "Preserve the retained plan and reconcile its exact filesystem prefix", 1, { effect: "publication_pending", publication_sha256: this.sha256, cause: error instanceof Error ? error.message : String(error) });
408
+ }
409
+ finally {
410
+ lock.release();
411
+ }
412
+ }
413
+ }