@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,442 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { AppError } from "../shared/errors.js";
5
+ import { PreparedMemoryLayer, isCanonicalOnlyMemoryPath } from "./memory-layer.js";
6
+ import { hybridDigest } from "./hybrid-contracts.js";
7
+ import { readBoundedReleaseFile } from "./release-source.js";
8
+ import { prepareJsonFile } from "../cli/input-preparation.js";
9
+ import { validateSchema } from "./schema-validation.js";
10
+ import { fileURLToPath } from "node:url";
11
+ import { spawnSync } from "node:child_process";
12
+ import { localEnvironmentAllowed } from "./workspace-files.js";
13
+ const hash = (value) => createHash("sha256").update(value).digest("hex");
14
+ function projectPaths(workspace, gitExecutable, env, trackedOnly = false) {
15
+ const gitEnv = Object.fromEntries(Object.entries({ ...process.env, ...env }).filter(([key]) => !key.startsWith("GIT_")));
16
+ const result = spawnSync(gitExecutable, ["-c", "core.fsmonitor=false", "-c", "core.untrackedCache=false",
17
+ "-C", workspace, "ls-files", "-z", "--cached", ...(trackedOnly ? [] : ["--others", "--exclude-standard"])], { env: { ...gitEnv, GIT_OPTIONAL_LOCKS: "0" }, encoding: "utf8", timeout: 5000, maxBuffer: 1024 * 1024 });
18
+ if (result.error || result.signal || result.status !== 0)
19
+ refuse("git_observation_failed");
20
+ const paths = [...new Set(result.stdout.split("\0").filter(Boolean))].sort();
21
+ if (paths.length > 20_000)
22
+ refuse("source_inventory_exceeds_bound");
23
+ return paths;
24
+ }
25
+ function projectEvidencePath(logical) {
26
+ return typeof logical === "string" && logical.length <= 4096 && !path.posix.isAbsolute(logical) && !path.win32.isAbsolute(logical)
27
+ && !logical.includes("\\") && !logical.includes("\0") && logical.split("/").every(part => part && part !== "." && part !== ".."
28
+ && ![".git", "node_modules", ".dd-flow", ".memory-bank", ".tasks", ".dd-eval", ".codex", ".claude", ".zcode"].includes(part.toLowerCase()) && localEnvironmentAllowed(part));
29
+ }
30
+ /** Old public project inputs for preservation claims, never untracked scratch,
31
+ * provider state or link referents. The existing selection size bound applies. */
32
+ export function captureUpgradeProjectFiles(workspace, gitExecutable, env) {
33
+ const files = [];
34
+ let total = 0;
35
+ for (const logical of projectPaths(workspace, gitExecutable, env, true)) {
36
+ if (!projectEvidencePath(logical))
37
+ continue;
38
+ let parent = workspace, safe = true;
39
+ for (const part of logical.split("/").slice(0, -1)) {
40
+ parent = path.join(parent, part);
41
+ if (!fs.lstatSync(parent, { throwIfNoEntry: false })?.isDirectory()) {
42
+ safe = false;
43
+ break;
44
+ }
45
+ }
46
+ if (!safe)
47
+ continue;
48
+ const file = path.join(workspace, logical), stat = fs.lstatSync(file, { throwIfNoEntry: false });
49
+ if (!stat?.isFile())
50
+ continue;
51
+ if (stat.size > 16 * 1024 * 1024 || (total += stat.size) > 64 * 1024 * 1024)
52
+ refuse("source_inventory_exceeds_bound");
53
+ const observed = observeContent(file);
54
+ if (observed.reference.kind !== "file")
55
+ refuse("source_changed_during_inventory", file);
56
+ files.push({ path: logical, base64: observed.content.toString("base64"), sha256: observed.reference.sha256, mode: observed.reference.mode });
57
+ }
58
+ return files;
59
+ }
60
+ /** Decode frozen bytes only; today's files cannot fill missing old evidence. */
61
+ export function readUpgradeProjectFiles(value) {
62
+ if (!Array.isArray(value) || value.length > 20_000)
63
+ refuse("invalid_project_evidence");
64
+ const seen = new Set();
65
+ let total = 0;
66
+ for (const file of value) {
67
+ if (!file || Object.keys(file).sort().join() !== "base64,mode,path,sha256" || !projectEvidencePath(file.path)
68
+ || seen.has(file.path) || typeof file.base64 !== "string" || file.base64.length > 24 * 1024 * 1024
69
+ || !Number.isInteger(file.mode) || file.mode < 0 || file.mode > 0o777)
70
+ refuse("invalid_project_evidence");
71
+ const bytes = Buffer.from(file.base64, "base64");
72
+ if (bytes.toString("base64") !== file.base64 || bytes.length > 16 * 1024 * 1024 || (total += bytes.length) > 64 * 1024 * 1024
73
+ || hash(bytes) !== file.sha256)
74
+ refuse("invalid_project_evidence");
75
+ seen.add(file.path);
76
+ }
77
+ return structuredClone(value);
78
+ }
79
+ /** Literal link identity only: neither outside nor inside referents are read. */
80
+ export function captureUpgradeProjectLinks(workspace, gitExecutable, env) {
81
+ const paths = projectPaths(workspace, gitExecutable, env), links = [];
82
+ let total = 0;
83
+ for (const logical of paths) {
84
+ const parts = logical.split("/");
85
+ if (path.isAbsolute(logical) || parts.some(part => !part || part === "." || part === ".."))
86
+ refuse("unsafe_source_path");
87
+ if (parts.some(part => [".git", "node_modules", ".dd-flow"].includes(part) || !localEnvironmentAllowed(part)))
88
+ continue;
89
+ let directory = workspace, safe = true;
90
+ for (const part of parts.slice(0, -1)) {
91
+ directory = path.join(directory, part);
92
+ if (!fs.lstatSync(directory, { throwIfNoEntry: false })?.isDirectory()) {
93
+ safe = false;
94
+ break;
95
+ }
96
+ }
97
+ if (!safe)
98
+ continue;
99
+ const file = path.join(workspace, logical), stat = fs.lstatSync(file, { throwIfNoEntry: false });
100
+ if (!stat?.isSymbolicLink())
101
+ continue;
102
+ const observed = observeContent(file);
103
+ if ((total += observed.content.length) > 1024 * 1024)
104
+ refuse("link_metadata_exceeds_bound");
105
+ links.push({ path: logical, target_base64: observed.content.toString("base64"), sha256: observed.reference.sha256, mode: observed.reference.mode });
106
+ }
107
+ return links;
108
+ }
109
+ function refuse(reason, file) {
110
+ throw new AppError("upgrade_inventory_invalid", "Upgrade inventory cannot safely authorize content changes", 1, { reason, ...(file ? { file } : {}), effect: "no_effect" });
111
+ }
112
+ function observe(file) {
113
+ return observeContent(file).reference;
114
+ }
115
+ function observeContent(file) {
116
+ const before = fs.lstatSync(file);
117
+ if (!before.isFile() && !before.isSymbolicLink())
118
+ refuse("unsupported_source_kind", file);
119
+ const content = before.isSymbolicLink() ? fs.readlinkSync(file, { encoding: "buffer" })
120
+ : before.size === 0 ? Buffer.alloc(0) : readBoundedReleaseFile(file, 64 * 1024 * 1024);
121
+ const after = fs.lstatSync(file);
122
+ if (before.dev !== after.dev || before.ino !== after.ino || before.mtimeMs !== after.mtimeMs
123
+ || before.size !== after.size || before.mode !== after.mode)
124
+ refuse("source_changed_during_inventory", file);
125
+ const reference = { path: file, sha256: hash(content), kind: before.isSymbolicLink() ? "link" : "file", mode: before.mode & 0o777 };
126
+ return { reference, content };
127
+ }
128
+ /** Old @2 producers repeated canon_root under canon_repo_root. Observe that
129
+ * exact redundant hint only; it never selects a live canonical checkout. */
130
+ function validateUpgradeManifest(manifest, file) {
131
+ const data = { ...manifest };
132
+ if (Object.hasOwn(data, "canon_repo_root")) {
133
+ if (data.schema_id !== "dd-flow/project-flow-pack-manifest@2" || typeof data.canon_repo_root !== "string"
134
+ || data.canon_repo_root !== data.canon_root)
135
+ refuse("legacy_manifest_root_conflict", file);
136
+ delete data.canon_repo_root;
137
+ }
138
+ validateSchema({ schemaName: "project-flow-pack-manifest", file, data, schemaDir: fileURLToPath(new URL("../schemas/", import.meta.url)) });
139
+ }
140
+ /** Read-only ownership/delta map. Version mismatch alone grants no overwrite.
141
+ * Project manifest hashes are hints, not independent canonical authority.
142
+ * Without a prepared old release, canonical-looking files need semantic Work.
143
+ */
144
+ export function prepareMemoryUpgradeInventory(workspaceRoot, layer, baselineLayer) {
145
+ if (!(layer instanceof PreparedMemoryLayer))
146
+ refuse("unprepared_target_layer");
147
+ if (baselineLayer !== undefined && !(baselineLayer instanceof PreparedMemoryLayer))
148
+ refuse("unprepared_baseline_layer");
149
+ if (!path.isAbsolute(workspaceRoot) || fs.realpathSync(workspaceRoot) !== workspaceRoot)
150
+ refuse("noncanonical_workspace");
151
+ const root = path.join(workspaceRoot, ".memory-bank");
152
+ if (!fs.lstatSync(root).isDirectory())
153
+ refuse("linked_or_missing_memory_bank");
154
+ if (!fs.lstatSync(path.join(root, "dd-flow")).isDirectory())
155
+ refuse("linked_or_missing_project_pack");
156
+ if (!fs.lstatSync(path.join(root, "dd-flow/manifest.json")).isFile())
157
+ refuse("linked_or_missing_manifest");
158
+ const prepared = prepareJsonFile(path.join(root, "dd-flow/manifest.json"), "Current project flow-pack manifest");
159
+ validateUpgradeManifest(prepared.value, prepared.file);
160
+ const manifest = prepared.value, current = new Map(), directories = new Set();
161
+ const baselineFiles = baselineLayer?.files();
162
+ if (baselineFiles) {
163
+ const content = baselineFiles.get("dd-flow/manifest.json");
164
+ if (!content)
165
+ refuse("baseline_manifest_missing");
166
+ let old;
167
+ try {
168
+ old = JSON.parse(content.toString("utf8"));
169
+ }
170
+ catch {
171
+ return refuse("baseline_manifest_invalid");
172
+ }
173
+ validateUpgradeManifest(old, "retained-baseline/dd-flow/manifest.json");
174
+ if (old.source_commit !== manifest.source_commit || old.canon_version_at_source_commit !== manifest.canon_version_at_source_commit)
175
+ refuse("baseline_provenance_mismatch");
176
+ }
177
+ let bytes = 0;
178
+ const collect = (directory, prefix = "") => {
179
+ for (const name of fs.readdirSync(directory).sort()) {
180
+ const relative = prefix + name, file = path.join(directory, name), stat = fs.lstatSync(file);
181
+ if (stat.isDirectory()) {
182
+ if (relative.split("/").length > 64)
183
+ refuse("unbounded_directory_depth");
184
+ directories.add(relative);
185
+ collect(file, `${relative}/`);
186
+ }
187
+ else {
188
+ if (current.size >= 20_000 || (bytes += stat.size) > 64 * 1024 * 1024)
189
+ refuse("unbounded_current_inventory");
190
+ current.set(relative, observe(file));
191
+ }
192
+ }
193
+ };
194
+ collect(root);
195
+ const target = layer.files(), installed = new Set(manifest.included_files.map(file => `dd-flow/${file}`));
196
+ const paths = [...new Set([...current.keys(), ...target.keys(), ...installed])].sort();
197
+ const entries = paths.map(logical => {
198
+ const source = current.get(logical) ?? null, content = target.get(logical);
199
+ const expected = logical.startsWith("dd-flow/") ? manifest.file_hashes?.[logical.slice(8)] : undefined;
200
+ const baselineBytes = baselineFiles?.get(logical);
201
+ const baseline = baselineBytes && (logical.startsWith("mbb/") || installed.has(logical) || isCanonicalOnlyMemoryPath(logical))
202
+ && (expected === undefined || expected === hash(baselineBytes)) ? hash(baselineBytes) : null;
203
+ const destination = content ? { path: logical, sha256: hash(content) } : null;
204
+ const canonical = source?.kind === "file" && baseline !== null && source.sha256 === baseline;
205
+ const generated = logical === "dd-flow/manifest.json" || logical.startsWith("dd-flow/.generated/")
206
+ && manifest.generated_allow_patterns?.includes(".generated/**");
207
+ let ownership = "project", action = "retain";
208
+ if (generated) {
209
+ ownership = "generated";
210
+ if (source && destination && source.sha256 !== destination.sha256)
211
+ action = "update";
212
+ }
213
+ else if (canonical) {
214
+ ownership = "canonical";
215
+ action = !destination ? "archive" : destination.sha256 === source.sha256 ? "retain" : "update";
216
+ }
217
+ else if (installed.has(logical) || logical.startsWith("dd-flow/") || logical.startsWith("mbb/")) {
218
+ ownership = "local-customized";
219
+ if (destination && (!source || source.kind !== "file" || destination.sha256 !== source.sha256) || installed.has(logical) && !source)
220
+ action = "semantic-work";
221
+ }
222
+ if (!source && destination && !installed.has(logical)) {
223
+ ownership = "canonical";
224
+ action = "add";
225
+ }
226
+ const ancestors = logical.split("/").slice(0, -1).map((_part, index, parts) => parts.slice(0, index + 1).join("/"));
227
+ if (destination && (directories.has(logical) || ancestors.some(parent => current.has(parent)))) {
228
+ ownership = "local-customized";
229
+ action = "semantic-work";
230
+ }
231
+ return { path: logical, ownership, action, current: source, target: destination, baseline_sha256: baseline };
232
+ });
233
+ const inventory = { schema_id: "dd-flow/mb-upgrade-inventory@1", workspace_root: workspaceRoot,
234
+ current_manifest: { path: prepared.file, sha256: hash(prepared.text) }, target_layer_sha256: layer.sha256,
235
+ baseline_layer_sha256: baselineLayer?.sha256 ?? null,
236
+ entries, local_divergences: entries.filter(entry => entry.ownership === "local-customized").map(entry => entry.path) };
237
+ return { inventory, sha256: hybridDigest(inventory) };
238
+ }
239
+ /** Older selections called the CLI-owned manifest "retain" even when its
240
+ * target bytes differed. Admit that exact historical representation only;
241
+ * all source/target hashes, modes, ownership and other actions still match.
242
+ * Never rewrite the retained inventory or its issued evidence projection. */
243
+ export function matchesRetainedMemoryUpgradeInventory(current, retained) {
244
+ if (current.sha256 === retained.sha256)
245
+ return true;
246
+ const legacy = retained.inventory.entries.find(entry => entry.path === "dd-flow/manifest.json"
247
+ && entry.ownership === "generated" && entry.action === "retain");
248
+ if (!legacy)
249
+ return false;
250
+ const inventory = { ...current.inventory, entries: current.inventory.entries.map(entry => entry.path === legacy.path && entry.ownership === "generated" && entry.action === "update"
251
+ ? { ...entry, action: "retain" } : entry) };
252
+ return hybridDigest(inventory) === retained.sha256;
253
+ }
254
+ /** Full accepted upgrade plan, unlike the reusable partial permission helper.
255
+ * Refuse omissions while diff-analysis is still correctable, before its plan
256
+ * becomes immutable or Work/publication effects begin.
257
+ */
258
+ export function validateUpgradePathMap(inventory, changes) {
259
+ const paths = upgradeAuthoringPaths(inventory, changes);
260
+ for (const entry of inventory.entries) {
261
+ const required = entry.ownership === "canonical" && entry.action !== "retain"
262
+ || entry.ownership === "generated" && entry.target && entry.current?.sha256 !== entry.target.sha256
263
+ || entry.ownership === "local-customized" && entry.action === "semantic-work";
264
+ if (!required)
265
+ continue;
266
+ const logical = `.memory-bank/${entry.path}`;
267
+ const covered = changes.some(change => {
268
+ if (change.ownership !== entry.ownership)
269
+ return false;
270
+ if (entry.ownership === "generated")
271
+ return change.action === "update" && change.from === logical && change.to === logical;
272
+ if (entry.ownership === "local-customized")
273
+ return change.from === logical || !entry.current && change.to === logical;
274
+ if (entry.action === "archive")
275
+ return change.from === logical && ["archive", "move"].includes(change.action);
276
+ if (entry.action === "add")
277
+ return change.to === logical && ["add", "move"].includes(change.action);
278
+ return change.from === logical && change.to === logical && ["update", "semantic-work"].includes(change.action);
279
+ });
280
+ if (!covered)
281
+ refuse("incomplete_upgrade_path_map", entry.path);
282
+ }
283
+ return paths;
284
+ }
285
+ /** Finite authoring paths from an independently accepted diff. Retention and
286
+ * archival are not semantic write permissions; removal/publication has its own
287
+ * mechanical owner. A project snapshot cannot grant canonical ownership.
288
+ */
289
+ export function upgradeAuthoringPaths(inventory, changes) {
290
+ const entries = new Map(inventory.entries.map(entry => [entry.path, entry]));
291
+ const result = new Map();
292
+ const aliases = new Map();
293
+ const physical = (value) => value.normalize("NFC").toLowerCase();
294
+ const currentPhysicalPaths = new Map();
295
+ for (const entry of inventory.entries)
296
+ if (entry.current) {
297
+ const key = physical(entry.path), paths = currentPhysicalPaths.get(key) ?? [];
298
+ paths.push(entry.path);
299
+ currentPhysicalPaths.set(key, paths);
300
+ }
301
+ // A diff describes one transition from the retained tree, not sequential
302
+ // edits. Mechanical publication and semantic Work cannot share a path.
303
+ const claim = (paths) => {
304
+ for (const value of new Set(paths)) {
305
+ const key = physical(value);
306
+ for (const [prior, logical] of aliases) {
307
+ if (prior === key && logical !== value)
308
+ refuse("aliased_upgrade_destinations");
309
+ if (prior === key || prior.startsWith(`${key}/`) || key.startsWith(`${prior}/`))
310
+ refuse("conflicting_upgrade_effects");
311
+ }
312
+ aliases.set(key, value);
313
+ }
314
+ };
315
+ const logical = (value) => {
316
+ if (!value.startsWith(".memory-bank/") || value.includes("\\") || value.includes("\0")
317
+ || value.split("/").some(part => !part || part === "." || part === ".."))
318
+ refuse("invalid_upgrade_authoring_path");
319
+ return value.slice(".memory-bank/".length);
320
+ };
321
+ for (const change of changes) {
322
+ const from = change.from === null ? null : logical(change.from), to = change.to === null ? null : logical(change.to);
323
+ const source = from === null ? undefined : entries.get(from), destination = to === null ? undefined : entries.get(to);
324
+ if (from !== null && !source?.current)
325
+ refuse("missing_upgrade_source");
326
+ const owner = source ?? destination;
327
+ if (owner ? owner.ownership !== change.ownership : change.ownership !== "project")
328
+ refuse("changed_upgrade_ownership");
329
+ if (change.action === "retain" && from !== null && to !== null && from !== to || change.action === "add" && from !== null
330
+ || change.action === "update" && (from === null || from !== to) || change.action === "move" && (from === null || to === null || from === to))
331
+ refuse("invalid_upgrade_path_action");
332
+ if (change.action === "retain") {
333
+ if (from === null && (to === null || !destination || destination.current !== null || destination.ownership !== "local-customized"))
334
+ refuse("invalid_upgrade_path_action");
335
+ continue;
336
+ }
337
+ if (change.action === "archive") {
338
+ if (source?.ownership !== "canonical" || !source.baseline_sha256)
339
+ refuse("unverified_canonical_archive");
340
+ if (to !== null)
341
+ refuse("invalid_upgrade_path_action");
342
+ claim([from]);
343
+ continue;
344
+ }
345
+ if (owner?.ownership === "generated" && change.action !== "update")
346
+ refuse("invalid_generated_upgrade_action");
347
+ if (to === null || change.action === "add" && destination?.current || from !== to && destination?.current)
348
+ refuse("conflicting_upgrade_destination");
349
+ if (owner?.ownership === "local-customized" && change.action !== "semantic-work" && change.action !== "move")
350
+ refuse("customized_upgrade_requires_semantic_work");
351
+ if (owner?.ownership === "canonical" && change.action === "move"
352
+ && (source?.action !== "archive" || destination?.ownership !== "canonical" || destination.action !== "add"))
353
+ refuse("invalid_canonical_move");
354
+ if (destination?.current?.kind === "link" || source?.current?.kind === "link"
355
+ || currentPhysicalPaths.get(physical(to))?.some(value => value !== to)
356
+ || inventory.entries.some(entry => entry.current && (entry.path.startsWith(`${to}/`) || to.startsWith(`${entry.path}/`))))
357
+ refuse("unsafe_upgrade_destination_kind");
358
+ claim(from === null ? [to] : [from, to]);
359
+ if (owner?.ownership === "generated" || owner?.ownership === "canonical" && change.action !== "semantic-work")
360
+ continue;
361
+ const paths = result.get(change.unit_id) ?? [];
362
+ if (!paths.includes(change.to))
363
+ paths.push(change.to);
364
+ result.set(change.unit_id, paths);
365
+ }
366
+ return result;
367
+ }
368
+ function preservationFile(inventory, logical) {
369
+ const parts = logical.split("/");
370
+ if (parts.some(part => !part || part === "." || part === "..") || path.isAbsolute(logical) || logical.includes("\\") || logical.includes("\0"))
371
+ refuse("invalid_preservation_path");
372
+ const root = path.join(inventory.workspace_root, ".memory-bank");
373
+ if (fs.realpathSync(root) !== root)
374
+ refuse("linked_preservation_root");
375
+ for (let index = 1; index < parts.length; index++)
376
+ if (!fs.lstatSync(path.join(root, ...parts.slice(0, index))).isDirectory())
377
+ refuse("linked_preservation_parent");
378
+ return path.join(root, logical);
379
+ }
380
+ /** Capture before any content effect. Local facts and customized canonical
381
+ * files are retained alike; a symlink contributes its link bytes, not its target.
382
+ * The enclosing immutable selection/SQL receipt owns custody, not these hashes.
383
+ */
384
+ export function captureUpgradePreservationBaseline(selected) {
385
+ if (hybridDigest(selected.inventory) !== selected.sha256)
386
+ refuse("changed_baseline_inventory");
387
+ const files = selected.inventory.entries.filter(entry => entry.current).map(entry => {
388
+ const file = preservationFile(selected.inventory, entry.path), observed = observeContent(file);
389
+ if (hybridDigest(observed.reference) !== hybridDigest(entry.current))
390
+ refuse("baseline_source_changed", file);
391
+ return { path: entry.path, base64: observed.content.toString("base64") };
392
+ });
393
+ const baseline = { schema_id: "dd-flow/mb-upgrade-preservation-baseline@1", inventory_sha256: selected.sha256, files };
394
+ readUpgradePreservationBaseline(baseline, selected);
395
+ return baseline;
396
+ }
397
+ /** Pure historical decoding, including under an SQL writer/read snapshot.
398
+ * No filesystem access, restoration, overwrite permission or semantic PASS.
399
+ */
400
+ export function readUpgradePreservationBaseline(baseline, selected) {
401
+ if (!baseline || Object.keys(baseline).sort().join() !== "files,inventory_sha256,schema_id"
402
+ || baseline.schema_id !== "dd-flow/mb-upgrade-preservation-baseline@1" || baseline.inventory_sha256 !== selected.sha256
403
+ || hybridDigest(selected.inventory) !== selected.sha256 || !Array.isArray(baseline.files) || baseline.files.length > 20_000)
404
+ refuse("invalid_preservation_baseline");
405
+ const expected = selected.inventory.entries.filter(entry => entry.current);
406
+ if (baseline.files.length !== expected.length)
407
+ refuse("incomplete_preservation_baseline");
408
+ const files = new Map();
409
+ let size = 0;
410
+ for (const [index, entry] of baseline.files.entries()) {
411
+ const original = expected[index], reference = original.current;
412
+ if (!entry || Object.keys(entry).sort().join() !== "base64,path" || entry.path !== original.path || files.has(entry.path)
413
+ || typeof entry.base64 !== "string" || entry.base64.length > 90 * 1024 * 1024)
414
+ refuse("invalid_preservation_baseline_file");
415
+ const content = Buffer.from(entry.base64, "base64");
416
+ if ((size += content.length) > 64 * 1024 * 1024 || content.toString("base64") !== entry.base64 || hash(content) !== reference.sha256
417
+ || !["file", "link"].includes(reference.kind) || !Number.isInteger(reference.mode) || reference.mode < 0 || reference.mode > 0o777)
418
+ refuse("preservation_baseline_bytes_changed");
419
+ files.set(entry.path, { bytes: content, kind: reference.kind, mode: reference.mode, sha256: reference.sha256 });
420
+ }
421
+ return files;
422
+ }
423
+ /** Exact-byte and mode preservation for files outside canonical replacement.
424
+ * Semantic edits are intentionally not excused here: their acceptance needs
425
+ * separately named facts/invariants and independent review, not a path bypass.
426
+ */
427
+ export function verifyUpgradePreservation(inventory) {
428
+ const violations = [];
429
+ for (const entry of inventory.entries) {
430
+ if (!entry.current || !["project", "local-customized"].includes(entry.ownership))
431
+ continue;
432
+ try {
433
+ const actual = observe(preservationFile(inventory, entry.path));
434
+ if (actual.sha256 !== entry.current.sha256 || actual.kind !== entry.current.kind || actual.mode !== entry.current.mode)
435
+ violations.push({ path: entry.path, reason: "preserved_source_changed" });
436
+ }
437
+ catch {
438
+ violations.push({ path: entry.path, reason: "preserved_source_unavailable" });
439
+ }
440
+ }
441
+ return { ok: violations.length === 0, violations };
442
+ }
@@ -0,0 +1,73 @@
1
+ import { AppError } from "../shared/errors.js";
2
+ import { hybridDigest } from "./hybrid-contracts.js";
3
+ import { PreparedMemoryUpgradePublication } from "./memory-upgrade-publication.js";
4
+ export const upgradePublicationIntentEvent = "memory.upgrade_publication_intent";
5
+ export const upgradePublicationCompleteEvent = "memory.upgrade_publication_complete";
6
+ export const upgradePublicationKey = (binding) => `memory_upgrade_publication:${hybridDigest(binding.scope)}`;
7
+ export function upgradePublicationReceipt(binding, plan, complete = false) {
8
+ return { schema_id: complete ? "dd-flow/memory-upgrade-publication-complete@1" : "dd-flow/memory-upgrade-publication-intent@1",
9
+ run_id: binding.scope.run_id, scope_sha256: hybridDigest(binding.scope), binding_sha256: hybridDigest(binding), plan_sha256: plan.sha256,
10
+ ...(complete ? { outputs: plan.outputs() } : {}) };
11
+ }
12
+ function refuse(reason) {
13
+ throw new AppError("memory_upgrade_publication_custody_invalid", "Publication lacks its exact SQL intent or completion", 1, { reason, effect: "no_effect" });
14
+ }
15
+ /** SQL-only read. The caller selects binding from RUN/bootstrap/registration
16
+ * and settled Work, not agent JSON. Historical readability grants no effects
17
+ * or current filesystem proof; the physical publisher and Stage check those.
18
+ */
19
+ export function readMemoryUpgradePublication(context, binding) {
20
+ return context.db.readSnapshot(() => {
21
+ const row = context.db.get("SELECT value_json FROM runtime_config WHERE key=?", [upgradePublicationKey(binding)]);
22
+ const events = (event) => context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type=? AND json_extract(payload_json,'$.scope_sha256')=? LIMIT 2", [binding.scope.project_id, event, hybridDigest(binding.scope)]);
23
+ const intents = events(upgradePublicationIntentEvent), completions = events(upgradePublicationCompleteEvent);
24
+ if (!row && !intents.length && !completions.length)
25
+ return null;
26
+ if (!row || intents.length !== 1 || completions.length > 1 || row.value_json.length > 180 * 1024 * 1024
27
+ || intents[0].payload_json.length > 32768 || completions.some(row => row.payload_json.length > 16 * 1024 * 1024))
28
+ refuse("intent_custody_missing_or_unbounded");
29
+ try {
30
+ const value = JSON.parse(row.value_json);
31
+ if (!value || Object.keys(value).sort().join() !== "binding,plan_json,plan_sha256" || hybridDigest(value.binding) !== hybridDigest(binding)
32
+ || typeof value.plan_json !== "string")
33
+ refuse("publication_binding_changed");
34
+ const plan = PreparedMemoryUpgradePublication.restore(Buffer.from(value.plan_json), value.plan_sha256);
35
+ if (hybridDigest(JSON.parse(intents[0].payload_json)) !== hybridDigest(upgradePublicationReceipt(binding, plan)))
36
+ refuse("intent_changed");
37
+ if (completions.length && hybridDigest(JSON.parse(completions[0].payload_json)) !== hybridDigest(upgradePublicationReceipt(binding, plan, true)))
38
+ refuse("completion_changed");
39
+ return { plan, completed: completions.length === 1 };
40
+ }
41
+ catch (error) {
42
+ if (error instanceof AppError)
43
+ throw error;
44
+ return refuse("invalid_publication_record");
45
+ }
46
+ });
47
+ }
48
+ /** Read a completed publication after Stage acceptance. The original Work
49
+ * digest stays in its independently audited publication intent; later FINISH
50
+ * must not reconstruct original Work from repaired current documents. All
51
+ * remaining identity fields are selected again by the RUN/bootstrap owner.
52
+ * This read grants no publication effects or current-content acceptance. */
53
+ export function readCompletedMemoryUpgradePublication(context, expected) {
54
+ return context.db.readSnapshot(() => {
55
+ const row = context.db.get("SELECT value_json FROM runtime_config WHERE key=?", [upgradePublicationKey(expected)]);
56
+ if (!row || Buffer.byteLength(row.value_json) > 180 * 1024 * 1024)
57
+ refuse("completed_publication_required");
58
+ let binding;
59
+ try {
60
+ binding = JSON.parse(row.value_json).binding;
61
+ }
62
+ catch {
63
+ return refuse("invalid_publication_record");
64
+ }
65
+ if (!binding || typeof binding.work_custody_sha256 !== "string" || !/^[a-f0-9]{64}$/.test(binding.work_custody_sha256)
66
+ || hybridDigest(binding) !== hybridDigest({ ...expected, work_custody_sha256: binding.work_custody_sha256 }))
67
+ refuse("publication_binding_changed");
68
+ const retained = readMemoryUpgradePublication(context, binding);
69
+ if (!retained?.completed)
70
+ refuse("completed_publication_required");
71
+ return { binding: structuredClone(binding), plan: retained.plan };
72
+ });
73
+ }