@deksden-com/dd-flow-cli 0.9.0-beta.152 → 0.9.0-beta.156

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 (227) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/build-info.json +8 -8
  3. package/dist/cli/command-inputs.js +33 -3
  4. package/dist/cli/command-metadata.js +73 -2
  5. package/dist/cli/delivery.js +108 -0
  6. package/dist/cli/diagnostic-invocation.js +23 -7
  7. package/dist/cli/diagnostic.js +14 -3
  8. package/dist/cli/help.js +117 -1
  9. package/dist/cli/hook-ingress.js +36 -49
  10. package/dist/cli/interactive-invocation.js +33 -0
  11. package/dist/cli/interactive.js +114 -0
  12. package/dist/cli/memory-fix-invocation.js +52 -0
  13. package/dist/cli/memory-fix.js +100 -0
  14. package/dist/cli/memory-flow-admission.js +7 -3
  15. package/dist/cli/memory-init.js +5 -4
  16. package/dist/cli/native-hook-ingress.js +5 -1
  17. package/dist/cli/native-hook-preparation.js +31 -4
  18. package/dist/cli/run-cli.js +274 -19
  19. package/dist/cli.js +17 -3
  20. package/dist/domain/delivery-flows.js +8 -0
  21. package/dist/domain/distill-flow.js +20 -0
  22. package/dist/domain/interactive-flow.js +1 -0
  23. package/dist/domain/memory-fix-flow.js +2 -0
  24. package/dist/domain/review-fix-flow.js +2 -0
  25. package/dist/domain/source-snapshot-flows.js +4 -0
  26. package/dist/harness-runtime/bin/dd-agy.mjs +18 -3
  27. package/dist/harness-runtime/bin/dd-grok.mjs +15 -5
  28. package/dist/harness-runtime/bin/dd-zcode.mjs +15 -7
  29. package/dist/harness-runtime/lib/agy-error-boundary.mjs +18 -10
  30. package/dist/harness-runtime/lib/agy-native-inspection.mjs +12 -3
  31. package/dist/harness-runtime/lib/agy-native-inventory.mjs +19 -0
  32. package/dist/harness-runtime/lib/agy-terminal-envelope.mjs +13 -6
  33. package/dist/harness-runtime/lib/agy-transcript-stream.mjs +54 -0
  34. package/dist/harness-runtime/lib/daemon-observation.d.mts +1 -0
  35. package/dist/harness-runtime/lib/daemon-observation.mjs +12 -4
  36. package/dist/harness-runtime/lib/daemon-operations.mjs +20 -5
  37. package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +1 -0
  38. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +371 -114
  39. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +43 -3
  40. package/dist/harness-runtime/lib/dd-codex.mjs +36 -4
  41. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +25 -11
  42. package/dist/harness-runtime/lib/dd-droid.mjs +124 -11
  43. package/dist/harness-runtime/lib/dd-grok-daemon.d.mts +1 -0
  44. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +137 -25
  45. package/dist/harness-runtime/lib/dd-grok.mjs +4 -2
  46. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +115 -17
  47. package/dist/harness-runtime/lib/dd-opencode.mjs +10 -7
  48. package/dist/harness-runtime/lib/dd-zcode-daemon.d.mts +1 -0
  49. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +115 -22
  50. package/dist/harness-runtime/lib/dd-zcode.mjs +75 -36
  51. package/dist/harness-runtime/lib/delegation-instructions.d.mts +2 -1
  52. package/dist/harness-runtime/lib/delegation-instructions.mjs +15 -5
  53. package/dist/harness-runtime/lib/delivery-command.mjs +84 -0
  54. package/dist/harness-runtime/lib/driver-recovery.mjs +0 -19
  55. package/dist/harness-runtime/lib/droid-observation.mjs +9 -5
  56. package/dist/harness-runtime/lib/lifecycle-contract.d.mts +1 -0
  57. package/dist/harness-runtime/lib/lifecycle-contract.mjs +2 -1
  58. package/dist/harness-runtime/lib/native-hook-command.d.mts +3 -2
  59. package/dist/harness-runtime/lib/native-hook-command.mjs +63 -8
  60. package/dist/harness-runtime/lib/native-streams.mjs +3 -10
  61. package/dist/harness-runtime/lib/opencode-plugin.mjs +36 -9
  62. package/dist/harness-runtime/lib/operation-errors.mjs +13 -0
  63. package/dist/harness-runtime/lib/operation-progress.mjs +6 -2
  64. package/dist/harness-runtime/lib/process-json.mjs +4 -5
  65. package/dist/harness-runtime/lib/tool-observations.mjs +20 -10
  66. package/dist/harness-runtime/lib/zcode-native-hook-ownership.mjs +109 -0
  67. package/dist/schemas/code-work-result.schema.json +2 -1
  68. package/dist/schemas/delivery-stage-result.schema.json +286 -0
  69. package/dist/schemas/distill-stage-result.schema.json +667 -0
  70. package/dist/schemas/distill-work-result.schema.json +428 -0
  71. package/dist/schemas/execution-bundle.schema.json +6 -2
  72. package/dist/schemas/interactive-stage-result.schema.json +47 -0
  73. package/dist/schemas/interactive-work-result.schema.json +63 -0
  74. package/dist/schemas/mb-fix-stage-result.schema.json +93 -0
  75. package/dist/schemas/project-flow-pack-manifest.schema.json +2 -2
  76. package/dist/schemas/review-fix-stage-result.schema.json +210 -0
  77. package/dist/schemas/run-control-receipt.schema.json +1 -0
  78. package/dist/services/assigned-lifecycle.js +382 -0
  79. package/dist/services/check-runner-evidence.js +45 -8
  80. package/dist/services/cli-operation-classifier.js +6 -1
  81. package/dist/services/code-checks.js +46 -29
  82. package/dist/services/codex-hook-delivery.js +11 -2
  83. package/dist/services/controller-child-repair.js +95 -0
  84. package/dist/services/controller-execution.js +4 -1
  85. package/dist/services/controller-fanout.js +19 -9
  86. package/dist/services/controller-memory-worker.js +6 -4
  87. package/dist/services/controller-result-authority.js +122 -0
  88. package/dist/services/dashboard.js +4 -2
  89. package/dist/services/delivery-access.js +123 -0
  90. package/dist/services/delivery-approval.js +68 -0
  91. package/dist/services/delivery-artifact.js +86 -0
  92. package/dist/services/delivery-checks.js +178 -0
  93. package/dist/services/delivery-execution.js +181 -0
  94. package/dist/services/delivery-intents.js +212 -0
  95. package/dist/services/delivery-operation.js +81 -0
  96. package/dist/services/delivery-policy.js +91 -0
  97. package/dist/services/delivery-procedures.js +43 -0
  98. package/dist/services/delivery-request.js +99 -0
  99. package/dist/services/delivery-result.js +144 -0
  100. package/dist/services/delivery-stage-commands.js +26 -0
  101. package/dist/services/delivery-stage-entry.js +56 -0
  102. package/dist/services/delivery-stage-handlers.js +135 -0
  103. package/dist/services/delivery-stage-results.js +94 -0
  104. package/dist/services/descriptor-coordinator-session.js +3 -1
  105. package/dist/services/descriptor-run-start.js +24 -4
  106. package/dist/services/descriptor-stage-coordinator.js +78 -10
  107. package/dist/services/descriptor-stage-settlement.js +59 -14
  108. package/dist/services/diagnostic-repair-intake.js +123 -0
  109. package/dist/services/diagnostic-repair-source-intake.js +102 -0
  110. package/dist/services/diagnostic-report.js +30 -0
  111. package/dist/services/diagnostic-run-bundle.js +48 -9
  112. package/dist/services/diagnostic-snapshot.js +2 -2
  113. package/dist/services/diagnostic-stage-handlers.js +3 -2
  114. package/dist/services/diagnostic-stage-results.js +26 -0
  115. package/dist/services/diagnostic-start.js +56 -6
  116. package/dist/services/diagnostic-work-collection.js +3 -3
  117. package/dist/services/distill-document-publication.js +280 -0
  118. package/dist/services/distill-promotion.js +197 -0
  119. package/dist/services/distill-publication-custody.js +162 -0
  120. package/dist/services/distill-stage-handlers.js +249 -0
  121. package/dist/services/distill-stage-results.js +105 -0
  122. package/dist/services/distill-user-inputs.js +4 -0
  123. package/dist/services/distill-work-batch.js +42 -0
  124. package/dist/services/distill-work-collection.js +74 -0
  125. package/dist/services/distill-work-registration.js +93 -0
  126. package/dist/services/distill-work-results.js +98 -0
  127. package/dist/services/eval-snapshots.js +17 -7
  128. package/dist/services/external-work-launch.js +8 -2
  129. package/dist/services/flow-catalog.js +24 -0
  130. package/dist/services/frozen-engine.js +19 -2
  131. package/dist/services/generic-execution.js +4 -3
  132. package/dist/services/harness-adapter.js +2 -14
  133. package/dist/services/hook-memory-routing.js +1 -1
  134. package/dist/services/hooks.js +31 -6
  135. package/dist/services/hybrid-contracts.js +6 -1
  136. package/dist/services/installed-flow-source.js +37 -2
  137. package/dist/services/interactive-attached-session.js +215 -0
  138. package/dist/services/interactive-baseline-custody.js +54 -0
  139. package/dist/services/interactive-before-versions.js +148 -0
  140. package/dist/services/interactive-bundle-data.js +85 -0
  141. package/dist/services/interactive-checks.js +179 -0
  142. package/dist/services/interactive-execution.js +33 -0
  143. package/dist/services/interactive-finish-integration.js +236 -0
  144. package/dist/services/interactive-git-baseline.js +230 -0
  145. package/dist/services/interactive-git-dispatch.js +278 -0
  146. package/dist/services/interactive-input-delivery.js +82 -0
  147. package/dist/services/interactive-mutation-scope.js +231 -0
  148. package/dist/services/interactive-request.js +287 -0
  149. package/dist/services/interactive-run-bundle.js +109 -0
  150. package/dist/services/interactive-stage-entry.js +41 -0
  151. package/dist/services/interactive-stage-handlers.js +289 -0
  152. package/dist/services/interactive-stage-observation.js +136 -0
  153. package/dist/services/interactive-stage-results.js +111 -0
  154. package/dist/services/interactive-user-inputs.js +142 -0
  155. package/dist/services/interactive-work-batch.js +43 -0
  156. package/dist/services/interactive-work-collection.js +119 -0
  157. package/dist/services/interactive-work-completion.js +180 -0
  158. package/dist/services/interactive-work-context.js +249 -0
  159. package/dist/services/interactive-work-history.js +112 -0
  160. package/dist/services/interactive-work-registration.js +124 -0
  161. package/dist/services/interactive-work-results.js +115 -0
  162. package/dist/services/interactive-work-start.js +155 -0
  163. package/dist/services/lifecycle-invocations.js +250 -33
  164. package/dist/services/managed-daemon-binding.js +52 -1
  165. package/dist/services/memory-compose-handler.js +2 -22
  166. package/dist/services/memory-finish-integration.js +58 -17
  167. package/dist/services/memory-fix-finish-integration.js +129 -0
  168. package/dist/services/memory-fix-git-dispatch.js +139 -0
  169. package/dist/services/memory-fix-intake-custody.js +11 -0
  170. package/dist/services/memory-fix-intake-settlement.js +127 -0
  171. package/dist/services/memory-fix-publication.js +196 -0
  172. package/dist/services/memory-fix-repair-admission.js +116 -0
  173. package/dist/services/memory-fix-request.js +141 -0
  174. package/dist/services/memory-fix-run-bundle.js +83 -0
  175. package/dist/services/memory-fix-scope.js +78 -0
  176. package/dist/services/memory-fix-sources.js +85 -0
  177. package/dist/services/memory-fix-stage-entry.js +24 -0
  178. package/dist/services/memory-fix-stage-handlers.js +282 -0
  179. package/dist/services/memory-fix-stage-results.js +106 -0
  180. package/dist/services/memory-fix-workspace.js +127 -0
  181. package/dist/services/memory-flow-user-inputs.js +88 -0
  182. package/dist/services/memory-init-contexts.js +55 -5
  183. package/dist/services/memory-init-controller-start.js +20 -4
  184. package/dist/services/memory-layer.js +7 -3
  185. package/dist/services/memory-native-review-copy.js +27 -3
  186. package/dist/services/memory-repair-document-delta.js +1 -0
  187. package/dist/services/project-flow-source.js +195 -0
  188. package/dist/services/project-git-integration.js +92 -0
  189. package/dist/services/repair-intents.js +2 -7
  190. package/dist/services/retained-native-hook.js +274 -0
  191. package/dist/services/review-fix-handoff.js +82 -0
  192. package/dist/services/review-fix-intake-custody.js +17 -0
  193. package/dist/services/review-fix-stage-entry.js +43 -0
  194. package/dist/services/review-fix-stage-handlers.js +176 -0
  195. package/dist/services/review-fix-stage-results.js +86 -0
  196. package/dist/services/run-control-worker.js +42 -1
  197. package/dist/services/run-control.js +40 -5
  198. package/dist/services/run-controller-adapter.js +37 -3
  199. package/dist/services/run-controller-process.js +25 -4
  200. package/dist/services/run-controller-recovery.js +46 -6
  201. package/dist/services/run-controller-state.js +3 -2
  202. package/dist/services/run-controller.js +257 -44
  203. package/dist/services/run-recovery.js +31 -2
  204. package/dist/services/run-timeline.js +59 -0
  205. package/dist/services/runs.js +18 -31
  206. package/dist/services/sessions.js +2 -1
  207. package/dist/services/stage-handlers.js +55 -3
  208. package/dist/services/stage-lifecycle.js +4 -3
  209. package/dist/services/stage-pause.js +26 -9
  210. package/dist/services/stage-report-renderer.js +2 -6
  211. package/dist/services/technical-notes.js +31 -0
  212. package/dist/services/verification-matrix.js +1 -1
  213. package/dist/services/version-status.js +1 -1
  214. package/dist/services/vnext-code-review.js +74 -5
  215. package/dist/services/vnext-code.js +8 -32
  216. package/dist/services/vnext-fanout.js +53 -8
  217. package/dist/services/vnext-merge.js +10 -5
  218. package/dist/services/vnext-plan-review.js +1 -1
  219. package/dist/services/vnext-plan.js +1 -1
  220. package/dist/services/vnext-protocolize.js +1 -1
  221. package/dist/services/vnext-specify.js +10 -5
  222. package/dist/services/work-registry.js +255 -69
  223. package/dist/services/worktrees.js +7 -4
  224. package/dist/storage/database.js +2 -1
  225. package/dist/verification-components.json +82 -0
  226. package/package.json +3 -2
  227. package/tools/verification-jobs.mjs +35 -10
@@ -55,6 +55,36 @@ export function prepareDiagnosticReport(result, analysis, critics) {
55
55
  dashboard_links: critics.map(work => ({ work_id: work.workId, result: `run://${result.run_id}/works/${work.workId}/result.json` }))
56
56
  };
57
57
  }
58
+ /** Pure repair-intake selection, not acceptance or mutation authority. The
59
+ * caller must obtain report bytes from the completed RUN's committed custody,
60
+ * then separately bind the user's approved write scope. Never infer selection
61
+ * from severity, recommendations or a non-empty findings register. */
62
+ export function selectDiagnosticFindings(report, findingIds) {
63
+ const invalid = (reason) => {
64
+ throw new AppError("repair_selection_invalid", "Select exact reviewed finding IDs before preparing repair scope", 2, { reason, effect: "no_effect" });
65
+ };
66
+ if (!Array.isArray(findingIds) || !findingIds.length || findingIds.length > 4096
67
+ || findingIds.some(id => typeof id !== "string" || !id.trim()) || new Set(findingIds).size !== findingIds.length)
68
+ invalid("explicit_unique_selection_required");
69
+ const register = new Map(report.findings_register.map(finding => [finding.id, finding]));
70
+ if (register.size !== report.findings_register.length)
71
+ invalid("ambiguous_finding_register");
72
+ const selected = findingIds.map(id => {
73
+ const finding = register.get(id);
74
+ if (!finding)
75
+ return invalid("unknown_finding");
76
+ if (!["accepted", "downgraded", "def_candidate", "review_fix_candidate"].includes(finding.resolution.disposition))
77
+ invalid("finding_not_accepted");
78
+ return structuredClone(finding);
79
+ });
80
+ return {
81
+ source: { ...structuredClone(report.run), report_sha256: hybridDigest(report) },
82
+ selected_findings: selected,
83
+ unselected_ids: report.findings_register.filter(finding => !findingIds.includes(finding.id)).map(finding => finding.id),
84
+ limitations: [...report.limitations],
85
+ authorization: "not_granted"
86
+ };
87
+ }
58
88
  /** HTML embeds exactly the authoritative JSON value; untrusted prose is text,
59
89
  * never HTML or executable script. Markdown remains a compact companion. */
60
90
  export function renderDiagnosticReport(report) {
@@ -1,6 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { AppError } from "../shared/errors.js";
3
- import { isDiagnosticFlow } from "../domain/diagnostic-flows.js";
3
+ import { isSourceSnapshotFlow } from "../domain/source-snapshot-flows.js";
4
+ import { isDeliveryFlow } from "../domain/delivery-flows.js";
4
5
  import { PreparedInstalledFlowSource } from "./installed-flow-source.js";
5
6
  import { FrozenGenericExecution } from "./generic-execution.js";
6
7
  import { AdmittedFrozenEngine } from "./frozen-engine.js";
@@ -10,34 +11,65 @@ import { requireProjectByRoot } from "./projects.js";
10
11
  import { nextDescriptorRunId } from "./descriptor-run-start.js";
11
12
  import { bundledHandlerContext } from "./stage-handlers.js";
12
13
  import { hybridDigest } from "./hybrid-contracts.js";
14
+ import { readDiagnosticRequest } from "./diagnostic-start.js";
13
15
  const bytes = (value) => Buffer.from(JSON.stringify(value));
14
16
  const hash = (value) => createHash("sha256").update(value).digest("hex");
15
17
  function refuse(reason) { throw new AppError("diagnostic_run_bundle_invalid", "Diagnostic bundle requires its frozen source, execution and snapshot", 2, { reason, effect: "no_effect" }); }
16
18
  /** Initial construction only. Retry after allocation restores the retained RUN
17
19
  * bundle, never current operator settings/canon. boundAt belongs to the request. */
18
20
  export function prepareDiagnosticRunBundle(context, input) {
21
+ return prepareRequestRunBundle(context, input, "source_snapshot");
22
+ }
23
+ /** Delivery shares immutable request/engine/profile retention, not diagnosis's
24
+ * read-only cloned workspace or its permission model. No effect is admitted. */
25
+ export function prepareDeliveryRunBundle(context, input) {
26
+ return prepareRequestRunBundle(context, input, "delivery");
27
+ }
28
+ function prepareRequestRunBundle(context, input, mode) {
19
29
  if (!(input.source instanceof PreparedInstalledFlowSource) || !(input.execution instanceof FrozenGenericExecution)
20
30
  || !(input.engine instanceof AdmittedFrozenEngine) || !Number.isFinite(Date.parse(input.boundAt)))
21
31
  refuse("unprepared_selection");
22
32
  const frozen = input.execution.snapshot(), flowId = frozen.settings.flow_id;
23
- if (!isDiagnosticFlow(flowId))
24
- refuse("diagnostic_flow_required");
33
+ if (mode === "delivery" ? !isDeliveryFlow(flowId) : !isSourceSnapshotFlow(flowId))
34
+ refuse("registered_request_flow_required");
35
+ if (mode === "delivery" && frozen.settings.stage_session_mode === "same_session")
36
+ refuse("independent_delivery_verifier_session_required");
37
+ if (flowId === "review-fix" && frozen.settings.stage_session_mode === "same_session")
38
+ refuse("independent_scope_review_session_required");
39
+ if ((mode === "delivery" || flowId === "mb-distill" || flowId === "review-fix") && (typeof input.requestText !== "string" || !input.requestText.trim()
40
+ || Buffer.byteLength(input.requestText) > 64 * 1024))
41
+ refuse(mode === "delivery" ? "original_delivery_request_required" : flowId === "mb-distill" ? "original_distill_request_required" : "original_review_fix_request_required");
42
+ const request = flowId === "review-fix" ? readDiagnosticRequest(context, { projectRoot: frozen.scope.projectRoot,
43
+ requestId: frozen.scope.requestId, flowId }) : null;
44
+ if (flowId === "review-fix" && (!request?.repair_source || request.created_at !== input.boundAt
45
+ || request.objective !== input.requestText || request.source_sha256 !== input.source.sha256
46
+ || request.execution_sha256 !== input.execution.sha256))
47
+ refuse("original_repair_request_required");
25
48
  const project = requireProjectByRoot(context, frozen.scope.projectRoot);
26
- const snapshot = readDiagnosticSnapshot(context, { projectId: project.id, projectRoot: frozen.scope.projectRoot,
27
- sourceRoot: frozen.scope.projectRoot, requestId: frozen.scope.requestId, flowId });
28
- if (!snapshot || snapshot.root !== frozen.scope.workspace)
49
+ const snapshot = isSourceSnapshotFlow(flowId) ? readDiagnosticSnapshot(context, { projectId: project.id, projectRoot: frozen.scope.projectRoot,
50
+ sourceRoot: frozen.scope.projectRoot, requestId: frozen.scope.requestId, flowId }) : null;
51
+ if (mode === "source_snapshot" && (!snapshot || snapshot.root !== frozen.scope.workspace))
29
52
  refuse("snapshot_selection_changed");
30
53
  input.source.revalidate();
31
54
  const flow = input.source.resolveRegisteredFlow();
32
55
  if (flow.descriptor.id !== flowId || flow.sha256 !== frozen.settings.descriptor_sha256)
33
56
  refuse("flow_selection_changed");
34
- input.engine.admitDiagnosticFlow(input.source, flowId);
57
+ if (isDeliveryFlow(flowId))
58
+ input.engine.admitDeliveryFlow(input.source, flowId);
59
+ else if (flowId === "review-fix")
60
+ input.engine.admitReviewFixFlow(input.source);
61
+ else if (flowId === "mb-distill")
62
+ input.engine.admitDistillFlow(input.source);
63
+ else if (flowId === "mb-audit" || flowId === "project-review")
64
+ input.engine.admitDiagnosticFlow(input.source, flowId);
65
+ else
66
+ refuse("registered_request_flow_required");
35
67
  const manifest = input.engine.manifest(), checksum = manifest.integrity.checksum;
36
68
  if (typeof checksum !== "string")
37
69
  refuse("engine_identity_missing");
38
70
  const runId = nextDescriptorRunId(context, { projectRoot: frozen.scope.projectRoot, requestId: frozen.scope.requestId, slug: flowId });
39
71
  const binding = { schema_id: "dd-flow/run-engine-binding@1", run_id: runId,
40
- project_root: frozen.scope.projectRoot, bound_at: input.boundAt, source: "run_creation", reason: "Request-bound read-only diagnostic",
72
+ project_root: frozen.scope.projectRoot, bound_at: input.boundAt, source: "run_creation", reason: isDeliveryFlow(flowId) ? "Request-bound delivery; startup grants no productive mutation approval" : flowId === "review-fix" ? "Request-bound selected finding scope and explicit formalization handoff" : flowId === "mb-distill" ? "Request-bound practice distillation with separate destination authority" : "Request-bound read-only diagnostic",
41
73
  engine: { package_name: manifest.package_name, package_version: manifest.package_version, engine_version: manifest.engine_version,
42
74
  integrity_checksum: checksum, snapshot_root: manifest.snapshot_root }, probe: { status: "not_required", command: null } };
43
75
  const blobs = input.source.blobs();
@@ -47,6 +79,12 @@ export function prepareDiagnosticRunBundle(context, input) {
47
79
  blobs.set(logical, content);
48
80
  return { path: logical, sha256: hash(content) };
49
81
  };
82
+ // Use the retained request verbatim, not the coordinator's interpretation or
83
+ // a later mutable intake file. It is task data, never implicit write scope.
84
+ if (mode === "delivery" || flowId === "mb-distill" || flowId === "review-fix")
85
+ put("runtime/source-request.md", Buffer.from(input.requestText));
86
+ if (request?.repair_source)
87
+ put(request.repair_source.path, Buffer.from(request.repair_source.base64, "base64"));
50
88
  for (const ref of [frozen.settings.attached_session_receipt, frozen.settings.delegation.native_capacity_receipt]) {
51
89
  if (!ref)
52
90
  continue;
@@ -60,7 +98,8 @@ export function prepareDiagnosticRunBundle(context, input) {
60
98
  const execution = put("runtime/execution.json", bytes(frozen.settings));
61
99
  const engineBinding = put("runtime/engine-binding.json", bytes(binding));
62
100
  const engineManifest = put("runtime/engine-manifest.json", input.engine.manifestBytes());
63
- put("runtime/diagnostic-snapshot.json", bytes(snapshot));
101
+ if (snapshot)
102
+ put("runtime/diagnostic-snapshot.json", bytes(snapshot));
64
103
  const provenance = { canon: input.source.canonProvenance(), project_pack: null };
65
104
  const bundle = { schema_id: "dd-flow/execution-bundle@1", run_id: runId, flow_id: flowId,
66
105
  descriptor: input.source.descriptorReference(), execution_settings: execution, profile_settings: profile,
@@ -2,7 +2,7 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { execFileSync } from "node:child_process";
4
4
  import { AppError } from "../shared/errors.js";
5
- import { isDiagnosticFlow } from "../domain/diagnostic-flows.js";
5
+ import { isSourceSnapshotFlow } from "../domain/source-snapshot-flows.js";
6
6
  import { hybridDigest } from "./hybrid-contracts.js";
7
7
  import { appendAudit } from "./audit.js";
8
8
  import { reviewSourceHash, reviewSourceFilter, reviewExecutionCopyHash } from "./review-copy.js";
@@ -32,7 +32,7 @@ export function diagnosticSnapshotPaths(context, value, runId) {
32
32
  }
33
33
  function validate(context, input, runId) {
34
34
  const physical = physicalSnapshot(context, input, runId);
35
- if (!isDiagnosticFlow(input.flowId) || !input.requestId?.trim() || input.requestId.length > 200
35
+ if (!isSourceSnapshotFlow(input.flowId) || !input.requestId?.trim() || input.requestId.length > 200
36
36
  || !path.isAbsolute(physical.projectRoot) || physical.projectRoot !== physical.sourceRoot
37
37
  || fs.realpathSync(physical.sourceRoot) !== physical.sourceRoot)
38
38
  refuse("invalid_selection");
@@ -8,7 +8,7 @@ import { PreparedDescriptorStageStart } from "./descriptor-stage-start.js";
8
8
  import { PreparedDescriptorStageCoordinator, readDescriptorStageInput } from "./descriptor-stage-coordinator.js";
9
9
  import { PreparedDescriptorStageSettlement } from "./descriptor-stage-settlement.js";
10
10
  import { PreparedDescriptorRunSettlement } from "./descriptor-run-settlement.js";
11
- import { assertDiagnosticCriticResolution, assertDiagnosticReportData, assertDiagnosticStageScope, sealDiagnosticStageLimitations, validateDiagnosticStageResult } from "./diagnostic-stage-results.js";
11
+ import { assertDiagnosticCriticResolution, assertDiagnosticReportData, assertDiagnosticStageScope, materializeDiagnosticRetainedFields, sealDiagnosticStageLimitations, validateDiagnosticStageResult } from "./diagnostic-stage-results.js";
12
12
  import { readAcceptedStageData } from "./stage-report-renderer.js";
13
13
  import { registerDiagnosticStageGraph } from "./diagnostic-work-registration.js";
14
14
  import { PreparedDiagnosticWorkCollection } from "./diagnostic-work-collection.js";
@@ -53,7 +53,8 @@ export function finishDiagnosticStage(context, input) {
53
53
  const previous = previousStage ? readAcceptedStageData(context, { ...scope, stage: previousStage }) : null;
54
54
  const prior = previous ? JSON.parse(previous.bytes.toString("utf8")) : null;
55
55
  const collection = ["audit", "analyze", "review"].includes(input.stage) ? PreparedDiagnosticWorkCollection.prepare(context, input) : null;
56
- const candidates = ["audit", "analyze"].includes(input.stage) ? sealDiagnosticCandidateIds(input.bytes, collection.results()) : input.bytes;
56
+ const proposal = materializeDiagnosticRetainedFields(input.bytes, prior, input.stage);
57
+ const candidates = ["audit", "analyze"].includes(input.stage) ? sealDiagnosticCandidateIds(proposal, collection.results()) : proposal;
57
58
  const bytes = sealDiagnosticStageLimitations(candidates, [
58
59
  ...(prior?.details.limitations ?? []), ...(collection?.results() ?? []).flatMap(work => work.result.limitations)
59
60
  ], prior?.details.missing_expected_sources ?? []);
@@ -7,6 +7,32 @@ import { verifyDiagnosticCoverage, verifyDiagnosticSources } from "./diagnostic-
7
7
  const refuse = (reason) => {
8
8
  throw new AppError("diagnostic_stage_result_invalid", "Diagnostic result does not match its frozen stage", 2, { reason, effect: "no_effect", ...(reason === "result_schema_invalid" ? { phase: "prepare", recoverable: true } : {}) });
9
9
  };
10
+ /** Retained fields are CLI data, not a transcription task for the coordinator. */
11
+ export function materializeDiagnosticRetainedFields(bytes, prior, stage) {
12
+ if (!prior || !bytes.length || bytes.length > 1024 * 1024)
13
+ return bytes;
14
+ let value;
15
+ try {
16
+ value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
17
+ }
18
+ catch {
19
+ return bytes;
20
+ }
21
+ if (!value || typeof value !== "object" || !value.details || typeof value.details !== "object" || Array.isArray(value.details))
22
+ return bytes;
23
+ const retained = stage === "report" ? Object.entries(prior.details).filter(([key]) => key !== "limitations")
24
+ : ["focus", "coverage"].map(key => [key, prior.details[key]]);
25
+ for (const [key, expected] of retained) {
26
+ if (Object.hasOwn(value.details, key) && hybridDigest(value.details[key]) !== hybridDigest(expected))
27
+ throw new AppError("diagnostic_stage_invalid", "A proposal cannot replace CLI-retained predecessor data", 2, { reason: `accepted_${key}_changed`, effect: "no_effect", phase: "prepare", recoverable: true });
28
+ value.details[key] = structuredClone(expected);
29
+ }
30
+ if (!Object.hasOwn(value.details, "limitations"))
31
+ value.details.limitations = [];
32
+ if (!Object.hasOwn(value.details, "missing_expected_sources"))
33
+ value.details.missing_expected_sources = structuredClone(prior.details.missing_expected_sources);
34
+ return Buffer.from(JSON.stringify(value));
35
+ }
10
36
  /** Accepted limitations and missing-source observations belong to predecessor/Work custody, not a coordinator's
11
37
  * transcription. Keep its annotations too; validate the complete sealed result
12
38
  * with the frozen schema afterwards, without truncating overflowing data. */
@@ -1,5 +1,6 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { AppError } from "../shared/errors.js";
2
- import { isDiagnosticFlow } from "../domain/diagnostic-flows.js";
3
+ import { isSourceSnapshotFlow } from "../domain/source-snapshot-flows.js";
3
4
  import { PreparedInstalledFlowSource } from "./installed-flow-source.js";
4
5
  import { FrozenGenericExecution } from "./generic-execution.js";
5
6
  import { AdmittedFrozenEngine } from "./frozen-engine.js";
@@ -11,6 +12,7 @@ import { requireProjectByRoot } from "./projects.js";
11
12
  import { resolveProjectRoot } from "../storage/paths.js";
12
13
  import { hybridDigest } from "./hybrid-contracts.js";
13
14
  import { appendAudit } from "./audit.js";
15
+ import { prepareReviewFixSourceIntake } from "./review-fix-intake-custody.js";
14
16
  const event = "diagnostic.request";
15
17
  function refuse(reason) { throw new AppError("diagnostic_request_invalid", "Diagnostic request requires its original frozen selection", 2, { reason, effect: "no_effect" }); }
16
18
  export function readDiagnosticRequest(context, scope) {
@@ -22,18 +24,47 @@ export function readDiagnosticRequest(context, scope) {
22
24
  refuse("ambiguous_request");
23
25
  const value = JSON.parse(rows[0].payload_json);
24
26
  if (value.schema_id !== "dd-flow/diagnostic-request@1" || value.project_id !== project.id
25
- || hybridDigest(value.scope) !== hybridDigest(scope) || !isDiagnosticFlow(value.scope.flowId))
27
+ || hybridDigest(value.scope) !== hybridDigest(scope) || !isSourceSnapshotFlow(value.scope.flowId))
26
28
  refuse("request_scope_changed");
29
+ if (scope.flowId === "review-fix") {
30
+ try {
31
+ const source = value.repair_source;
32
+ if (!source || source.path !== "runtime/repair-intake.json" || typeof source.base64 !== "string"
33
+ || !/^[a-f0-9]{64}$/.test(source.sha256) || !Array.isArray(source.selection?.findingIds)
34
+ || !source.selection.findingIds.length || source.selection.findingIds.length > 4096)
35
+ refuse("repair_source_missing");
36
+ const bytes = Buffer.from(source.base64, "base64");
37
+ if (!bytes.length || bytes.length > 4 * 1024 * 1024 || bytes.toString("base64") !== source.base64
38
+ || createHash("sha256").update(bytes).digest("hex") !== source.sha256)
39
+ refuse("repair_source_changed");
40
+ const capsule = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
41
+ if (capsule.schema_id !== "dd-flow/review-fix-source-intake@1" || capsule.intake.authorization !== "not_granted"
42
+ || capsule.intake.source.id !== source.selection.sourceRunId
43
+ || hybridDigest(capsule.intake.selected_findings.map((finding) => finding.id)) !== hybridDigest(source.selection.findingIds))
44
+ refuse("repair_selection_changed");
45
+ }
46
+ catch (error) {
47
+ if (error instanceof AppError)
48
+ throw error;
49
+ return refuse("repair_source_invalid");
50
+ }
51
+ }
52
+ else if (value.repair_source !== undefined)
53
+ refuse("foreign_repair_source");
27
54
  return value;
28
55
  }
29
56
  /** Freeze all ambient choices before copying source or allocating a RUN. */
30
57
  export function retainDiagnosticRequest(context, input) {
31
58
  const { scope, source, execution, engine } = input;
32
59
  if (!(source instanceof PreparedInstalledFlowSource) || !(execution instanceof FrozenGenericExecution)
33
- || !(engine instanceof AdmittedFrozenEngine) || !isDiagnosticFlow(scope.flowId) || !/^[A-Za-z0-9][A-Za-z0-9_.-]{0,95}$/.test(scope.requestId)
60
+ || !(engine instanceof AdmittedFrozenEngine) || !isSourceSnapshotFlow(scope.flowId) || !/^[A-Za-z0-9][A-Za-z0-9_.-]{0,95}$/.test(scope.requestId)
34
61
  || !input.objective.trim() || Buffer.byteLength(input.objective) > 64 * 1024)
35
62
  refuse("invalid_selection");
63
+ if ((scope.flowId === "review-fix") !== (input.repairSelection !== undefined))
64
+ refuse("repair_selection_required_only_for_review_fix");
36
65
  const project = requireProjectByRoot(context, scope.projectRoot), frozen = execution.snapshot();
66
+ if (scope.flowId === "review-fix" && frozen.settings.stage_session_mode === "same_session")
67
+ refuse("independent_scope_review_session_required");
37
68
  const workspace = diagnosticSnapshotRoot(context, { projectId: project.id, projectRoot: scope.projectRoot, sourceRoot: scope.projectRoot,
38
69
  requestId: scope.requestId, flowId: scope.flowId });
39
70
  if (frozen.scope.projectRoot !== scope.projectRoot || frozen.scope.requestId !== scope.requestId || frozen.scope.workspace !== workspace
@@ -41,15 +72,34 @@ export function retainDiagnosticRequest(context, input) {
41
72
  || frozen.settings.attached_session_receipt || frozen.settings.delegation.native_capacity_receipt)
42
73
  refuse("execution_selection_changed");
43
74
  source.revalidate();
44
- engine.admitDiagnosticFlow(source, scope.flowId);
75
+ if (scope.flowId === "review-fix")
76
+ engine.admitReviewFixFlow(source);
77
+ else if (scope.flowId === "mb-distill")
78
+ engine.admitDistillFlow(source);
79
+ else
80
+ engine.admitDiagnosticFlow(source, scope.flowId);
45
81
  const manifest = engine.manifest();
46
82
  if (typeof manifest.integrity.checksum !== "string")
47
83
  refuse("engine_identity_missing");
84
+ let repairSource;
85
+ if (scope.flowId === "review-fix") {
86
+ const prior = readDiagnosticRequest(context, scope);
87
+ if (prior) {
88
+ if (hybridDigest(prior.repair_source.selection) !== hybridDigest(input.repairSelection))
89
+ refuse("repair_selection_changed");
90
+ repairSource = prior.repair_source;
91
+ }
92
+ else {
93
+ const prepared = prepareReviewFixSourceIntake(context, { projectRoot: scope.projectRoot, ...input.repairSelection });
94
+ repairSource = { selection: structuredClone(input.repairSelection), path: prepared.path, sha256: prepared.sha256, base64: prepared.bytes.toString("base64") };
95
+ }
96
+ }
48
97
  const value = { schema_id: "dd-flow/diagnostic-request@1", project_id: project.id, scope: structuredClone(scope),
49
98
  created_at: context.now(), objective: input.objective, selection: structuredClone(input.selection),
50
99
  source: source.snapshot(), source_sha256: source.sha256, execution: frozen, execution_sha256: execution.sha256,
51
100
  engine: { identity: { package_name: manifest.package_name, package_version: manifest.package_version, engine_version: manifest.engine_version,
52
- integrity_checksum: manifest.integrity.checksum, snapshot_root: manifest.snapshot_root }, manifest_sha256: engine.manifestSha256 } };
101
+ integrity_checksum: manifest.integrity.checksum, snapshot_root: manifest.snapshot_root }, manifest_sha256: engine.manifestSha256 },
102
+ ...(repairSource ? { repair_source: repairSource } : {}) };
53
103
  if (Buffer.byteLength(JSON.stringify(value)) > 48 * 1024 * 1024)
54
104
  refuse("request_exceeds_bound");
55
105
  return context.db.writeTransaction(() => {
@@ -79,6 +129,6 @@ export function startDiagnosticRun(context, scope) {
79
129
  requestId: scope.requestId, projectRoot: scope.projectRoot, workspace: snapshot.root
80
130
  });
81
131
  const engine = AdmittedFrozenEngine.admitSelection(retained.engine.identity, retained.engine.manifest_sha256);
82
- const bundle = prepareDiagnosticRunBundle(context, { source, execution, engine, boundAt: retained.created_at });
132
+ const bundle = prepareDiagnosticRunBundle(context, { source, execution, engine, boundAt: retained.created_at, requestText: retained.objective });
83
133
  return PreparedDescriptorRunStart.prepare(context, { bundle }).apply(context);
84
134
  }
@@ -17,7 +17,7 @@ export function assertDiagnosticSessionIndependence(workers, excluded) {
17
17
  || new Set(keys).size !== keys.length || keys.some(key => excludedKeys.has(key)))
18
18
  refuse("physical_session_reused");
19
19
  }
20
- function completedWork(context, scope, workId) {
20
+ export function readCompletedResearchWork(context, scope, workId) {
21
21
  const work = context.db.get("SELECT status,result FROM works WHERE project_id=? AND run_id=? AND work_id=?", [scope.projectId, scope.runId, workId]);
22
22
  if (work?.status !== "completed" || !work.result || Buffer.byteLength(work.result) > 1024 * 1024)
23
23
  refuse("work_not_completed");
@@ -36,12 +36,12 @@ function collectSql(context, scope, rootId) {
36
36
  if (!graph || graph.parent_work_id !== rootId)
37
37
  refuse("registration_required");
38
38
  const batch = diagnosticWorkBatch(graph.input);
39
- const works = batch.works.map(item => completedWork(context, scope, graph.work_ids[item.key]));
39
+ const works = batch.works.map(item => readCompletedResearchWork(context, scope, graph.work_ids[item.key]));
40
40
  const coordinators = context.db.all("SELECT s.harness,s.provider_session_id FROM work_sessions ws JOIN sessions s ON s.project_id=? AND s.run_id=? AND s.session_id=ws.session_id WHERE ws.work_id=?", [scope.projectId, scope.runId, rootId]);
41
41
  const authorGraph = scope.stage === "review" ? readRegisteredDiagnosticGraph(context, { ...scope, stage: graph.input.flow_id === "mb-audit" ? "audit" : "analyze" }) : null;
42
42
  if (scope.stage === "review" && !authorGraph)
43
43
  refuse("author_graph_missing");
44
- const authors = authorGraph ? Object.values(authorGraph.work_ids).map(id => completedWork(context, scope, id)) : [];
44
+ const authors = authorGraph ? Object.values(authorGraph.work_ids).map(id => readCompletedResearchWork(context, scope, id)) : [];
45
45
  assertDiagnosticSessionIndependence(works.map(value => value.session), [...coordinators, ...authors.map(value => value.session)]);
46
46
  return { graph, works, authors, coordinators };
47
47
  }
@@ -0,0 +1,280 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { AppError } from "../shared/errors.js";
5
+ import { hybridDigest } from "./hybrid-contracts.js";
6
+ import { MemoryRepairDocumentBaseline, orderMemoryDocumentEntries } from "./memory-repair-document-delta.js";
7
+ import { RetainedMemoryRepairPublicationPlan } from "./memory-repair-publication-plan.js";
8
+ import { stageRetainedMemoryRepairFile, verifyRetainedMemoryRepairFile } from "./memory-repair-publication-files.js";
9
+ import { acquireInstallationLock } from "./installation-lock.js";
10
+ import { publishImmutableInput } from "../storage/immutable-input.js";
11
+ import { ObservedMemorySources } from "./memory-source-observations.js";
12
+ const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
13
+ function refuse(reason) {
14
+ throw new AppError("distill_publication_invalid", "Distillation publication requires its exact retained document plan", 2, { reason, effect: "no_effect" });
15
+ }
16
+ function beforeFiles(plan) {
17
+ const encoded = JSON.parse(plan.bytes().toString("utf8"));
18
+ const before = JSON.parse(Buffer.from(encoded.before_base64, "base64").toString("utf8"));
19
+ return before.entries.filter((entry) => entry.kind === "file" && before.write_paths.includes(entry.path))
20
+ .map((entry) => ({ path: entry.path, sha256: entry.sha256 }));
21
+ }
22
+ /** Selected original text only. Protected neighboring/private document bytes
23
+ * must never be exported as verifier context. No destination write occurs. */
24
+ export function captureDistillBeforeDocuments(root, plan) {
25
+ const selected = beforeFiles(plan), observed = ObservedMemorySources.capture({ workspace: root }, selected.map(entry => ({ root: "workspace", path: entry.path })));
26
+ return validateDistillBeforeDocuments(plan, selected.map(entry => ({ ...entry, base64: observed.bytes({ root: "workspace", path: entry.path }).toString("base64") })));
27
+ }
28
+ /** SQL decoding is filesystem-free. The original before-view, not a fresh file,
29
+ * determines exact membership and hashes, including originally empty files. */
30
+ export function validateDistillBeforeDocuments(plan, value) {
31
+ const expected = beforeFiles(plan);
32
+ if (!Array.isArray(value) || value.length !== expected.length || value.length > 4096)
33
+ refuse("original_documents_required");
34
+ let total = 0;
35
+ const seen = new Set();
36
+ for (const document of value) {
37
+ if (!document || typeof document !== "object" || Object.keys(document).sort().join() !== "base64,path,sha256"
38
+ || typeof document.base64 !== "string" || document.base64.length > 24 * 1024 * 1024 || seen.has(document.path)
39
+ || !expected.some(entry => entry.path === document.path && entry.sha256 === document.sha256))
40
+ refuse("original_document_changed");
41
+ const bytes = Buffer.from(document.base64, "base64");
42
+ total += bytes.length;
43
+ if (bytes.length > 16 * 1024 * 1024 || total > 64 * 1024 * 1024 || bytes.toString("base64") !== document.base64 || hash(bytes) !== document.sha256)
44
+ refuse("original_document_changed");
45
+ seen.add(document.path);
46
+ }
47
+ const byPath = new Map(value.map(document => [document.path, document]));
48
+ return expected.map(entry => ({ ...entry, base64: byPath.get(entry.path).base64 }));
49
+ }
50
+ export function distillBeforeDocumentReferences(plan) {
51
+ return beforeDocumentReferences(plan, "distill");
52
+ }
53
+ export function memoryFixBeforeDocumentReferences(plan) {
54
+ return beforeDocumentReferences(plan, "memory-fix");
55
+ }
56
+ function beforeDocumentReferences(plan, namespace) {
57
+ return beforeFiles(plan).map(entry => ({ destination_path: entry.path,
58
+ source: { root: "run", path: `artifacts/${namespace}/before/${entry.sha256}.md`, sha256: entry.sha256 } }));
59
+ }
60
+ /** Restore original verifier inputs from retained SQL bytes before target
61
+ * effects, including after a lost projection. Never read the changed target. */
62
+ export function publishDistillBeforeDocuments(runRoot, plan, documents) {
63
+ return publishBeforeDocuments(runRoot, plan, documents, "distill");
64
+ }
65
+ export function publishMemoryFixBeforeDocuments(runRoot, plan, documents) {
66
+ return publishBeforeDocuments(runRoot, plan, documents, "memory-fix");
67
+ }
68
+ function publishBeforeDocuments(runRoot, plan, documents, family) {
69
+ const retained = validateDistillBeforeDocuments(plan, documents);
70
+ if (!retained.length)
71
+ return;
72
+ if (!path.isAbsolute(runRoot) || fs.realpathSync(runRoot) !== runRoot || !fs.lstatSync(runRoot).isDirectory())
73
+ refuse("unsafe_baseline_home");
74
+ const artifacts = privateDirectory(runRoot, "artifacts"), namespace = privateDirectory(artifacts, family), directory = privateDirectory(namespace, "before");
75
+ for (const document of retained)
76
+ publishImmutableInput(path.join(directory, `${document.sha256}.md`), Buffer.from(document.base64, "base64"), "distill_before_document_conflict");
77
+ }
78
+ /** Physical plan only, not scope approval or RUN/Work authority. The caller
79
+ * independently selects canonical exceptions and retains this plan in SQL
80
+ * before any apply. Capture protects the entire destination Memory Bank,
81
+ * including unrelated files and opaque private/link entries. */
82
+ export function prepareDistillDocumentPlan(destinationRoot, outputs, canonicalPaths = new Set()) {
83
+ return prepareDocumentPlan(destinationRoot, outputs, canonicalPaths, "distill");
84
+ }
85
+ /** Physical bounded repair plan only. A null output explicitly requests removal
86
+ * of that selected existing document. Intake/Work/user authority belongs to the
87
+ * caller; this helper never expands selection or admits canonical exceptions. */
88
+ export function prepareMemoryFixDocumentPlan(destinationRoot, outputs) {
89
+ return prepareDocumentPlan(destinationRoot, outputs, new Set(), "memory_fix");
90
+ }
91
+ function prepareDocumentPlan(destinationRoot, outputs, canonicalPaths, family) {
92
+ const refuse = (reason) => { throw new AppError(`${family}_publication_invalid`, "Publication requires its exact retained document plan", 2, { reason, effect: "no_effect" }); };
93
+ if (!outputs.length || outputs.length > 4096 || new Set(outputs.map(output => output.path)).size !== outputs.length)
94
+ refuse("invalid_output_selection");
95
+ let total = 0;
96
+ for (const output of outputs) {
97
+ if (output.bytes === null && family === "memory_fix")
98
+ continue;
99
+ if (!(output.bytes instanceof Uint8Array) || output.bytes.length > 16 * 1024 * 1024 || (total += output.bytes.length) > 64 * 1024 * 1024)
100
+ refuse("unbounded_output");
101
+ try {
102
+ new TextDecoder("utf-8", { fatal: true }).decode(output.bytes);
103
+ }
104
+ catch {
105
+ refuse("non_text_document");
106
+ }
107
+ }
108
+ const selected = [...outputs].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
109
+ const before = MemoryRepairDocumentBaseline.capture(destinationRoot, selected.map(output => output.path), canonicalPaths);
110
+ const entries = new Map(before.entries().map(entry => [entry.path, entry]));
111
+ const actions = [];
112
+ for (const output of selected) {
113
+ if (output.bytes === null) {
114
+ const prior = entries.get(output.path);
115
+ if (!prior || prior.kind !== "file")
116
+ return refuse("delete_requires_existing_document");
117
+ entries.delete(output.path);
118
+ actions.push({ path: output.path, before: prior, after: null, bytes_base64: null });
119
+ continue;
120
+ }
121
+ const parts = output.path.split("/");
122
+ for (let length = 1; length < parts.length; length++) {
123
+ const logical = parts.slice(0, length).join("/");
124
+ if (!entries.has(logical)) {
125
+ const after = { path: logical, kind: "directory", mode: 0o755, sha256: null };
126
+ entries.set(logical, after);
127
+ actions.push({ path: logical, before: null, after, bytes_base64: null });
128
+ }
129
+ }
130
+ const prior = entries.get(output.path) ?? null;
131
+ const after = { path: output.path, kind: "file", mode: prior?.mode ?? 0o644, sha256: hash(output.bytes) };
132
+ if (hybridDigest(prior) === hybridDigest(after))
133
+ continue;
134
+ entries.set(output.path, after);
135
+ actions.push({ path: output.path, before: prior, after, bytes_base64: Buffer.from(output.bytes).toString("base64") });
136
+ }
137
+ const beforeData = JSON.parse(before.bytes().toString("utf8"));
138
+ const afterMaterial = { ...beforeData, entries: orderMemoryDocumentEntries(entries.values()) };
139
+ const after = MemoryRepairDocumentBaseline.restore(Buffer.from(JSON.stringify(afterMaterial)), hybridDigest({ ...afterMaterial, schema_id: undefined }));
140
+ const receipt = { before_sha256: before.sha256, before_base64: before.bytes().toString("base64"),
141
+ after_sha256: after.sha256, after_base64: after.bytes().toString("base64"), actions };
142
+ before.revalidate(destinationRoot);
143
+ return RetainedMemoryRepairPublicationPlan.restore(Buffer.from(JSON.stringify(receipt)), hybridDigest(receipt));
144
+ }
145
+ function privateDirectory(parent, name) {
146
+ const directory = path.join(parent, name);
147
+ if (!fs.lstatSync(directory, { throwIfNoEntry: false }))
148
+ fs.mkdirSync(directory, { mode: 0o700 });
149
+ const stat = fs.lstatSync(directory);
150
+ if (!stat.isDirectory() || fs.realpathSync(directory) !== directory)
151
+ refuse("unsafe_staging_directory");
152
+ return directory;
153
+ }
154
+ function syncDirectory(directory) {
155
+ const fd = fs.openSync(directory, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
156
+ try {
157
+ fs.fsyncSync(fd);
158
+ }
159
+ finally {
160
+ fs.closeSync(fd);
161
+ }
162
+ }
163
+ /** One recoverable physical step of an independently retained plan. This
164
+ * grants no publication authority: its owner must fence SQL/source/scope
165
+ * custody on every call, then retain the actual completion separately.
166
+ * A changed prefix is never recaptured or overwritten on retry. */
167
+ export function applyDistillDocumentStep(destinationRoot, plan, assertAuthority) {
168
+ return applyDocumentStep(destinationRoot, plan, assertAuthority, "distill");
169
+ }
170
+ /** Same prefix/recovery mechanics as distill, with selected removals moved into
171
+ * private staging rather than erased. No Stage or publication authority here. */
172
+ export function applyMemoryFixDocumentStep(destinationRoot, plan, assertAuthority) {
173
+ return applyDocumentStep(destinationRoot, plan, assertAuthority, "memory_fix");
174
+ }
175
+ function applyDocumentStep(destinationRoot, plan, assertAuthority, family) {
176
+ const refuse = (reason) => { throw new AppError(`${family}_publication_invalid`, "Publication requires its exact retained document plan", 2, { reason, effect: "no_effect" }); };
177
+ if (!(plan instanceof RetainedMemoryRepairPublicationPlan) || typeof assertAuthority !== "function")
178
+ refuse("retained_plan_and_authority_required");
179
+ const actions = plan.actions();
180
+ if (family === "distill" && actions.some(action => !action.after))
181
+ refuse("distillation_does_not_delete_documents");
182
+ assertAuthority();
183
+ const initial = plan.classify(destinationRoot);
184
+ const removedDirectory = path.join(destinationRoot, ".dd-flow/memory-fix-publications", plan.sha256);
185
+ for (const [index, action] of actions.slice(0, initial.completed).entries())
186
+ if (!action.after) {
187
+ if (fs.realpathSync(removedDirectory) !== removedDirectory || (fs.lstatSync(removedDirectory).mode & 0o7777) !== 0o700)
188
+ refuse("removed_document_custody_changed");
189
+ const logical = `removed-${index}.md`, location = { root: "workspace", path: logical };
190
+ const original = ObservedMemorySources.capture({ workspace: removedDirectory }, [location]);
191
+ const stat = fs.lstatSync(path.join(removedDirectory, logical));
192
+ if (hash(original.bytes(location)) !== action.before.sha256 || !stat.isFile() || stat.nlink !== 1
193
+ || (stat.mode & 0o7777) !== action.before.mode)
194
+ refuse("removed_document_custody_changed");
195
+ syncDirectory(removedDirectory);
196
+ }
197
+ for (const action of actions.slice(0, initial.completed))
198
+ syncDirectory(path.dirname(path.join(destinationRoot, action.path)));
199
+ if (initial.completed === initial.total) {
200
+ assertAuthority();
201
+ return { ...initial, status: "applied", replayed: true };
202
+ }
203
+ const service = privateDirectory(destinationRoot, ".dd-flow"), namespace = privateDirectory(service, family === "distill" ? "distill-publications" : "memory-fix-publications");
204
+ const directory = privateDirectory(namespace, plan.sha256);
205
+ if ((fs.lstatSync(directory).mode & 0o7777) !== 0o700)
206
+ refuse("nonprivate_staging_directory");
207
+ publishImmutableInput(path.join(directory, "owner.json"), Buffer.from(JSON.stringify({ schema_id: family === "distill" ? "dd-flow/distill-document-staging@1" : "dd-flow/memory-fix-document-staging@1", destination_root: destinationRoot, plan_sha256: plan.sha256 })), `${family}_staging_conflict`);
208
+ const lock = acquireInstallationLock(path.join(namespace, "destination.lock"), plan.sha256);
209
+ let effected = false, released = false;
210
+ const release = () => { lock.release(); released = true; };
211
+ try {
212
+ lock.assertOwnership();
213
+ assertAuthority();
214
+ const current = plan.classify(destinationRoot);
215
+ if (current.completed !== initial.completed)
216
+ refuse("publication_cursor_changed");
217
+ const action = actions[current.completed], destination = path.join(destinationRoot, action.path);
218
+ if (!action.after) {
219
+ const removed = path.join(directory, `removed-${current.completed}.md`);
220
+ if (fs.lstatSync(removed, { throwIfNoEntry: false }))
221
+ refuse("foreign_removed_document");
222
+ assertAuthority();
223
+ lock.assertOwnership();
224
+ if (plan.classify(destinationRoot).completed !== current.completed)
225
+ refuse("publication_cursor_changed");
226
+ effected = true;
227
+ fs.renameSync(destination, removed);
228
+ syncDirectory(directory);
229
+ }
230
+ else if (action.after.kind === "file") {
231
+ const staged = stageRetainedMemoryRepairFile(directory, `action-${current.completed}.tmp`, plan.output(action.path), action.after.mode);
232
+ verifyRetainedMemoryRepairFile(staged.path, plan.output(action.path), action.after.mode);
233
+ assertAuthority();
234
+ lock.assertOwnership();
235
+ if (plan.classify(destinationRoot).completed !== current.completed)
236
+ refuse("publication_cursor_changed");
237
+ effected = true;
238
+ fs.renameSync(staged.path, destination);
239
+ syncDirectory(directory);
240
+ }
241
+ else {
242
+ const staged = path.join(directory, `directory-${current.completed}.tmp`);
243
+ if (!fs.lstatSync(staged, { throwIfNoEntry: false }))
244
+ fs.mkdirSync(staged, { mode: 0o700 });
245
+ const stat = fs.lstatSync(staged);
246
+ if (!stat.isDirectory() || fs.realpathSync(staged) !== staged || fs.readdirSync(staged).length
247
+ || ![0o700, action.after.mode].includes(stat.mode & 0o7777))
248
+ refuse("foreign_staged_directory");
249
+ fs.chmodSync(staged, action.after.mode);
250
+ syncDirectory(staged);
251
+ assertAuthority();
252
+ lock.assertOwnership();
253
+ if (plan.classify(destinationRoot).completed !== current.completed)
254
+ refuse("publication_cursor_changed");
255
+ effected = true;
256
+ fs.renameSync(staged, destination);
257
+ syncDirectory(directory);
258
+ }
259
+ syncDirectory(path.dirname(destination));
260
+ const observed = plan.classify(destinationRoot);
261
+ if (observed.completed !== current.completed + 1)
262
+ refuse("unexpected_publication_effect");
263
+ release();
264
+ return { ...observed, status: observed.completed === observed.total ? "applied" : "partial", replayed: false };
265
+ }
266
+ catch (error) {
267
+ let releaseFailure;
268
+ if (!released)
269
+ try {
270
+ release();
271
+ }
272
+ catch (failure) {
273
+ releaseFailure = failure instanceof Error ? failure.message : String(failure);
274
+ }
275
+ if (!effected && !releaseFailure)
276
+ throw error;
277
+ throw new AppError(`${family}_publication_pending`, "Reconcile the same retained publication; do not prepare a new baseline", 1, { effect: effected ? "publication_pending" : "no_effect", plan_sha256: plan.sha256, cause: error instanceof Error ? error.message : String(error),
278
+ ...(releaseFailure ? { release_failure: releaseFailure } : {}) });
279
+ }
280
+ }