@deksden-com/dd-flow-cli 0.9.0-beta.136 → 0.9.0-beta.138

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 (230) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +9 -0
  3. package/dist/build-info.json +6 -6
  4. package/dist/cli/argument-values.js +5 -5
  5. package/dist/cli/command-inputs.js +89 -20
  6. package/dist/cli/command-metadata.js +56 -3
  7. package/dist/cli/command-option-descriptions.js +5 -0
  8. package/dist/cli/executor-onboarding.js +8 -3
  9. package/dist/cli/flows.js +29 -0
  10. package/dist/cli/help.js +240 -6
  11. package/dist/cli/input-preparation.js +54 -5
  12. package/dist/cli/memory-flow-admission.js +44 -0
  13. package/dist/cli/memory-init-invocation.js +78 -0
  14. package/dist/cli/memory-init.js +240 -0
  15. package/dist/cli/memory-storage-admission.js +53 -0
  16. package/dist/cli/native-hook-ingress.js +1 -1
  17. package/dist/cli/response-file.js +2 -0
  18. package/dist/cli/run-cli.js +283 -160
  19. package/dist/cli/setup.js +9 -7
  20. package/dist/cli.js +54 -2
  21. package/dist/domain/stage-handler-identities.js +20 -0
  22. package/dist/harness-runtime/DD-ZCODE.md +13 -0
  23. package/dist/harness-runtime/bin/dd-opencode.mjs +3 -3
  24. package/dist/harness-runtime/lib/daemon-operations.d.mts +1 -0
  25. package/dist/harness-runtime/lib/daemon-operations.mjs +4 -3
  26. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +4 -2
  27. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +11 -5
  28. package/dist/harness-runtime/lib/dd-codex.mjs +22 -10
  29. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +2 -1
  30. package/dist/harness-runtime/lib/dd-droid.mjs +2 -1
  31. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +4 -2
  32. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +20 -5
  33. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +45 -10
  34. package/dist/harness-runtime/lib/dd-zcode.mjs +7 -3
  35. package/dist/harness-runtime/lib/delegation-instructions.mjs +5 -3
  36. package/dist/harness-runtime/lib/lifecycle-command.mjs +1 -0
  37. package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -0
  38. package/dist/harness-runtime/lib/managed-daemon.mjs +50 -15
  39. package/dist/harness-runtime/lib/managed-flow-env.mjs +15 -0
  40. package/dist/harness-runtime/lib/native-hook-command.mjs +1 -1
  41. package/dist/harness-runtime/lib/native-request-proof.d.mts +2 -0
  42. package/dist/harness-runtime/lib/native-request-proof.mjs +65 -0
  43. package/dist/harness-runtime/lib/opencode-plugin.mjs +5 -1
  44. package/dist/harness-runtime/lib/operation-progress.mjs +3 -1
  45. package/dist/runtime/context.js +1 -1
  46. package/dist/schemas/discovery-result.schema.json +184 -0
  47. package/dist/schemas/execution-bundle.schema.json +205 -0
  48. package/dist/schemas/execution-settings.schema.json +162 -0
  49. package/dist/schemas/flow-descriptor.schema.json +54 -0
  50. package/dist/schemas/flow-run.schema.json +22 -0
  51. package/dist/schemas/harness-catalog.schema.json +80 -0
  52. package/dist/schemas/mb-init-compose-result.schema.json +457 -0
  53. package/dist/schemas/mb-init-discover-result.schema.json +500 -0
  54. package/dist/schemas/mb-init-finish-result.schema.json +422 -0
  55. package/dist/schemas/mb-init-preflight-result.schema.json +367 -0
  56. package/dist/schemas/mb-init-review-result.schema.json +396 -0
  57. package/dist/schemas/mb-init-verify-result.schema.json +391 -0
  58. package/dist/schemas/memory-bootstrap-receipt-v2.schema.json +67 -0
  59. package/dist/schemas/memory-bootstrap-receipt.schema.json +354 -0
  60. package/dist/schemas/memory-composition-work-result.schema.json +38 -0
  61. package/dist/schemas/memory-repair-work-result.schema.json +49 -0
  62. package/dist/schemas/memory-review-correction-plan.schema.json +30 -0
  63. package/dist/schemas/memory-review-work-result.schema.json +43 -0
  64. package/dist/schemas/model-catalog.schema.json +153 -0
  65. package/dist/schemas/profile-settings.schema.json +122 -0
  66. package/dist/schemas/release-manifest.schema.json +291 -0
  67. package/dist/schemas/run-engine-binding.schema.json +1 -1
  68. package/dist/services/check-runner-evidence.js +51 -4
  69. package/dist/services/cli-operation-classifier.js +8 -47
  70. package/dist/services/code-checks.js +387 -34
  71. package/dist/services/controller-child-observer.js +99 -0
  72. package/dist/services/controller-execution.js +88 -0
  73. package/dist/services/controller-fanout.js +79 -13
  74. package/dist/services/dashboard.js +8 -2
  75. package/dist/services/descriptor-coordinator-session.js +35 -0
  76. package/dist/services/descriptor-root-work.js +53 -0
  77. package/dist/services/descriptor-run-settlement.js +105 -0
  78. package/dist/services/descriptor-run-start.js +449 -0
  79. package/dist/services/descriptor-stage-coordinator.js +197 -0
  80. package/dist/services/descriptor-stage-settlement.js +222 -0
  81. package/dist/services/descriptor-stage-start.js +139 -0
  82. package/dist/services/engines.js +39 -22
  83. package/dist/services/eval-snapshots.js +83 -25
  84. package/dist/services/execution-bundles.js +263 -0
  85. package/dist/services/execution-policy.js +20 -16
  86. package/dist/services/external-work-launch.js +91 -11
  87. package/dist/services/flow-catalog.js +21 -0
  88. package/dist/services/flow-descriptors.js +82 -0
  89. package/dist/services/frozen-engine.js +242 -0
  90. package/dist/services/generic-execution.js +164 -0
  91. package/dist/services/harness-adapter.js +28 -2
  92. package/dist/services/hooks.js +24 -6
  93. package/dist/services/hybrid-contracts.js +31 -3
  94. package/dist/services/ids.js +29 -1
  95. package/dist/services/installation-lock.js +7 -4
  96. package/dist/services/installed-flow-source.js +183 -0
  97. package/dist/services/lifecycle-invocations.js +257 -78
  98. package/dist/services/memory-accepted-composition.js +77 -0
  99. package/dist/services/memory-baseline-preservation.js +65 -0
  100. package/dist/services/memory-completed-correction-wave.js +320 -0
  101. package/dist/services/memory-compose-handler.js +258 -0
  102. package/dist/services/memory-composition-acceptance.js +199 -0
  103. package/dist/services/memory-composition-graph.js +134 -0
  104. package/dist/services/memory-composition-registration.js +146 -0
  105. package/dist/services/memory-composition-work-custody.js +151 -0
  106. package/dist/services/memory-composition-work-results.js +96 -0
  107. package/dist/services/memory-corrected-review-acceptance.js +129 -0
  108. package/dist/services/memory-corrected-review-batch.js +30 -0
  109. package/dist/services/memory-corrected-review-findings.js +106 -0
  110. package/dist/services/memory-corrected-review-inputs.js +210 -0
  111. package/dist/services/memory-corrected-review-projection.js +78 -0
  112. package/dist/services/memory-corrected-review-receipt.js +170 -0
  113. package/dist/services/memory-corrected-review-registration.js +191 -0
  114. package/dist/services/memory-correction-check-receipt.js +219 -0
  115. package/dist/services/memory-correction-input-owner.js +6 -0
  116. package/dist/services/memory-discover-handler.js +104 -0
  117. package/dist/services/memory-finish-evidence.js +435 -0
  118. package/dist/services/memory-finish-handler.js +215 -0
  119. package/dist/services/memory-finish-integration.js +280 -0
  120. package/dist/services/memory-finish-readiness.js +122 -0
  121. package/dist/services/memory-git-integration.js +151 -0
  122. package/dist/services/memory-historical-corrected-review.js +206 -0
  123. package/dist/services/memory-historical-original-review.js +130 -0
  124. package/dist/services/memory-historical-repair-registration.js +99 -0
  125. package/dist/services/memory-historical-second-review.js +128 -0
  126. package/dist/services/memory-init-bootstrap-readiness.js +210 -0
  127. package/dist/services/memory-init-contexts.js +89 -0
  128. package/dist/services/memory-init-controller-start.js +84 -0
  129. package/dist/services/memory-init-engine-release.js +223 -0
  130. package/dist/services/memory-init-git-policy.js +136 -0
  131. package/dist/services/memory-init-lint-capability.js +185 -0
  132. package/dist/services/memory-init-native-capacity.js +241 -0
  133. package/dist/services/memory-init-preflight.js +312 -0
  134. package/dist/services/memory-init-project-bootstrap.js +599 -0
  135. package/dist/services/memory-init-request-candidate.js +157 -0
  136. package/dist/services/memory-init-request-input.js +66 -0
  137. package/dist/services/memory-init-resource-storage.js +125 -0
  138. package/dist/services/memory-init-run-allocation.js +81 -0
  139. package/dist/services/memory-init-run-bundle.js +98 -0
  140. package/dist/services/memory-init-start.js +175 -0
  141. package/dist/services/memory-init-startup-custody.js +80 -0
  142. package/dist/services/memory-init-startup-input.js +240 -0
  143. package/dist/services/memory-init-storage.js +81 -0
  144. package/dist/services/memory-init-stores.js +359 -0
  145. package/dist/services/memory-init-worktrunk-capability.js +172 -0
  146. package/dist/services/memory-layer-publication.js +611 -0
  147. package/dist/services/memory-layer.js +156 -0
  148. package/dist/services/memory-lint-config.js +85 -0
  149. package/dist/services/memory-native-review-copy.js +165 -0
  150. package/dist/services/memory-normal-flow-entry.js +75 -0
  151. package/dist/services/memory-ordinary-defaults.js +112 -0
  152. package/dist/services/memory-preflight-bootstrap-custody.js +115 -0
  153. package/dist/services/memory-preflight-handler.js +259 -0
  154. package/dist/services/memory-repair-current-view.js +299 -0
  155. package/dist/services/memory-repair-document-delta.js +451 -0
  156. package/dist/services/memory-repair-history.js +491 -0
  157. package/dist/services/memory-repair-physical-binding.js +312 -0
  158. package/dist/services/memory-repair-publication-files.js +152 -0
  159. package/dist/services/memory-repair-publication-intent.js +27 -0
  160. package/dist/services/memory-repair-publication-plan.js +160 -0
  161. package/dist/services/memory-repair-publication-source.js +225 -0
  162. package/dist/services/memory-repair-publication.js +794 -0
  163. package/dist/services/memory-repair-registration.js +239 -0
  164. package/dist/services/memory-repair-work-baseline.js +413 -0
  165. package/dist/services/memory-repair-work-completion.js +97 -0
  166. package/dist/services/memory-repair-work-custody.js +73 -0
  167. package/dist/services/memory-repair-work-results.js +89 -0
  168. package/dist/services/memory-repair-workspace.js +252 -0
  169. package/dist/services/memory-repair-writer-copy.js +735 -0
  170. package/dist/services/memory-result-binding.js +173 -0
  171. package/dist/services/memory-retained-review-authority.js +129 -0
  172. package/dist/services/memory-retained-review-results.js +70 -0
  173. package/dist/services/memory-retained-second-baseline.js +102 -0
  174. package/dist/services/memory-retained-second-repairs.js +156 -0
  175. package/dist/services/memory-review-acceptance.js +214 -0
  176. package/dist/services/memory-review-correction-plan.js +171 -0
  177. package/dist/services/memory-review-handler.js +585 -0
  178. package/dist/services/memory-review-inputs.js +200 -0
  179. package/dist/services/memory-review-receipt.js +180 -0
  180. package/dist/services/memory-review-registration.js +108 -0
  181. package/dist/services/memory-review-work-custody.js +124 -0
  182. package/dist/services/memory-review-work-input.js +72 -0
  183. package/dist/services/memory-review-work-results.js +73 -0
  184. package/dist/services/memory-second-corrected-review-inputs.js +165 -0
  185. package/dist/services/memory-second-correction-plan.js +76 -0
  186. package/dist/services/memory-second-repair-baseline.js +91 -0
  187. package/dist/services/memory-second-repair-launch.js +614 -0
  188. package/dist/services/memory-second-repair-registration.js +88 -0
  189. package/dist/services/memory-second-repair-source.js +187 -0
  190. package/dist/services/memory-second-review-acceptance.js +115 -0
  191. package/dist/services/memory-source-observations.js +117 -0
  192. package/dist/services/memory-stage-dispatch.js +28 -0
  193. package/dist/services/memory-stage-results.js +199 -0
  194. package/dist/services/memory-verification-acceptance.js +135 -0
  195. package/dist/services/memory-verification-checks.js +252 -0
  196. package/dist/services/memory-verification-receipt.js +159 -0
  197. package/dist/services/memory-verify-handler.js +135 -0
  198. package/dist/services/memory-work-result-preparation.js +63 -0
  199. package/dist/services/projects.js +17 -2
  200. package/dist/services/recovery-snapshot-database.js +416 -1
  201. package/dist/services/repair-intents.js +1 -1
  202. package/dist/services/review-copy.js +10 -2
  203. package/dist/services/run-control.js +80 -9
  204. package/dist/services/run-controller-capture.js +10 -3
  205. package/dist/services/run-controller-process.js +21 -0
  206. package/dist/services/run-controller-recovery.js +3 -1
  207. package/dist/services/run-controller-state.js +1 -1
  208. package/dist/services/run-controller.js +184 -48
  209. package/dist/services/run-recovery-guard.js +2 -0
  210. package/dist/services/run-recovery.js +145 -5
  211. package/dist/services/run-start-custody.js +177 -0
  212. package/dist/services/runs.js +319 -25
  213. package/dist/services/stage-context.js +9 -1
  214. package/dist/services/stage-handlers.js +74 -0
  215. package/dist/services/stage-lifecycle.js +2 -1
  216. package/dist/services/stage-report-renderer.js +76 -0
  217. package/dist/services/stage-work-graph.js +17 -0
  218. package/dist/services/vnext-code.js +3 -3
  219. package/dist/services/vnext-execution-profile.js +2 -2
  220. package/dist/services/vnext-fanout.js +136 -10
  221. package/dist/services/vnext-merge.js +8 -3
  222. package/dist/services/vnext-plan-review.js +6 -4
  223. package/dist/services/vnext-protocolize.js +60 -2
  224. package/dist/services/work-registry.js +616 -71
  225. package/dist/services/workspace-files.js +1 -1
  226. package/dist/storage/database.js +84 -6
  227. package/dist/storage/writer-contract.js +11 -7
  228. package/package.json +13 -3
  229. package/tools/create-canon-release.mjs +33 -0
  230. package/tools/produce-hybrid-release.mjs +41 -0
@@ -0,0 +1,451 @@
1
+ import fs from "node:fs";
2
+ import { createHash } from "node:crypto";
3
+ import path from "node:path";
4
+ import { AppError } from "../shared/errors.js";
5
+ import { hybridDigest } from "./hybrid-contracts.js";
6
+ import { ObservedMemorySources } from "./memory-source-observations.js";
7
+ import { memorySourceKey } from "./memory-stage-results.js";
8
+ import { assertMemoryRepairWritePaths } from "./memory-review-correction-plan.js";
9
+ import { readBoundedReleaseFile } from "./release-source.js";
10
+ import { reviewEnvironmentAllowed } from "./review-copy.js";
11
+ const observationToken = Symbol("memory-repair-document-observation");
12
+ const transitionToken = Symbol("memory-repair-document-transition");
13
+ function ordered(entries) {
14
+ return [...entries].sort((a, b) => {
15
+ const left = a.path.split("/"), right = b.path.split("/");
16
+ for (let index = 0; index < Math.min(left.length, right.length); index++) {
17
+ if (left[index] !== right[index])
18
+ return left[index] < right[index] ? -1 : 1;
19
+ }
20
+ return left.length - right.length;
21
+ });
22
+ }
23
+ function refuse(reason) {
24
+ throw new AppError("memory_repair_document_delta_invalid", "Physical repair documents differ from their bounded baseline", 2, { reason, effect: "no_effect" });
25
+ }
26
+ const alias = (logical) => logical.normalize("NFC").toLowerCase();
27
+ function inventory(root) {
28
+ if (!path.isAbsolute(root) || fs.realpathSync(root) !== path.resolve(root) || !fs.lstatSync(root).isDirectory())
29
+ refuse("noncanonical_workspace");
30
+ const entries = [], aliases = new Set();
31
+ const visit = (logical, depth) => {
32
+ if (depth > 64 || entries.length >= 20_000 || logical.length > 4096)
33
+ refuse("unbounded_document_inventory");
34
+ const file = path.join(root, logical), stat = fs.lstatSync(file);
35
+ if (stat.isSymbolicLink() || !stat.isDirectory() && !stat.isFile() || stat.isFile() && stat.nlink !== 1)
36
+ refuse("linked_or_special_document");
37
+ if (aliases.has(alias(logical)))
38
+ refuse("aliased_document_inventory");
39
+ aliases.add(alias(logical));
40
+ entries.push({ path: logical, kind: stat.isDirectory() ? "directory" : "file", mode: stat.mode & 0o7777, sha256: null });
41
+ if (stat.isDirectory())
42
+ for (const name of fs.readdirSync(file).sort()) {
43
+ if (!name || name.includes("\\") || name.includes("\0") || name === "." || name === "..")
44
+ refuse("unsafe_document_name");
45
+ visit(`${logical}/${name}`, depth + 1);
46
+ }
47
+ };
48
+ visit(".memory-bank", 0);
49
+ return entries;
50
+ }
51
+ function capture(root) {
52
+ try {
53
+ const initial = inventory(root);
54
+ const files = initial.filter(entry => entry.kind === "file");
55
+ const evidenceAllowed = (logical) => logical.split("/").every(part => reviewEnvironmentAllowed(alias(part))
56
+ && ![".git", "node_modules", ".dd-flow"].includes(alias(part)));
57
+ let size = 0;
58
+ for (const entry of files) {
59
+ const bytes = fs.lstatSync(path.join(root, entry.path)).size;
60
+ if (bytes > 16 * 1024 * 1024 || (size += bytes) > 64 * 1024 * 1024)
61
+ refuse("documents_unavailable_or_changed");
62
+ }
63
+ // Preservation observes private files only as opaque hashes. Their bytes
64
+ // never enter an evidence owner or the sanitized writer copy.
65
+ const protectedHashes = () => new Map(files.filter(entry => !evidenceAllowed(entry.path)).map(entry => {
66
+ let parent = root;
67
+ for (const part of entry.path.split("/").slice(0, -1)) {
68
+ parent = path.join(parent, part);
69
+ if (!fs.lstatSync(parent).isDirectory())
70
+ refuse("linked_or_special_document");
71
+ }
72
+ return [memorySourceKey("workspace", entry.path), createHash("sha256")
73
+ .update(readBoundedReleaseFile(path.join(root, entry.path), 16 * 1024 * 1024)).digest("hex")];
74
+ }));
75
+ const protectedBefore = protectedHashes();
76
+ const observed = ObservedMemorySources.capture({ workspace: root }, files.filter(entry => evidenceAllowed(entry.path))
77
+ .map(entry => ({ root: "workspace", path: entry.path })));
78
+ if (hybridDigest(inventory(root)) !== hybridDigest(initial))
79
+ refuse("inventory_changed_during_capture");
80
+ observed.revalidate();
81
+ if (hybridDigest([...protectedHashes()]) !== hybridDigest([...protectedBefore]))
82
+ refuse("inventory_changed_during_capture");
83
+ if (hybridDigest(inventory(root)) !== hybridDigest(initial))
84
+ refuse("inventory_changed_during_capture");
85
+ const hashes = new Map([...observed.hashes(), ...protectedBefore]), entries = initial.map(entry => ({ ...entry,
86
+ sha256: entry.kind === "file" ? hashes.get(memorySourceKey("workspace", entry.path)) : null }));
87
+ return { entries, observed, sha256: hybridDigest(entries) };
88
+ }
89
+ catch (error) {
90
+ if (error instanceof AppError && error.code === "memory_repair_document_delta_invalid")
91
+ throw error;
92
+ return refuse("documents_unavailable_or_changed");
93
+ }
94
+ }
95
+ /** Physical MB-only observation, not provider/Session authority or permission to
96
+ * write the shared workspace. A registered owner must bind this start baseline,
97
+ * inspect the rest of its private workspace and journal bounded publication.
98
+ * It cannot use this helper alone to accept a Work or close a review finding.
99
+ */
100
+ export class MemoryRepairDocumentBaseline {
101
+ sha256;
102
+ #entries;
103
+ #paths;
104
+ constructor(entries, paths) {
105
+ this.#entries = structuredClone(entries);
106
+ this.#paths = [...paths];
107
+ this.sha256 = hybridDigest({ entries, write_paths: paths });
108
+ Object.freeze(this);
109
+ }
110
+ static capture(workspace, writePaths) {
111
+ assertMemoryRepairWritePaths(writePaths);
112
+ const snapshot = capture(workspace), byPath = new Map(snapshot.entries.map(entry => [entry.path, entry]));
113
+ for (const logical of writePaths) {
114
+ if (byPath.get(logical)?.kind === "directory")
115
+ refuse("write_path_is_directory");
116
+ if (snapshot.entries.some(entry => alias(entry.path) === alias(logical) && entry.path !== logical))
117
+ refuse("aliased_write_path");
118
+ const parts = logical.split("/");
119
+ for (let index = 1; index < parts.length; index++) {
120
+ const parent = parts.slice(0, index).join("/");
121
+ if (byPath.get(parent)?.kind === "file" || snapshot.entries.some(entry => alias(entry.path) === alias(parent) && entry.path !== parent))
122
+ refuse("invalid_write_parent");
123
+ }
124
+ }
125
+ return new MemoryRepairDocumentBaseline(snapshot.entries, writePaths);
126
+ }
127
+ entries() { return structuredClone(this.#entries); }
128
+ /** Adapt an independently accepted document view to the next registered
129
+ * Work's approved paths without re-capturing mutable workspace bytes.
130
+ * The caller still owns graph/current-view authority; this grants none.
131
+ */
132
+ withWritePaths(writePaths) {
133
+ assertMemoryRepairWritePaths(writePaths);
134
+ const value = { schema_id: "dd-flow/memory-repair-document-baseline@1", entries: this.entries(), write_paths: [...writePaths] };
135
+ return MemoryRepairDocumentBaseline.restore(Buffer.from(JSON.stringify(value)), hybridDigest({ entries: value.entries, write_paths: value.write_paths }));
136
+ }
137
+ /** Metadata composition only, never admission or filesystem publication.
138
+ * A caller must independently prove both retained views belong to a settled
139
+ * repair. Disjoint concurrent deltas preserve the current inventory; file
140
+ * conflicts never rebase. Identical new parent directories may be shared.
141
+ */
142
+ composeSettledDelta(before, after) {
143
+ if (!(before instanceof MemoryRepairDocumentBaseline) || !(after instanceof MemoryRepairDocumentBaseline)
144
+ || hybridDigest(before.#paths) !== hybridDigest(after.#paths))
145
+ refuse("invalid_settled_document_views");
146
+ const prior = new Map(before.#entries.map(entry => [entry.path, entry]));
147
+ const final = new Map(after.#entries.map(entry => [entry.path, entry]));
148
+ const current = new Map(this.#entries.map(entry => [entry.path, structuredClone(entry)]));
149
+ let changedFiles = 0;
150
+ for (const logical of new Set([...prior.keys(), ...final.keys()])) {
151
+ const old = prior.get(logical) ?? null, next = final.get(logical) ?? null;
152
+ if (hybridDigest(old) === hybridDigest(next))
153
+ continue;
154
+ const present = current.get(logical) ?? null;
155
+ if (next?.kind === "directory") {
156
+ if (old !== null || ![0o700, 0o755].includes(next.mode)
157
+ || !after.#entries.some(entry => entry.kind === "file" && before.#paths.includes(entry.path) && entry.path.startsWith(`${logical}/`)))
158
+ refuse("invalid_settled_directory_delta");
159
+ if (present !== null && hybridDigest(present) !== hybridDigest(next))
160
+ refuse("settled_document_conflict");
161
+ }
162
+ else {
163
+ if (!before.#paths.includes(logical) || old?.kind === "directory" || next && next.kind !== "file"
164
+ || next && (old ? next.mode !== old.mode : ![0o600, 0o644].includes(next.mode))
165
+ || old && next && old.sha256 === next.sha256)
166
+ refuse("invalid_settled_file_delta");
167
+ if (hybridDigest(present) !== hybridDigest(old))
168
+ refuse("settled_document_conflict");
169
+ changedFiles++;
170
+ }
171
+ if (next)
172
+ current.set(logical, structuredClone(next));
173
+ else
174
+ current.delete(logical);
175
+ }
176
+ if (!changedFiles)
177
+ refuse("empty_settled_document_delta");
178
+ const entries = ordered(current.values()), value = { schema_id: "dd-flow/memory-repair-document-baseline@1", entries, write_paths: [...this.#paths] };
179
+ return MemoryRepairDocumentBaseline.restore(Buffer.from(JSON.stringify(value)), hybridDigest({ entries, write_paths: value.write_paths }));
180
+ }
181
+ /** Project only the independently selected regular files into a sanitized
182
+ * writer view. The protected source snapshot stays intact. This is metadata
183
+ * derivation, not file copying or authority to choose more source inputs.
184
+ */
185
+ exported(selected) {
186
+ if (!Array.isArray(selected) || selected.length > 20_000 || selected.some(logical => typeof logical !== "string"))
187
+ refuse("invalid_export_selection");
188
+ const byPath = new Map(this.#entries.map(entry => [entry.path, entry])), retained = new Set([".memory-bank"]);
189
+ const allowed = (logical) => logical.split("/").every(part => reviewEnvironmentAllowed(alias(part))
190
+ && ![".git", "node_modules", ".dd-flow"].includes(alias(part)));
191
+ const parents = (logical) => {
192
+ const parts = logical.split("/");
193
+ for (let index = 1; index < parts.length; index++) {
194
+ const parent = parts.slice(0, index).join("/");
195
+ if (byPath.get(parent)?.kind === "directory")
196
+ retained.add(parent);
197
+ }
198
+ };
199
+ for (const logical of selected) {
200
+ if (!logical.startsWith(".memory-bank/") || logical.length > 4096 || logical.includes("\\") || logical.includes("\0")
201
+ || logical.split("/").some(part => !part || part === "." || part === ".."))
202
+ refuse("invalid_export_selection");
203
+ if (!allowed(logical))
204
+ continue;
205
+ if (byPath.get(logical)?.kind !== "file")
206
+ refuse("selected_export_file_missing");
207
+ if (retained.has(logical))
208
+ refuse("repeated_export_selection");
209
+ retained.add(logical);
210
+ parents(logical);
211
+ }
212
+ for (const logical of this.#paths) {
213
+ if (byPath.has(logical) && !retained.has(logical))
214
+ refuse("write_target_not_exported");
215
+ parents(logical);
216
+ }
217
+ const exported = new MemoryRepairDocumentBaseline(this.#entries.filter(entry => retained.has(entry.path)), this.#paths);
218
+ // Apply the same bounded, strict representation used by SQL restoration.
219
+ return MemoryRepairDocumentBaseline.restore(exported.bytes(), exported.sha256);
220
+ }
221
+ bytes() {
222
+ const bytes = Buffer.from(JSON.stringify({ schema_id: "dd-flow/memory-repair-document-baseline@1", entries: this.#entries, write_paths: this.#paths }));
223
+ if (bytes.length > 8 * 1024 * 1024)
224
+ refuse("unbounded_baseline_receipt");
225
+ return bytes;
226
+ }
227
+ /** Decode an independently retained receipt, not an agent result. The caller
228
+ * still has to prove SQL/start ownership of expectedSha256 before use.
229
+ */
230
+ static restore(bytes, expectedSha256) {
231
+ try {
232
+ if (!(bytes instanceof Uint8Array) || !bytes.length || bytes.length > 8 * 1024 * 1024
233
+ || typeof expectedSha256 !== "string" || !/^[a-f0-9]{64}$/.test(expectedSha256))
234
+ refuse("invalid_baseline_receipt");
235
+ const value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
236
+ if (!value || Object.keys(value).sort().join(",") !== "entries,schema_id,write_paths"
237
+ || value.schema_id !== "dd-flow/memory-repair-document-baseline@1" || !Array.isArray(value.entries)
238
+ || !value.entries.length || value.entries.length > 20_000)
239
+ refuse("invalid_baseline_receipt");
240
+ assertMemoryRepairWritePaths(value.write_paths);
241
+ const byPath = new Map(), aliases = new Set();
242
+ for (const entry of value.entries) {
243
+ if (!entry || Object.keys(entry).sort().join(",") !== "kind,mode,path,sha256"
244
+ || typeof entry.path !== "string" || entry.path.length > 4096 || entry.path.split("/").length > 65
245
+ || entry.path !== ".memory-bank" && !entry.path.startsWith(".memory-bank/")
246
+ || entry.path.includes("\\") || entry.path.includes("\0") || entry.path.split("/").some(part => !part || part === "." || part === "..")
247
+ || !["file", "directory"].includes(entry.kind) || !Number.isInteger(entry.mode) || entry.mode < 0 || entry.mode > 0o7777
248
+ || entry.kind === "directory" && entry.sha256 !== null
249
+ || entry.kind === "file" && (typeof entry.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(entry.sha256))
250
+ || aliases.has(alias(entry.path)))
251
+ refuse("invalid_baseline_receipt");
252
+ if (entry.path === ".memory-bank" ? byPath.size !== 0 || entry.kind !== "directory"
253
+ : byPath.get(path.posix.dirname(entry.path))?.kind !== "directory")
254
+ refuse("invalid_baseline_receipt");
255
+ aliases.add(alias(entry.path));
256
+ byPath.set(entry.path, entry);
257
+ }
258
+ for (const logical of value.write_paths) {
259
+ if (byPath.get(logical)?.kind === "directory" || value.entries.some((entry) => alias(entry.path) === alias(logical) && entry.path !== logical))
260
+ refuse("invalid_baseline_receipt");
261
+ const parts = logical.split("/");
262
+ for (let index = 1; index < parts.length; index++) {
263
+ const parent = parts.slice(0, index).join("/");
264
+ if (byPath.get(parent)?.kind === "file" || value.entries.some((entry) => alias(entry.path) === alias(parent) && entry.path !== parent))
265
+ refuse("invalid_baseline_receipt");
266
+ }
267
+ }
268
+ const prepared = new MemoryRepairDocumentBaseline(value.entries, value.write_paths);
269
+ if (prepared.sha256 !== expectedSha256 || !prepared.bytes().equals(Buffer.from(bytes)))
270
+ refuse("baseline_receipt_changed");
271
+ return prepared;
272
+ }
273
+ catch (error) {
274
+ if (error instanceof AppError && error.code === "memory_repair_document_delta_invalid")
275
+ throw error;
276
+ return refuse("invalid_baseline_receipt");
277
+ }
278
+ }
279
+ revalidate(workspace) {
280
+ if (hybridDigest(capture(workspace).entries) !== hybridDigest(this.#entries))
281
+ refuse("documents_changed_after_baseline");
282
+ }
283
+ /** Derive the protected shared-workspace transition from an authenticated
284
+ * private-copy observation. Excluded/private/canonical source entries are
285
+ * retained, never inferred from the sanitized execution copy. This is not
286
+ * permission to publish or settle a Work.
287
+ */
288
+ publicationTransition(exported, delta, startProtected = this) {
289
+ if (!ObservedMemoryRepairDelta.isObserved(delta))
290
+ refuse("physical_observation_required");
291
+ if (delta.baselineSha256 !== exported.sha256 || startProtected.exported(exported.entries().filter(entry => entry.kind === "file").map(entry => entry.path)).sha256 !== exported.sha256)
292
+ refuse("foreign_publication_baseline");
293
+ const before = new Map(this.#entries.map(entry => [entry.path, entry]));
294
+ const start = new Map(startProtected.entries().map(entry => [entry.path, entry]));
295
+ const visible = new Map(exported.entries().map(entry => [entry.path, entry]));
296
+ const observed = new Map(delta.afterEntries().map(entry => [entry.path, entry]));
297
+ const actions = [], added = new Set();
298
+ for (const change of delta.changes()) {
299
+ if (!this.#paths.includes(change.path) || (before.get(change.path)?.sha256 ?? null) !== change.before_sha256
300
+ || hybridDigest(before.get(change.path) ?? null) !== hybridDigest(start.get(change.path) ?? null))
301
+ refuse("foreign_publication_delta");
302
+ const next = observed.get(change.path) ?? null;
303
+ if ((next?.sha256 ?? null) !== change.after_sha256 || next && next.kind !== "file")
304
+ refuse("foreign_publication_delta");
305
+ if (next) {
306
+ const parts = change.path.split("/");
307
+ for (let length = 1; length < parts.length; length++) {
308
+ const parent = parts.slice(0, length).join("/");
309
+ const currentParent = before.get(parent), originalParent = start.get(parent);
310
+ if (currentParent && (currentParent.kind !== "directory"
311
+ || hybridDigest(currentParent) !== hybridDigest(originalParent ?? observed.get(parent) ?? null)))
312
+ refuse("foreign_publication_parent");
313
+ if (!before.has(parent) && !added.has(parent)) {
314
+ const entry = observed.get(parent);
315
+ if (visible.has(parent) || entry?.kind !== "directory")
316
+ refuse("foreign_publication_parent");
317
+ actions.push({ path: parent, before: null, after: entry });
318
+ added.add(parent);
319
+ }
320
+ }
321
+ }
322
+ actions.push({ path: change.path, before: before.get(change.path) ?? null, after: next });
323
+ }
324
+ const transition = new MemoryRepairDocumentTransition(transitionToken, this, delta, actions, startProtected.sha256);
325
+ // Apply the strict retained representation before a publication owner can
326
+ // register this expected view or perform any shared filesystem mutation.
327
+ transition.after();
328
+ return transition;
329
+ }
330
+ observeResult(workspace, result) {
331
+ const snapshot = capture(workspace), before = new Map(this.#entries.map(entry => [entry.path, entry]));
332
+ const after = new Map(snapshot.entries.map(entry => [entry.path, entry]));
333
+ const changed = [...new Set([...before.keys(), ...after.keys()])].sort().filter(logical => hybridDigest(before.get(logical) ?? null) !== hybridDigest(after.get(logical) ?? null));
334
+ const changes = [];
335
+ for (const logical of changed) {
336
+ const old = before.get(logical), current = after.get(logical);
337
+ if (old?.kind === "directory" || current?.kind === "directory") {
338
+ // Creating declared documents can create their parent directories; no
339
+ // unrelated empty directory, removed directory or chmod is a delta.
340
+ if (old || current?.kind !== "directory" || ![0o700, 0o755].includes(current.mode)
341
+ || !result.changes.some(change => change.after_sha256 !== null && change.path.startsWith(`${logical}/`)))
342
+ refuse("unregistered_directory_delta");
343
+ continue;
344
+ }
345
+ if (!this.#paths.includes(logical))
346
+ refuse("undeclared_document_delta");
347
+ if (old && current && old.mode !== current.mode || !old && current && ![0o600, 0o644].includes(current.mode))
348
+ refuse("document_mode_delta");
349
+ if (old?.sha256 === current?.sha256)
350
+ refuse("noncontent_document_delta");
351
+ changes.push({ path: logical, before_sha256: old?.sha256 ?? null, after_sha256: current?.sha256 ?? null });
352
+ }
353
+ const proposed = [...result.changes].sort((a, b) => a.path.localeCompare(b.path, "en"));
354
+ changes.sort((a, b) => a.path.localeCompare(b.path, "en"));
355
+ if (hybridDigest(changes) !== hybridDigest(proposed))
356
+ refuse("claimed_delta_differs_from_bytes");
357
+ if (result.outcome === "blocked" && changed.length || result.outcome === "accepted" && !changes.length)
358
+ refuse("outcome_differs_from_physical_delta");
359
+ return new ObservedMemoryRepairDelta(observationToken, this.sha256, workspace, snapshot.sha256, changes, snapshot.observed, snapshot.entries);
360
+ }
361
+ }
362
+ export class ObservedMemoryRepairDelta {
363
+ static isObserved(value) {
364
+ return typeof value === "object" && value !== null && #observed in value;
365
+ }
366
+ sha256;
367
+ baselineSha256;
368
+ #workspace;
369
+ #snapshotSha256;
370
+ #changes;
371
+ #observed;
372
+ #after;
373
+ constructor(token, baselineSha256, workspace, snapshotSha256, changes, observed, after) {
374
+ if (token !== observationToken)
375
+ refuse("physical_observation_required");
376
+ this.baselineSha256 = baselineSha256;
377
+ this.#workspace = workspace;
378
+ this.#snapshotSha256 = snapshotSha256;
379
+ this.#changes = structuredClone(changes);
380
+ this.#observed = observed;
381
+ this.#after = structuredClone(after);
382
+ this.sha256 = hybridDigest({ baseline_sha256: baselineSha256, snapshot_sha256: snapshotSha256, changes });
383
+ Object.freeze(this);
384
+ }
385
+ changes() { return structuredClone(this.#changes); }
386
+ afterEntries() { return structuredClone(this.#after); }
387
+ bytes(logical) {
388
+ if (!this.#changes.some(change => change.path === logical && change.after_sha256 !== null))
389
+ refuse("unselected_delta_bytes");
390
+ return this.#observed.bytes({ root: "workspace", path: logical });
391
+ }
392
+ revalidate() { if (capture(this.#workspace).sha256 !== this.#snapshotSha256)
393
+ refuse("documents_changed_after_delta"); }
394
+ }
395
+ /** Exact publication prefix model. A SQL/Session-owned publisher must admit
396
+ * the transition independently before applying these actions. This component
397
+ * only derives and observes views; it never mutates the shared workspace.
398
+ */
399
+ export class MemoryRepairDocumentTransition {
400
+ sha256;
401
+ #before;
402
+ #delta;
403
+ #actions;
404
+ constructor(token, before, delta, actions, startProtectedSha256) {
405
+ if (token !== transitionToken)
406
+ refuse("publication_transition_required");
407
+ this.#before = before;
408
+ this.#delta = delta;
409
+ this.#actions = structuredClone(actions);
410
+ this.sha256 = hybridDigest({ before_sha256: before.sha256, start_protected_sha256: startProtectedSha256, delta_sha256: delta.sha256, actions });
411
+ Object.freeze(this);
412
+ }
413
+ actions() { return structuredClone(this.#actions); }
414
+ state(completed) {
415
+ if (!Number.isSafeInteger(completed) || completed < 0 || completed > this.#actions.length)
416
+ refuse("invalid_publication_cursor");
417
+ const entries = new Map(this.#before.entries().map(entry => [entry.path, entry]));
418
+ for (const action of this.#actions.slice(0, completed)) {
419
+ if (action.after)
420
+ entries.set(action.path, action.after);
421
+ else
422
+ entries.delete(action.path);
423
+ }
424
+ const retained = JSON.parse(this.#before.bytes().toString("utf8"));
425
+ const value = { ...retained, entries: ordered(entries.values()) };
426
+ const expected = hybridDigest({ entries: value.entries, write_paths: value.write_paths });
427
+ return MemoryRepairDocumentBaseline.restore(Buffer.from(JSON.stringify(value)), expected);
428
+ }
429
+ after() { return this.state(this.#actions.length); }
430
+ classify(workspace) {
431
+ const actual = capture(workspace), entries = new Map(actual.entries.map(entry => [entry.path, entry]));
432
+ let completed = 0, pending = false;
433
+ for (const action of this.#actions) {
434
+ const current = entries.get(action.path) ?? null;
435
+ if (hybridDigest(current) === hybridDigest(action.after)) {
436
+ if (pending)
437
+ refuse("publication_not_known_prefix");
438
+ completed++;
439
+ }
440
+ else if (hybridDigest(current) === hybridDigest(action.before))
441
+ pending = true;
442
+ else
443
+ refuse("publication_not_known_prefix");
444
+ }
445
+ if (hybridDigest(actual.entries) !== hybridDigest(this.state(completed).entries()))
446
+ refuse("publication_not_known_prefix");
447
+ return { completed, total: this.#actions.length, status: completed === this.#actions.length ? "after" : completed === 0 ? "before" : "partial" };
448
+ }
449
+ bytes(logical) { return this.#delta.bytes(logical); }
450
+ revalidateDelta() { this.#delta.revalidate(); }
451
+ }