@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,199 @@
1
+ import path from "node:path";
2
+ import { createHash } from "node:crypto";
3
+ import { AppError } from "../shared/errors.js";
4
+ import { hybridDigest } from "./hybrid-contracts.js";
5
+ import { runtimeHarness } from "./execution-policy.js";
6
+ import { PreparedMemoryStageResult, memorySourceKey } from "./memory-stage-results.js";
7
+ import { ObservedMemorySources } from "./memory-source-observations.js";
8
+ import { readWorkBatchCustody } from "./work-registry.js";
9
+ import { readMemoryCompositionRegistration, readRegisteredMemoryComposition } from "./memory-composition-registration.js";
10
+ import { validateMemoryCompositionWorkResult } from "./memory-composition-work-results.js";
11
+ const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
12
+ function refuse(reason) {
13
+ throw new AppError("memory_composition_acceptance_invalid", "Composition lacks registered, settled and current output custody", 2, { reason, effect: "no_effect" });
14
+ }
15
+ function object(bytes, reason) {
16
+ try {
17
+ const value = JSON.parse(bytes);
18
+ if (!value || typeof value !== "object" || Array.isArray(value))
19
+ refuse(reason);
20
+ return value;
21
+ }
22
+ catch {
23
+ return refuse(reason);
24
+ }
25
+ }
26
+ function roots(context, scope) {
27
+ const row = context.db.get("SELECT workspace_root,run_root,run_dir FROM runs WHERE project_id=? AND id=?", [scope.project_id, scope.run_id]);
28
+ if (!row || !path.isAbsolute(row.workspace_root) || !path.isAbsolute(row.run_root ?? row.run_dir))
29
+ refuse("assigned_roots_missing");
30
+ return { workspace: row.workspace_root, run: row.run_root ?? row.run_dir };
31
+ }
32
+ /** SQL-only selection. Neither a completed status nor a mutable result file
33
+ * can substitute for the output-preparation receipt committed by the memory
34
+ * Work writer together with its result and WorkSession.
35
+ */
36
+ function settled(context, scope, workId, harness, registrationSha256, assigned) {
37
+ const allocation = readWorkBatchCustody(context, workId);
38
+ if (hybridDigest(allocation.scope) !== hybridDigest(scope))
39
+ refuse("foreign_work_allocation");
40
+ const work = context.db.get("SELECT status,result,result_schema FROM works WHERE project_id=? AND run_id=? AND work_id=?", [scope.project_id, scope.run_id, workId]);
41
+ if (!work || work.status !== "completed" || work.result_schema !== "dd-flow/memory-composition-work-result@1"
42
+ || typeof work.result !== "string" || !work.result.length || Buffer.byteLength(work.result) > 1024 * 1024)
43
+ refuse("work_not_settled");
44
+ const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type='memory.composition_work_settlement' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.work_id')=? LIMIT 2", [scope.project_id, scope.run_id, workId]);
45
+ if (rows.length !== 1 || Buffer.byteLength(rows[0].payload_json) > 32768)
46
+ refuse("work_settlement_receipt_missing");
47
+ const receipt = object(rows[0].payload_json, "invalid_work_settlement_receipt");
48
+ const links = context.db.all("SELECT id,session_id,status,result_path,result_receipt_json FROM work_sessions WHERE work_id=? AND status='completed' LIMIT 2", [workId]);
49
+ if (links.length !== 1 || !links[0].result_receipt_json || Buffer.byteLength(links[0].result_receipt_json) > 32768
50
+ || context.db.get("SELECT 1 FROM work_sessions WHERE work_id=? AND status='running'", [workId]))
51
+ refuse("completed_work_session_missing");
52
+ const link = links[0], publication = object(link.result_receipt_json, "invalid_work_result_receipt");
53
+ const session = context.db.get("SELECT provider_session_id FROM sessions WHERE project_id=? AND session_id=? AND run_id=? AND harness=?", [scope.project_id, link.session_id, scope.run_id, harness]);
54
+ if (!session)
55
+ refuse("work_session_identity_changed");
56
+ const bytes = Buffer.from(work.result), resultSha256 = hash(bytes);
57
+ if (hybridDigest(receipt) !== hybridDigest({ schema_id: "dd-flow/memory-composition-work-settlement@1", run_id: scope.run_id, work_id: workId,
58
+ work_session_id: link.id, session_id: link.session_id, harness, provider_session_id: session.provider_session_id,
59
+ scope, registration_sha256: registrationSha256, allocation_sha256: hybridDigest(allocation), roots: assigned, result_sha256: resultSha256 }))
60
+ refuse("work_settlement_custody_changed");
61
+ if (publication.work_id !== workId || publication.work_session_id !== link.id || publication.status !== "completed" || publication.result_sha256 !== resultSha256
62
+ || publication.previous_sha256 !== null && (typeof publication.previous_sha256 !== "string" || !/^[a-f0-9]{64}$/.test(publication.previous_sha256)))
63
+ refuse("work_result_receipt_changed");
64
+ if (link.result_path !== path.join(assigned.run, "works", workId, "result.json"))
65
+ refuse("work_result_path_changed");
66
+ return { bytes, authority: hybridDigest({ receipt, publication }), assigned,
67
+ evidence: { root: "run", path: `works/${workId}/result.json`, sha256: resultSha256 } };
68
+ }
69
+ /** Final compose output custody, not semantic verification/review/finish.
70
+ * The blueprint is restored from registration; it is never rewritten using
71
+ * the final result. Historical worker bytes are schema-checked without
72
+ * asserting that a dependent assembly left their artifacts untouched.
73
+ */
74
+ export class PreparedMemoryCompositionAcceptance {
75
+ #scope;
76
+ #original;
77
+ #registrationSha256;
78
+ #workers;
79
+ #artifacts;
80
+ #observed;
81
+ constructor(scope, original, registrationSha256, workers, artifacts, observed) {
82
+ this.#scope = structuredClone(scope);
83
+ this.#original = original;
84
+ this.#registrationSha256 = registrationSha256;
85
+ this.#workers = workers;
86
+ this.#artifacts = artifacts;
87
+ this.#observed = observed;
88
+ Object.freeze(this);
89
+ }
90
+ static prepare(context, input) {
91
+ const registration = readMemoryCompositionRegistration(context, input.scope);
92
+ const original = readRegisteredMemoryComposition(context, input), value = original.value();
93
+ const works = value.details.works;
94
+ const batch = original.compositionBatch({ runId: input.scope.run_id, inputSha256: input.binding.inputSha256, executionSha256: input.binding.executionSha256, resultSchema: "dd-flow/memory-composition-work-result@1" });
95
+ const routes = new Map(original.compositionRoutes().map(route => [route.key, runtimeHarness(route.execution.worker)]));
96
+ const registrationSha256 = hybridDigest(registration), assigned = roots(context, input.scope);
97
+ const selected = context.db.readSnapshot(() => works.map(work => {
98
+ const id = registration.work_ids[work.key], harness = routes.get(work.key);
99
+ return { key: work.key, id, harness, ...settled(context, input.scope, id, harness, registrationSha256, assigned) };
100
+ }));
101
+ const outputs = new Map(selected.map(item => [item.key, validateMemoryCompositionWorkResult(item.bytes, input.engine, {
102
+ runId: input.scope.run_id, inputSha256: input.binding.inputSha256, executionSha256: input.binding.executionSha256,
103
+ workId: item.id, workKey: item.key, composition: original, compositionSha256: original.sha256
104
+ })]));
105
+ // The admitted graph has ordered all overlapping writers. Use its real
106
+ // dependencies, not array order, wall-clock timestamps or agent claims.
107
+ const remaining = new Map(works.map(work => [work.key, work.depends_on.length])), children = new Map();
108
+ for (const work of works)
109
+ for (const dependency of work.depends_on)
110
+ children.set(dependency, [...(children.get(dependency) ?? []), work.key]);
111
+ const ready = works.filter(work => !work.depends_on.length).map(work => work.key);
112
+ const current = new Map(), aliases = new Map();
113
+ for (let index = 0; index < ready.length; index++) {
114
+ const key = ready[index], output = outputs.get(key), emitted = new Map();
115
+ for (const unit of output.units)
116
+ for (const artifact of unit.artifacts) {
117
+ const identity = memorySourceKey(artifact.root, artifact.path), alias = memorySourceKey(artifact.root, artifact.path.normalize("NFC").toLowerCase());
118
+ if (aliases.has(alias) && aliases.get(alias) !== identity)
119
+ refuse("aliased_output_paths");
120
+ aliases.set(alias, identity);
121
+ const record = emitted.get(identity) ?? { artifact, units: new Set() };
122
+ record.units.add(unit.id);
123
+ emitted.set(identity, record);
124
+ }
125
+ for (const [identity, record] of emitted)
126
+ current.set(identity, record);
127
+ for (const child of children.get(key) ?? []) {
128
+ const count = remaining.get(child) - 1;
129
+ remaining.set(child, count);
130
+ if (!count)
131
+ ready.push(child);
132
+ }
133
+ }
134
+ if (ready.length !== batch.works.length)
135
+ refuse("registered_graph_changed");
136
+ const artifacts = new Map();
137
+ for (const record of current.values())
138
+ for (const id of record.units)
139
+ artifacts.set(id, [...(artifacts.get(id) ?? []), record.artifact]);
140
+ const observed = ObservedMemorySources.capture(assigned, [...current.values()].map(record => ({ root: record.artifact.root, path: record.artifact.path })));
141
+ const hashes = observed.hashes();
142
+ if ([...current.values()].some(record => hashes.get(memorySourceKey(record.artifact.root, record.artifact.path)) !== record.artifact.sha256))
143
+ refuse("current_output_bytes_changed");
144
+ const prepared = new PreparedMemoryCompositionAcceptance(input.scope, original, registrationSha256, selected.map(item => ({ id: item.id, harness: item.harness, authority: item.authority, evidence: item.evidence })), artifacts, observed);
145
+ prepared.assertCustody(context);
146
+ return prepared;
147
+ }
148
+ /** Only these independently settled output locations may supplement baseline
149
+ * evidence in the final result binding. The returned map is a defensive copy.
150
+ */
151
+ outputHashes() { return this.#observed.hashes(); }
152
+ historicalEvidence() {
153
+ const units = this.#original.value().units;
154
+ return new Set(units.flatMap(unit => unit.evidence.map(ref => `${memorySourceKey(ref.root, ref.path)}\0${ref.sha256}`)));
155
+ }
156
+ resultEvidence() { return this.#workers.map(worker => structuredClone(worker.evidence)); }
157
+ outputLocations() {
158
+ const selected = new Map();
159
+ for (const artifacts of this.#artifacts.values())
160
+ for (const artifact of artifacts)
161
+ selected.set(memorySourceKey(artifact.root, artifact.path), { root: artifact.root, path: artifact.path });
162
+ return [...selected.values()];
163
+ }
164
+ assertResult(result) {
165
+ if (!(result instanceof PreparedMemoryStageResult))
166
+ refuse("prepared_result_required");
167
+ const original = this.#original.value(), final = result.value(), value = final;
168
+ const identity = ["schema_id", "run_id", "input_sha256", "predecessors"];
169
+ if (hybridDigest(identity.map(field => original[field])) !== hybridDigest(identity.map(field => final[field])))
170
+ refuse("foreign_result_identity");
171
+ if (value.stage !== "compose" || value.outcome !== "accepted" || hybridDigest(value.details) !== hybridDigest(original.details)
172
+ || value.findings.some(finding => finding.severity !== "minor" && finding.status !== "resolved"))
173
+ refuse("composition_not_accepted_or_plan_changed");
174
+ const inventory = new Map(original.units.map(unit => [unit.id, unit]));
175
+ if (value.units.length !== inventory.size || new Set(value.units.map(unit => unit.id)).size !== inventory.size)
176
+ refuse("changed_unit_coverage");
177
+ for (const unit of value.units) {
178
+ const initial = inventory.get(unit.id);
179
+ if (!initial || unit.aspect !== initial.aspect || unit.applicability !== initial.applicability || unit.applicability === "unknown"
180
+ || unit.outcome !== (unit.applicability === "not-applicable" ? "not-applicable" : "accepted"))
181
+ refuse("unaccepted_unit_or_changed_assignment");
182
+ const expected = this.#artifacts.get(unit.id) ?? [], identities = new Set(unit.artifacts.map(artifact => hybridDigest(artifact)));
183
+ if (unit.applicability !== "not-applicable" && !expected.length || identities.size !== expected.length
184
+ || expected.some(artifact => !identities.has(hybridDigest(artifact))))
185
+ refuse("unit_outputs_not_settled");
186
+ }
187
+ }
188
+ assertCustody(context) {
189
+ context.db.readSnapshot(() => {
190
+ if (hybridDigest(readMemoryCompositionRegistration(context, this.#scope)) !== this.#registrationSha256)
191
+ refuse("registration_changed");
192
+ const assigned = roots(context, this.#scope);
193
+ for (const worker of this.#workers)
194
+ if (settled(context, this.#scope, worker.id, worker.harness, this.#registrationSha256, assigned).authority !== worker.authority)
195
+ refuse("work_settlement_changed");
196
+ });
197
+ }
198
+ revalidate(context) { this.#observed.revalidate(); this.assertCustody(context); }
199
+ }
@@ -0,0 +1,134 @@
1
+ import { AppError } from "../shared/errors.js";
2
+ import { localEnvironmentAllowed } from "./workspace-files.js";
3
+ function refuse(reason) {
4
+ throw new AppError("memory_stage_result_invalid", "Memory composition graph cannot be dispatched", 2, { reason, phase: "prepare", effect: "no_effect" });
5
+ }
6
+ // Portable scopes must remain safe on case-insensitive/normalizing filesystems.
7
+ const physicalPath = (value) => value.normalize("NFC").toLowerCase();
8
+ const projectConfigurationPaths = [".memory-bank/dd-flow/project-execution.json", ".memory-bank/dd-flow/project-workspace.json"];
9
+ /** Exact project-owned defaults, never directory or case aliases of canon. */
10
+ export function memoryProjectConfigurationPath(value) { return projectConfigurationPaths.includes(value); }
11
+ /** Check both assigned directories and their concrete output descendants. */
12
+ export function memoryCompositionOutputAllowed(value) {
13
+ if (overlaps(value, ".memory-bank/dd-flow") && !memoryProjectConfigurationPath(value))
14
+ return false;
15
+ return value.split("/").every(part => localEnvironmentAllowed(part)
16
+ && ![".git", "node_modules", ".dd-flow"].includes(physicalPath(part)));
17
+ }
18
+ function overlaps(left, right) {
19
+ const a = physicalPath(left), b = physicalPath(right);
20
+ return a === "." || b === "." || a === b || a.startsWith(`${b}/`) || b.startsWith(`${a}/`);
21
+ }
22
+ /** Called only after strict result-schema and contained-path admission. These
23
+ * semantic graph checks precede Work allocation, not merely worker dispatch.
24
+ * They do not grant filesystem access or replace frozen profile/Work custody.
25
+ */
26
+ export function validateMemoryCompositionGraph(value) {
27
+ const result = value;
28
+ const unresolved = result.units.findIndex(unit => unit.applicability === "unknown");
29
+ if (unresolved !== -1)
30
+ throw new AppError("memory_stage_result_invalid", "Resolve the composition target before assigning Work; missing facts may remain explicit in its content", 2, { reason: "unresolved_composition_applicability", phase: "prepare", effect: "no_effect", unit_id: result.units[unresolved].id,
31
+ pointer: `/units/${unresolved}/applicability`, hint: "Choose applicable for an intended document or compact shelf, or not-applicable with a reason and no assigned writer. Do not invent missing project facts." });
32
+ const works = result.details.works;
33
+ const byKey = new Map(works.map(work => [work.key, work]));
34
+ if (byKey.size !== works.length || new Set(works.map(work => physicalPath(work.key))).size !== works.length)
35
+ refuse("duplicate_composition_work");
36
+ for (const work of works) {
37
+ if (work.depends_on.some(key => !byKey.has(key)))
38
+ refuse("unknown_composition_dependency");
39
+ for (const scopes of [work.read_scopes, work.write_scopes]) {
40
+ if (new Set(scopes.map(scope => `${scope.root}\0${physicalPath(scope.path)}`)).size !== scopes.length)
41
+ refuse("duplicate_composition_scope");
42
+ }
43
+ for (const scope of work.write_scopes) {
44
+ if (scope.path === ".")
45
+ refuse("unbounded_composition_write");
46
+ if (scope.root === "workspace" && (scope.path !== ".memory-bank" && !scope.path.startsWith(".memory-bank/")))
47
+ refuse("foreign_composition_write");
48
+ // These exact files are project-owned defaults consumed by ordinary
49
+ // entry, not selected canon. Never grant their enclosing directory or
50
+ // case/normalization aliases; the installed prompt/schema pack is sealed.
51
+ const projectConfig = memoryProjectConfigurationPath(scope.path);
52
+ if (scope.root === "workspace" && !projectConfig && [".memory-bank/mbb", ".memory-bank/dd-flow"].some(canonical => overlaps(scope.path, canonical)))
53
+ refuse("canonical_composition_write");
54
+ // A contained semantic scope is not permission to alter credentials,
55
+ // repository metadata, installed dependencies or runtime state.
56
+ if (!memoryCompositionOutputAllowed(scope.path))
57
+ refuse("private_composition_write");
58
+ if (scope.root === "run" && ["execution-bundle", "run.json"].some(authority => overlaps(scope.path, authority)))
59
+ refuse("authority_composition_write");
60
+ // Mechanical RUN files and another Work's evidence are never semantic outputs.
61
+ if (scope.root === "run") {
62
+ const namespace = `memory-artifacts/compose/${work.key}`;
63
+ if (scope.path !== namespace && !scope.path.startsWith(`${namespace}/`))
64
+ refuse("foreign_composition_run_write");
65
+ }
66
+ }
67
+ }
68
+ // Iterative topological order avoids recursion on an agent-sized graph.
69
+ const remaining = new Map(works.map(work => [work.key, work.depends_on.length]));
70
+ const children = new Map();
71
+ for (const work of works)
72
+ for (const dependency of work.depends_on) {
73
+ const list = children.get(dependency) ?? [];
74
+ list.push(work.key);
75
+ children.set(dependency, list);
76
+ }
77
+ const ready = works.filter(work => !work.depends_on.length).map(work => work.key);
78
+ const positions = new Map(works.map((work, index) => [work.key, index]));
79
+ const ancestors = new Map();
80
+ const dependsOn = (work, dependency) => {
81
+ const position = positions.get(dependency);
82
+ return Boolean(ancestors.get(work)[position >>> 5] & (1 << (position & 31)));
83
+ };
84
+ for (let cursor = 0; cursor < ready.length; cursor++) {
85
+ const key = ready[cursor], work = byKey.get(key);
86
+ const inherited = new Uint32Array(Math.ceil(works.length / 32));
87
+ for (const dependency of work.depends_on) {
88
+ const position = positions.get(dependency);
89
+ inherited[position >>> 5] |= 1 << (position & 31);
90
+ const prior = ancestors.get(dependency);
91
+ for (let word = 0; word < prior.length; word++)
92
+ inherited[word] |= prior[word];
93
+ }
94
+ ancestors.set(key, inherited);
95
+ for (const child of children.get(key) ?? []) {
96
+ const count = remaining.get(child) - 1;
97
+ remaining.set(child, count);
98
+ if (!count)
99
+ ready.push(child);
100
+ }
101
+ }
102
+ if (ready.length !== works.length)
103
+ refuse("cyclic_composition_dependency");
104
+ const assemblies = works.filter(work => work.role === "index-assembly");
105
+ const declared = new Set(result.details.assembly_work_keys);
106
+ if (declared.size !== result.details.assembly_work_keys.length || declared.size !== assemblies.length
107
+ || assemblies.some(work => !declared.has(work.key)))
108
+ refuse("invalid_assembly_membership");
109
+ const applicable = new Set(result.units.filter(unit => unit.applicability !== "not-applicable").map(unit => unit.id));
110
+ for (const work of works)
111
+ if (work.unit_ids.some(id => !applicable.has(id)))
112
+ refuse("inapplicable_composition_work");
113
+ for (const unit of applicable) {
114
+ const authors = works.filter(work => work.unit_ids.includes(unit));
115
+ const writers = authors.filter(work => work.role === "writer");
116
+ const collectors = assemblies.filter(work => work.unit_ids.includes(unit));
117
+ if (!authors.length || !collectors.length)
118
+ refuse("incomplete_composition_coverage");
119
+ if (collectors.some(collector => writers.some(writer => !dependsOn(collector.key, writer.key))))
120
+ refuse("assembly_precedes_unit_writers");
121
+ }
122
+ for (let index = 0; index < works.length; index++) {
123
+ const left = works[index];
124
+ for (const right of works.slice(index + 1)) {
125
+ if (dependsOn(left.key, right.key) || dependsOn(right.key, left.key))
126
+ continue;
127
+ if (left.write_scopes.some(a => right.write_scopes.some(b => a.root === b.root && overlaps(a.path, b.path))))
128
+ refuse("unordered_composition_writers");
129
+ if (left.write_scopes.some(a => right.read_scopes.some(b => a.root === b.root && overlaps(a.path, b.path)))
130
+ || right.write_scopes.some(a => left.read_scopes.some(b => a.root === b.root && overlaps(a.path, b.path))))
131
+ refuse("unordered_composition_read_write");
132
+ }
133
+ }
134
+ }
@@ -0,0 +1,146 @@
1
+ import { createHash } from "node:crypto";
2
+ import { AppError } from "../shared/errors.js";
3
+ import { appendAudit } from "./audit.js";
4
+ import { hybridDigest } from "./hybrid-contracts.js";
5
+ import { PreparedMemoryStageResult } from "./memory-stage-results.js";
6
+ import { stageGraphScope } from "./stage-work-graph.js";
7
+ import { addWorkBatch, prepareWorkBatch, readWorkBatchCustody } from "./work-registry.js";
8
+ const workerSchema = "dd-flow/memory-composition-work-result@1";
9
+ function refuse(reason) {
10
+ throw new AppError("memory_composition_registration_invalid", "Retained composition registration cannot authorize this graph", 1, { reason, effect: "no_effect" });
11
+ }
12
+ const bytesHash = (bytes) => createHash("sha256").update(bytes).digest("hex");
13
+ function readReceipt(context, scope) {
14
+ const requestSha256 = hybridDigest(scope);
15
+ const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id = ? AND event_type = 'memory.composition_registration' AND json_extract(payload_json, '$.request_sha256') = ?", [scope.project_id, requestSha256]);
16
+ if (!rows.length)
17
+ return null;
18
+ try {
19
+ if (rows.length !== 1 || Buffer.byteLength(rows[0].payload_json) > 2 * 1024 * 1024)
20
+ refuse("ambiguous_or_unbounded_receipt");
21
+ const receipt = JSON.parse(rows[0].payload_json);
22
+ if (receipt.schema_id !== "dd-flow/memory-composition-registration@1" || receipt.request_sha256 !== requestSha256
23
+ || receipt.run_id !== scope.run_id || hybridDigest(receipt.scope) !== requestSha256 || receipt.result_schema !== workerSchema
24
+ || [receipt.batch_sha256, receipt.compose_result_sha256, receipt.input_sha256, receipt.execution_sha256].some(hash => typeof hash !== "string" || !/^[a-f0-9]{64}$/.test(hash))
25
+ || typeof receipt.result_base64 !== "string" || !receipt.work_ids || Array.isArray(receipt.work_ids)
26
+ || typeof receipt.work_ids !== "object" || !Object.keys(receipt.work_ids).length || Object.keys(receipt.work_ids).length > 4096
27
+ || Object.values(receipt.work_ids).some(id => typeof id !== "string")
28
+ || new Set(Object.values(receipt.work_ids)).size !== Object.keys(receipt.work_ids).length)
29
+ refuse("foreign_receipt_binding");
30
+ const bytes = Buffer.from(receipt.result_base64, "base64");
31
+ if (!bytes.length || bytes.length > 1024 * 1024 || bytes.toString("base64") !== receipt.result_base64
32
+ || bytesHash(bytes) !== receipt.compose_result_sha256)
33
+ refuse("changed_result_bytes");
34
+ for (const [key, id] of Object.entries(receipt.work_ids)) {
35
+ const custody = readWorkBatchCustody(context, id);
36
+ if (custody.item_key !== key || custody.request_sha256 !== requestSha256 || custody.batch_sha256 !== receipt.batch_sha256
37
+ || hybridDigest(custody.work_ids) !== hybridDigest(receipt.work_ids) || hybridDigest(custody.scope) !== requestSha256)
38
+ refuse("changed_work_custody");
39
+ }
40
+ return receipt;
41
+ }
42
+ catch (error) {
43
+ if (error instanceof AppError && error.code === "memory_composition_registration_invalid")
44
+ throw error;
45
+ return refuse("invalid_receipt_or_work_custody");
46
+ }
47
+ }
48
+ /** SQL-only identity check for the settlement writer; no schema or filesystem I/O. */
49
+ export function memoryCompositionRegistrationDigest(context, scope) {
50
+ const receipt = readReceipt(context, scope);
51
+ if (!receipt)
52
+ refuse("composition_receipt_missing");
53
+ return hybridDigest(receipt);
54
+ }
55
+ /** Independent SQL blueprint bytes, never the final result or mutable file. */
56
+ export function readMemoryCompositionRegistration(context, scope) {
57
+ const receipt = readReceipt(context, scope);
58
+ if (!receipt)
59
+ refuse("composition_receipt_missing");
60
+ return structuredClone(receipt);
61
+ }
62
+ /** Finish selects the sole original plan for this independently owned stage
63
+ * attempt; an agent result does not choose a request key or its Work graph.
64
+ */
65
+ export function selectMemoryCompositionRegistration(context, scope) {
66
+ const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type='memory.composition_registration' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.scope.stage')=? AND json_extract(payload_json,'$.scope.attempt')=? AND json_extract(payload_json,'$.scope.cycle')=? AND json_extract(payload_json,'$.scope.parent_work_id')=? LIMIT 2", [scope.project_id, scope.run_id, scope.stage, scope.attempt, scope.cycle, scope.parent_work_id]);
67
+ if (rows.length !== 1 || Buffer.byteLength(rows[0].payload_json) > 2 * 1024 * 1024)
68
+ refuse("original_composition_missing_or_ambiguous");
69
+ let retained;
70
+ try {
71
+ retained = JSON.parse(rows[0].payload_json);
72
+ }
73
+ catch {
74
+ return refuse("invalid_receipt_or_work_custody");
75
+ }
76
+ if (typeof retained?.scope?.request_key !== "string" || hybridDigest(retained.scope) !== hybridDigest({ ...scope, request_key: retained.scope.request_key }))
77
+ refuse("foreign_receipt_binding");
78
+ return readMemoryCompositionRegistration(context, { ...scope, request_key: retained.scope.request_key });
79
+ }
80
+ /** Registration custody, not semantic stage acceptance. The caller must first
81
+ * establish accepted predecessor/source and coordinator/Session authority.
82
+ * Prepared bytes and Work allocation commit together; no mutable file is read
83
+ * under the writer. An existing generic batch cannot be retroactively adopted.
84
+ */
85
+ export function registerMemoryComposition(context, input, assertHandlerCustody) {
86
+ if (!(input.composition instanceof PreparedMemoryStageResult))
87
+ refuse("foreign_prepared_result");
88
+ const value = input.composition.compositionBatch({ ...input.binding, resultSchema: workerSchema });
89
+ const bytes = input.composition.bytes();
90
+ const batchInput = { parentWorkId: input.parentWorkId, requestKey: input.requestKey, stage: "compose", value };
91
+ const prepared = prepareWorkBatch(context, batchInput);
92
+ return context.db.writeTransaction(() => {
93
+ assertHandlerCustody?.(context);
94
+ const parent = context.db.get("SELECT project_id, run_id FROM works WHERE work_id = ?", [input.parentWorkId]);
95
+ if (!parent || parent.run_id !== input.binding.runId)
96
+ refuse("foreign_coordinator");
97
+ const scope = { ...stageGraphScope(context, parent.project_id, parent.run_id, "compose"),
98
+ stage: "compose", parent_work_id: input.parentWorkId, request_key: input.requestKey };
99
+ // One immutable blueprint per coordinator/attempt. A different request key
100
+ // cannot create another productive graph or bypass a lost original receipt.
101
+ const originals = context.db.all("SELECT json_extract(payload_json,'$.request_sha256') AS request_sha256 FROM audit_events WHERE project_id=? AND event_type='memory.composition_registration' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.scope.stage')='compose' AND json_extract(payload_json,'$.scope.attempt')=? AND json_extract(payload_json,'$.scope.cycle')=? AND json_extract(payload_json,'$.scope.parent_work_id')=? LIMIT 2", [scope.project_id, scope.run_id, scope.attempt, scope.cycle, scope.parent_work_id]);
102
+ if (originals.length > 1 || originals.length === 1 && originals[0].request_sha256 !== hybridDigest(scope))
103
+ refuse("original_composition_already_registered");
104
+ const prior = readReceipt(context, scope);
105
+ if (!prior && context.db.get("SELECT 1 FROM works w JOIN stage_work_memberships m ON m.work_id=w.work_id AND m.project_id=w.project_id AND m.run_id=w.run_id WHERE w.project_id=? AND w.run_id=? AND w.parent_work_id=? AND w.result_schema=? AND m.stage='compose' AND m.attempt=? AND m.cycle=? LIMIT 1", [scope.project_id, scope.run_id, scope.parent_work_id, workerSchema, scope.attempt, scope.cycle]))
106
+ refuse("composition_receipt_missing");
107
+ if (prior && (prior.compose_result_sha256 !== input.composition.sha256 || prior.batch_sha256 !== hybridDigest(value.works)
108
+ || prior.input_sha256 !== input.binding.inputSha256 || prior.execution_sha256 !== input.binding.executionSha256))
109
+ refuse("request_conflict");
110
+ const allocation = addWorkBatch(context, { ...batchInput, prepared });
111
+ const workIds = allocation.work_ids;
112
+ if (prior) {
113
+ if (hybridDigest(prior.work_ids) !== hybridDigest(workIds))
114
+ refuse("changed_allocation");
115
+ }
116
+ else {
117
+ if (allocation.replayed)
118
+ refuse("composition_receipt_missing");
119
+ const receipt = { schema_id: "dd-flow/memory-composition-registration@1", scope, run_id: parent.run_id,
120
+ request_sha256: hybridDigest(scope), batch_sha256: hybridDigest(value.works), compose_result_sha256: input.composition.sha256,
121
+ result_base64: bytes.toString("base64"), input_sha256: input.binding.inputSha256,
122
+ execution_sha256: input.binding.executionSha256, result_schema: workerSchema, work_ids: workIds };
123
+ appendAudit(context, { projectId: parent.project_id, eventType: "memory.composition_registration", payload: { ...receipt } });
124
+ }
125
+ return { scope, compose_result_sha256: input.composition.sha256, work_ids: { ...workIds }, replayed: Boolean(prior) };
126
+ });
127
+ }
128
+ /** Restore the exact registered bytes and re-prepare with independently selected
129
+ * frozen schema/execution and predecessor/source observations. No file or settings
130
+ * fallback; restored registration does not attest completed Works or stage acceptance.
131
+ */
132
+ export function readRegisteredMemoryComposition(context, input) {
133
+ const receipt = readReceipt(context, input.scope);
134
+ if (!receipt)
135
+ refuse("composition_receipt_missing");
136
+ if (input.binding.runId !== input.scope.run_id || input.binding.stage !== "compose"
137
+ || input.binding.inputSha256 !== receipt.input_sha256 || input.binding.executionSha256 !== receipt.execution_sha256)
138
+ refuse("foreign_restore_binding");
139
+ const result = PreparedMemoryStageResult.prepare(Buffer.from(receipt.result_base64, "base64"), input.flow, input.binding);
140
+ const batch = result.compositionBatch({ runId: input.binding.runId, inputSha256: receipt.input_sha256,
141
+ executionSha256: receipt.execution_sha256, resultSchema: workerSchema });
142
+ if (result.sha256 !== receipt.compose_result_sha256 || hybridDigest(batch.works) !== receipt.batch_sha256
143
+ || batch.works.length !== Object.keys(receipt.work_ids).length || batch.works.some(item => !Object.hasOwn(receipt.work_ids, item.key)))
144
+ refuse("changed_registered_graph");
145
+ return result;
146
+ }
@@ -0,0 +1,151 @@
1
+ import path from "node:path";
2
+ import { AppError } from "../shared/errors.js";
3
+ import { hybridDigest } from "./hybrid-contracts.js";
4
+ import { PreparedMemoryCompositionWorkResult } from "./memory-composition-work-results.js";
5
+ import { PreparedMemoryStageResult } from "./memory-stage-results.js";
6
+ import { readRegisteredMemoryComposition, memoryCompositionRegistrationDigest } from "./memory-composition-registration.js";
7
+ import { readWorkBatchCustody } from "./work-registry.js";
8
+ import { stageWorkIds } from "./stage-work-graph.js";
9
+ import { assertRunBundleSqlAuthority } from "./runs.js";
10
+ import { PreparedMemoryResultBinding } from "./memory-result-binding.js";
11
+ import { appendAudit } from "./audit.js";
12
+ import { runtimeHarness } from "./execution-policy.js";
13
+ function refuse() {
14
+ throw new AppError("memory_composition_work_custody_invalid", "Registered Work differs from the independently selected composition", 1, { phase: "prepare", effect: "no_effect" });
15
+ }
16
+ function assignedRoots(context, custody) {
17
+ const run = context.db.get("SELECT workspace_root, run_root, run_dir FROM runs WHERE project_id = ? AND id = ?", [custody.scope.project_id, custody.scope.run_id]);
18
+ if (!run || !path.isAbsolute(run.workspace_root) || !path.isAbsolute(run.run_root ?? run.run_dir))
19
+ refuse();
20
+ return { workspace: run.workspace_root, run: run.run_root ?? run.run_dir };
21
+ }
22
+ /** Bridges independent SQL allocation custody and physical output preparation.
23
+ * The caller must independently select the accepted composition and frozen engine.
24
+ * This is not stage acceptance, Session settlement, or semantic completeness.
25
+ */
26
+ export class PreparedRegisteredMemoryCompositionWorkResult {
27
+ sha256;
28
+ workId;
29
+ #custody;
30
+ #result;
31
+ #roots;
32
+ #registration = null;
33
+ #stageBinding = null;
34
+ #harness;
35
+ constructor(custody, roots, result, harness) {
36
+ this.#custody = custody;
37
+ this.#roots = roots;
38
+ this.#result = result;
39
+ this.#harness = harness;
40
+ this.sha256 = result.sha256;
41
+ this.workId = custody.work_id;
42
+ Object.freeze(this);
43
+ }
44
+ /** Restart path: composition bytes come from CLI registration, while its
45
+ * predecessor/source/schema/execution binding still comes from the controller.
46
+ */
47
+ static prepareFromStageCustody(context, input, engine, binding) {
48
+ const custody = readWorkBatchCustody(context, binding.workId);
49
+ const selected = binding.bundle ? PreparedMemoryResultBinding.prepareRegisteredComposition(context, { ...binding, bundle: binding.bundle,
50
+ registrationScope: { project_id: binding.projectId, run_id: binding.runId, stage: "compose", attempt: custody.scope.attempt, cycle: custody.scope.cycle,
51
+ parent_work_id: binding.coordinatorWorkId, request_key: binding.requestKey } })
52
+ : binding.sources ? PreparedMemoryResultBinding.prepare(context, { ...binding, sources: binding.sources, stage: "compose" }) : refuse();
53
+ const prepared = this.prepareFromRegistration(context, input, engine, { ...binding, resultBinding: selected.binding() });
54
+ prepared.#stageBinding = selected;
55
+ prepared.assertCustody(context);
56
+ return prepared;
57
+ }
58
+ static prepareFromRegistration(context, input, engine, binding) {
59
+ const custody = readWorkBatchCustody(context, binding.workId);
60
+ if (custody.scope.stage !== "compose" || custody.scope.parent_work_id !== binding.coordinatorWorkId
61
+ || custody.scope.request_key !== binding.requestKey || !binding.resultBinding.executionSha256)
62
+ refuse();
63
+ const scope = { project_id: custody.scope.project_id, run_id: custody.scope.run_id,
64
+ stage: "compose", attempt: custody.scope.attempt, cycle: custody.scope.cycle,
65
+ parent_work_id: binding.coordinatorWorkId, request_key: binding.requestKey };
66
+ const registrationSha256 = memoryCompositionRegistrationDigest(context, scope);
67
+ const composition = readRegisteredMemoryComposition(context, { scope, flow: binding.flow, binding: binding.resultBinding });
68
+ const prepared = this.prepare(context, input, engine, { workId: binding.workId, coordinatorWorkId: binding.coordinatorWorkId, requestKey: binding.requestKey,
69
+ composition, compositionSha256: composition.sha256, runId: binding.resultBinding.runId,
70
+ inputSha256: binding.resultBinding.inputSha256, executionSha256: binding.resultBinding.executionSha256 });
71
+ prepared.#registration = { scope, sha256: registrationSha256 };
72
+ prepared.assertCustody(context);
73
+ return prepared;
74
+ }
75
+ static prepare(context, input, engine, binding) {
76
+ const custody = readWorkBatchCustody(context, binding.workId);
77
+ if (!(binding.composition instanceof PreparedMemoryStageResult) || binding.composition.sha256 !== binding.compositionSha256
78
+ || custody.scope.run_id !== binding.runId || custody.scope.stage !== "compose"
79
+ || custody.scope.parent_work_id !== binding.coordinatorWorkId || custody.scope.request_key !== binding.requestKey)
80
+ refuse();
81
+ const batch = binding.composition.compositionBatch({ ...binding, resultSchema: "dd-flow/memory-composition-work-result@1" });
82
+ if (hybridDigest(batch.works) !== custody.batch_sha256 || batch.works.length !== Object.keys(custody.work_ids).length
83
+ || batch.works.some(item => !Object.hasOwn(custody.work_ids, item.key)))
84
+ refuse();
85
+ const assigned = batch.works.find(item => item.key === custody.item_key);
86
+ const row = context.db.get("SELECT * FROM works WHERE project_id = ? AND run_id = ? AND work_id = ?", [custody.scope.project_id, binding.runId, binding.workId]);
87
+ if (!assigned || !row || row.parent_work_id !== binding.coordinatorWorkId || row.task !== assigned.task
88
+ || row.launch_policy !== assigned.launch_policy || row.result_schema !== assigned.result_schema
89
+ || hybridDigest(JSON.parse(row.depends_on_json)) !== hybridDigest(assigned.depends_on.map(key => custody.work_ids[key]))
90
+ || hybridDigest(JSON.parse(row.payload_json)) !== hybridDigest({ ...assigned.payload, _dd_flow_batch_request: {
91
+ request_sha256: custody.request_sha256, batch_sha256: custody.batch_sha256, item_key: custody.item_key
92
+ } }))
93
+ refuse();
94
+ const roots = assignedRoots(context, custody);
95
+ const result = PreparedMemoryCompositionWorkResult.prepare(input, engine, { ...binding, workKey: custody.item_key,
96
+ roots });
97
+ const route = binding.composition.compositionRoutes().find(item => item.key === custody.item_key);
98
+ if (!route)
99
+ refuse();
100
+ return new PreparedRegisteredMemoryCompositionWorkResult(custody, roots, result, runtimeHarness(route.execution.worker));
101
+ }
102
+ bytes() { return this.#result.bytes(); }
103
+ /** Physical I/O stays outside the writer; the reservation rechecks SQL only. */
104
+ revalidate(context) { this.#stageBinding?.revalidate(context); this.assertCustody(context); this.#result.revalidate(); }
105
+ assertCustody(context) {
106
+ if (hybridDigest(readWorkBatchCustody(context, this.#custody.work_id)) !== hybridDigest(this.#custody)
107
+ || hybridDigest(assignedRoots(context, this.#custody)) !== hybridDigest(this.#roots))
108
+ refuse();
109
+ if (this.#registration && memoryCompositionRegistrationDigest(context, this.#registration.scope) !== this.#registration.sha256)
110
+ refuse();
111
+ this.#stageBinding?.assertCustody(context);
112
+ }
113
+ /** Active settlement authority is separate from immutable allocation custody. */
114
+ assertSettlementScope(context, sessionId, workSessionId) {
115
+ this.assertCustody(context);
116
+ const scope = this.#custody.scope;
117
+ const run = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [scope.project_id, scope.run_id]);
118
+ const index = run ? JSON.parse(run.index_json) : null;
119
+ assertRunBundleSqlAuthority(context, { projectId: scope.project_id, runId: scope.run_id }, index?.execution_bundle);
120
+ const stage = index?.stage_runs?.findLast((item) => item.stage === "compose" && item.status === "running");
121
+ if (!this.#registration || !this.#stageBinding || index?.current_stage !== "compose" || !stage || (stage.attempt ?? "try-001") !== scope.attempt
122
+ || !stageWorkIds(context, scope.project_id, scope.run_id, "compose").has(this.workId)
123
+ || !stageWorkIds(context, scope.project_id, scope.run_id, "compose").has(scope.parent_work_id)
124
+ || !context.db.get("SELECT 1 FROM work_sessions WHERE id = ? AND work_id = ? AND session_id = ? AND status = 'running'", [workSessionId, this.workId, sessionId])
125
+ || !context.db.get("SELECT 1 FROM works WHERE work_id = ? AND project_id = ? AND run_id = ? AND status = 'running'", [scope.parent_work_id, scope.project_id, scope.run_id])
126
+ || !context.db.get("SELECT 1 FROM sessions s JOIN runs r ON r.project_id = s.project_id AND r.id = s.run_id WHERE s.project_id = ? AND s.session_id = ? AND s.run_id = ? AND s.worker_id = ? AND s.harness = ? AND s.status = 'active' AND s.current_stage = 'work' AND s.project_root = r.project_root AND s.workspace_path = r.workspace_root", [scope.project_id, sessionId, scope.run_id, this.workId, this.#harness])) {
127
+ throw new AppError("memory_work_settlement_scope_invalid", "Composition Work settlement no longer belongs to its active stage and Session", 1, { work_id: this.workId, effect: "no_effect" });
128
+ }
129
+ }
130
+ /** Called inside the Work settlement writer, before its status CAS. The
131
+ * receipt commits or rolls back with the result and physical WorkSession.
132
+ * It retains successful output preparation without requiring an obsolete
133
+ * writer artifact to survive a declared dependent assembly unchanged.
134
+ */
135
+ recordSettlement(context, sessionId, workSessionId) {
136
+ this.assertSettlementScope(context, sessionId, workSessionId);
137
+ const registration = this.#registration;
138
+ const rows = context.db.all("SELECT 1 FROM audit_events WHERE project_id=? AND event_type='memory.composition_work_settlement' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.work_id')=? LIMIT 1", [this.#custody.scope.project_id, this.#custody.scope.run_id, this.workId]);
139
+ if (rows.length)
140
+ refuse();
141
+ const session = context.db.get("SELECT provider_session_id FROM sessions WHERE project_id=? AND session_id=?", [this.#custody.scope.project_id, sessionId]);
142
+ if (!session || typeof session.provider_session_id !== "string" || !session.provider_session_id)
143
+ refuse();
144
+ appendAudit(context, { projectId: this.#custody.scope.project_id, eventType: "memory.composition_work_settlement", payload: {
145
+ schema_id: "dd-flow/memory-composition-work-settlement@1", run_id: this.#custody.scope.run_id, work_id: this.workId,
146
+ work_session_id: workSessionId, session_id: sessionId, harness: this.#harness, provider_session_id: session.provider_session_id,
147
+ scope: registration.scope, registration_sha256: registration.sha256,
148
+ allocation_sha256: hybridDigest(this.#custody), roots: this.#roots, result_sha256: this.sha256
149
+ } });
150
+ }
151
+ }