@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
@@ -0,0 +1,197 @@
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 { readFlowRunExecutionBundle, assertPreparedRunBundleSqlAuthority } from "./runs.js";
6
+ import { requireProjectByRoot } from "./projects.js";
7
+ import { resolveProjectRoot } from "../storage/paths.js";
8
+ import { readDescriptorStageInput } from "./descriptor-stage-coordinator.js";
9
+ import { readAcceptedStageData, prepareStagePacket, acceptStagePacket, publishStagePacket } from "./stage-report-renderer.js";
10
+ import { diagnosticSnapshotPaths, readDiagnosticSnapshot, readDiagnosticSnapshotCustody } from "./diagnostic-snapshot.js";
11
+ import { validateDistillStageResult } from "./distill-stage-results.js";
12
+ import { PreparedDistillWorkCollection } from "./distill-work-collection.js";
13
+ import { collectDistillCandidates } from "./distill-work-results.js";
14
+ import { readDistillUserInputs } from "./distill-user-inputs.js";
15
+ import { readPreparedMemoryFlowUserInputs } from "./memory-flow-user-inputs.js";
16
+ import { prepareDistillDocumentPlan, applyDistillDocumentStep, captureDistillBeforeDocuments, publishDistillBeforeDocuments } from "./distill-document-publication.js";
17
+ import { readDistillPublication, retainDistillPublication, completeDistillPublication, distillPublicationArtifact, readDistillPublicationReference } from "./distill-publication-custody.js";
18
+ import { hybridDigest } from "./hybrid-contracts.js";
19
+ function refuse(reason) {
20
+ throw new AppError("distill_promotion_invalid", "Promotion requires accepted review, delivered agreement and its exact destination", 2, { reason, effect: "no_effect" });
21
+ }
22
+ const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
23
+ const within = (root, target) => { const relative = path.relative(root, target); return !relative || !path.isAbsolute(relative) && relative !== ".." && !relative.startsWith(`..${path.sep}`); };
24
+ function destination(root, protectedRoots) {
25
+ if (!path.isAbsolute(root) || path.resolve(root) !== root || fs.realpathSync(root) !== root || !fs.lstatSync(root).isDirectory())
26
+ refuse("physical_destination_required");
27
+ for (const protectedRoot of protectedRoots) {
28
+ const resolved = fs.existsSync(protectedRoot) ? fs.realpathSync(protectedRoot) : path.resolve(protectedRoot);
29
+ if (within(resolved, root) || within(root, resolved))
30
+ refuse("destination_overlaps_source_or_runtime");
31
+ }
32
+ const stat = fs.lstatSync(root);
33
+ return { dev: stat.dev, ino: stat.ino };
34
+ }
35
+ /** The coordinator owns the semantic interpretation of agreement and proposed
36
+ * wording. This owner admits only actual user-source references and accepted
37
+ * independently reviewed candidates, then fences the full input set on effects.
38
+ * It does not infer agreement from a file named approval or from critic votes.
39
+ * No public launch/handler registration is implied by this internal owner. */
40
+ export class PreparedDistillPromotion {
41
+ #binding;
42
+ #plan;
43
+ #fence;
44
+ #publication;
45
+ #beforeDocuments;
46
+ #runRoot;
47
+ constructor(binding, plan, fence, publication, beforeDocuments, runRoot) {
48
+ this.#binding = structuredClone(binding);
49
+ this.#plan = plan;
50
+ this.#fence = fence;
51
+ this.#publication = { ...publication };
52
+ Object.freeze(this);
53
+ this.#beforeDocuments = structuredClone(beforeDocuments);
54
+ this.#runRoot = runRoot;
55
+ }
56
+ static prepare(context, input) {
57
+ if (!input.proposal || typeof input.proposal !== "object" || Array.isArray(input.proposal))
58
+ refuse("invalid_proposal_json");
59
+ const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot)), bundle = readFlowRunExecutionBundle(context, input);
60
+ if (!bundle || bundle.manifest.flow_id !== "mb-distill")
61
+ refuse("distill_bundle_required");
62
+ const scope = { projectId: project.id, runId: input.runId, stage: "promote" };
63
+ const stageInput = readDescriptorStageInput(context, scope, bundle);
64
+ if (stageInput.input.predecessor?.stage !== "review")
65
+ refuse("accepted_review_required");
66
+ const accepted = readAcceptedStageData(context, { ...scope, stage: "review" });
67
+ if (hybridDigest(stageInput.input.predecessor.result) !== hybridDigest(accepted.result))
68
+ refuse("review_predecessor_changed");
69
+ const frozen = bundle.execution.snapshot().scope, snapshotScope = { projectId: project.id, projectRoot: frozen.projectRoot,
70
+ sourceRoot: frozen.projectRoot, requestId: frozen.requestId, flowId: "mb-distill" };
71
+ const snapshot = readDiagnosticSnapshot(context, snapshotScope, input.runId);
72
+ if (!snapshot || snapshot.root !== frozen.workspace)
73
+ refuse("source_snapshot_required");
74
+ const reviewInput = readDescriptorStageInput(context, { ...scope, stage: "review" }, bundle, true, true);
75
+ const review = validateDistillStageResult(accepted.bytes, bundle.descriptor.stageBlob("review", "result_schema"), {
76
+ runId: input.runId, stage: "review", inputSha256: reviewInput.inputSha256, snapshotSha256: snapshot.source_sha256,
77
+ snapshotRoot: diagnosticSnapshotPaths(context, snapshot, input.runId).root, runRoot: stageInput.runRoot,
78
+ support: bundle.manifest.inventory.filter(ref => ref.path.startsWith("support/")), writeScope: null, promotionReceipt: null,
79
+ predecessors: reviewInput.input.predecessor ? [{ stage: reviewInput.input.predecessor.stage, ...reviewInput.input.predecessor.result }] : []
80
+ });
81
+ const authors = PreparedDistillWorkCollection.prepare(context, { ...input, stage: "extract" });
82
+ const critics = PreparedDistillWorkCollection.prepare(context, { ...input, stage: "review" }), criticResults = critics.results();
83
+ if (hybridDigest(review.details.candidates) !== hybridDigest(collectDistillCandidates(authors.results()).candidates))
84
+ refuse("review_candidates_changed");
85
+ for (const decision of review.details.decisions) {
86
+ if (!criticResults.some(work => work.result.decisions.some(vote => vote.candidate_id === decision.candidate_id && vote.disposition === decision.disposition)))
87
+ refuse("unsupported_review_resolution");
88
+ }
89
+ const users = readDistillUserInputs(context, input, bundle), proposal = structuredClone(input.proposal), selected = proposal.write_scope;
90
+ if (proposal.user_inputs_sha256 !== hybridDigest(users))
91
+ refuse("user_inputs_changed");
92
+ if (!selected || typeof selected.destination_root !== "string" || !Array.isArray(selected.candidate_ids) || !selected.candidate_ids.length
93
+ || new Set(selected.candidate_ids).size !== selected.candidate_ids.length || !Array.isArray(selected.paths) || !selected.paths.length
94
+ || new Set(selected.paths).size !== selected.paths.length || selected.paths.some(value => typeof value !== "string") || !selected.approval_source
95
+ || selected.approval_source.root !== "run" || !users.sources.some(source => ["request", "answer"].includes(source.role)
96
+ && source.path === selected.approval_source.path && source.sha256 === selected.approval_source.sha256))
97
+ refuse("delivered_agreement_required");
98
+ if (selected.candidate_ids.some(id => !review.details.decisions.some(decision => decision.candidate_id === id && ["adopt", "adapt"].includes(decision.disposition))))
99
+ refuse("unaccepted_candidate");
100
+ if (!Array.isArray(proposal.outputs) || !proposal.outputs.length || proposal.outputs.length > 4096
101
+ || proposal.outputs.some(output => !output || typeof output.content !== "string" || !selected.paths.includes(output.path)
102
+ || !Array.isArray(output.candidate_ids) || !output.candidate_ids.length || new Set(output.candidate_ids).size !== output.candidate_ids.length
103
+ || output.candidate_ids.some(id => !selected.candidate_ids.includes(id)))
104
+ || hybridDigest(proposal.outputs.map(output => output.path).sort()) !== hybridDigest([...selected.paths].sort())
105
+ || selected.candidate_ids.some(id => !proposal.outputs.some(output => output.candidate_ids.includes(id))))
106
+ refuse("output_scope_changed");
107
+ if (proposal.outputs.some(output => Buffer.byteLength(output.content) > 16 * 1024 * 1024)
108
+ || proposal.outputs.reduce((sum, output) => sum + Buffer.byteLength(output.content), 0) > 64 * 1024 * 1024)
109
+ refuse("unbounded_output");
110
+ const protectedRoots = [frozen.projectRoot, frozen.workspace, stageInput.runRoot, context.ddFlowHome];
111
+ const physical = destination(selected.destination_root, protectedRoots);
112
+ const binding = { project_id: project.id, run_id: input.runId, attempt: stageInput.attempt,
113
+ input_sha256: stageInput.inputSha256, bundle_sha256: bundle.sha256, review_sha256: accepted.result.sha256,
114
+ user_inputs_sha256: proposal.user_inputs_sha256, write_scope: selected };
115
+ const previous = readDistillPublication(context, binding);
116
+ const outputs = proposal.outputs.map(output => ({ path: output.path, bytes: Buffer.from(output.content) }));
117
+ const plan = previous?.plan ?? prepareDistillDocumentPlan(selected.destination_root, outputs, new Set(selected.paths));
118
+ const beforeDocuments = previous?.beforeDocuments ?? captureDistillBeforeDocuments(selected.destination_root, plan);
119
+ // Replay compares proposed bytes with the retained after-view; it never
120
+ // recaptures a partly changed destination or replaces the original intent.
121
+ const encoded = JSON.parse(plan.bytes().toString("utf8"));
122
+ const after = JSON.parse(Buffer.from(encoded.after_base64, "base64").toString("utf8"));
123
+ if (outputs.some(output => !after.entries.some((entry) => entry.path === output.path && entry.kind === "file" && entry.sha256 === hash(output.bytes))))
124
+ refuse("original_outputs_required");
125
+ const fence = (writer) => {
126
+ assertPreparedRunBundleSqlAuthority(writer, scope, bundle);
127
+ if (hybridDigest(readDescriptorStageInput(writer, scope, bundle)) !== hybridDigest(stageInput)
128
+ || hybridDigest(readAcceptedStageData(writer, { ...scope, stage: "review" })) !== hybridDigest(accepted)
129
+ || hybridDigest(readDiagnosticSnapshotCustody(writer, snapshotScope, input.runId)) !== hybridDigest(snapshot)
130
+ || hybridDigest(readPreparedMemoryFlowUserInputs(writer, scope, "mb-distill", bundle)) !== proposal.user_inputs_sha256
131
+ || hybridDigest(destination(selected.destination_root, protectedRoots)) !== hybridDigest(physical))
132
+ refuse("promotion_authority_changed");
133
+ authors.assertCustody(writer);
134
+ critics.assertCustody(writer);
135
+ };
136
+ const logical = `${bundle.descriptor.stage("promote").directory}/publication.json`;
137
+ return new PreparedDistillPromotion(binding, plan, fence, { file: path.join(stageInput.runRoot, logical), logical }, beforeDocuments, stageInput.runRoot);
138
+ }
139
+ binding() { return structuredClone(this.#binding); }
140
+ assertCustody(context) { this.#fence(context); }
141
+ applyStep(context) {
142
+ const fence = () => this.#fence(context), root = this.#binding.write_scope.destination_root;
143
+ retainDistillPublication(context, this.#binding, this.#plan, fence, this.#beforeDocuments);
144
+ publishDistillBeforeDocuments(this.#runRoot, this.#plan, this.#beforeDocuments);
145
+ const result = applyDistillDocumentStep(root, this.#plan, fence);
146
+ if (result.status !== "applied")
147
+ return { ...result, status: "partial" };
148
+ try {
149
+ const completed = completeDistillPublication(context, this.#binding, fence);
150
+ return { ...result, status: "applied", receipt: completed.receipt };
151
+ }
152
+ catch (error) {
153
+ // Files may already be published even though the final SQL observation
154
+ // failed. Do not report no_effect or let a caller replace the old intent.
155
+ throw new AppError("distill_publication_pending", "Reconcile the retained publication before claiming completion", 1, { effect: "publication_pending", plan_sha256: this.#plan.sha256, cause: error instanceof Error ? error.message : String(error) });
156
+ }
157
+ }
158
+ apply(context) {
159
+ const maximum = this.#plan.actions().length;
160
+ for (let step = 0; step <= maximum; step++) {
161
+ const result = this.applyStep(context);
162
+ if (result.status !== "applied")
163
+ continue;
164
+ try {
165
+ const packet = prepareStagePacket(this.#publication.file, distillPublicationArtifact(this.#binding, this.#plan));
166
+ context.db.writeTransaction(() => {
167
+ this.#fence(context);
168
+ const completed = readDistillPublication(context, this.#binding);
169
+ if (!completed?.completed || completed.plan.sha256 !== this.#plan.sha256)
170
+ refuse("completed_publication_required");
171
+ if (!readDistillPublicationReference(context, this.#binding, this.#plan, this.#publication.logical))
172
+ acceptStagePacket(context, { projectId: this.#binding.project_id, runId: this.#binding.run_id, stage: "promote", prepared: packet });
173
+ });
174
+ publishStagePacket(context, this.#binding.project_id, this.#binding.run_id, "promote", packet.file);
175
+ return { ...result, publication: { root: "run", path: this.#publication.logical, sha256: packet.sha256 } };
176
+ }
177
+ catch (error) {
178
+ throw new AppError("distill_publication_pending", "Published documents require their retained result projection before Stage acceptance", 1, { effect: "publication_pending", plan_sha256: this.#plan.sha256, cause: error instanceof Error ? error.message : String(error) });
179
+ }
180
+ }
181
+ return refuse("publication_did_not_progress");
182
+ }
183
+ }
184
+ export function publishDistillFromStage(context, input) {
185
+ if (!input.bytes.length || input.bytes.length > 1024 * 1024)
186
+ refuse("unbounded_proposal");
187
+ let proposal;
188
+ try {
189
+ proposal = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(input.bytes));
190
+ }
191
+ catch {
192
+ return refuse("invalid_proposal_json");
193
+ }
194
+ if (!proposal || typeof proposal !== "object" || Array.isArray(proposal))
195
+ refuse("invalid_proposal_json");
196
+ return PreparedDistillPromotion.prepare(context, { projectRoot: input.projectRoot, runId: input.runId, proposal }).apply(context);
197
+ }
@@ -0,0 +1,162 @@
1
+ import { AppError } from "../shared/errors.js";
2
+ import { RetainedMemoryRepairPublicationPlan } from "./memory-repair-publication-plan.js";
3
+ import { hybridDigest } from "./hybrid-contracts.js";
4
+ import { appendAudit } from "./audit.js";
5
+ import path from "node:path";
6
+ import { createHash } from "node:crypto";
7
+ import { resolveStagePublicationPath } from "./stage-report-renderer.js";
8
+ import { validateDistillBeforeDocuments, distillBeforeDocumentReferences } from "./distill-document-publication.js";
9
+ const intentEvent = "distill.publication_intent", completeEvent = "distill.publication_complete";
10
+ const key = (binding) => `distill_publication:${hybridDigest({ project_id: binding.project_id, run_id: binding.run_id })}`;
11
+ function refuse(reason) {
12
+ throw new AppError("distill_publication_custody_invalid", "Publication requires its original approved binding and retained plan", 1, { reason, effect: "no_effect" });
13
+ }
14
+ const receipt = (binding, plan, completed = false) => ({
15
+ schema_id: completed ? "dd-flow/distill-publication-complete@1" : "dd-flow/distill-publication-intent@1",
16
+ run_id: binding.run_id, binding_sha256: hybridDigest(binding), plan_sha256: plan.sha256
17
+ });
18
+ /** SQL custody only. The caller derives binding from the active Stage,
19
+ * accepted review and delivered user input, never from model authority fields.
20
+ * An existing intent cannot adopt today's partly changed destination as baseline. */
21
+ export function readDistillPublication(context, binding) {
22
+ return context.db.readSnapshot(() => {
23
+ const row = context.db.get("SELECT value_json FROM runtime_config WHERE key=?", [key(binding)]);
24
+ const events = (type) => context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type=? AND json_extract(payload_json,'$.run_id')=? LIMIT 2", [binding.project_id, type, binding.run_id]);
25
+ const intents = events(intentEvent), completions = events(completeEvent);
26
+ if (!row && !intents.length && !completions.length)
27
+ return null;
28
+ if (!row || intents.length !== 1 || completions.length > 1 || Buffer.byteLength(row.value_json) > 180 * 1024 * 1024
29
+ || [...intents, ...completions].some(value => Buffer.byteLength(value.payload_json) > 32768))
30
+ refuse("missing_or_unbounded_custody");
31
+ try {
32
+ const value = JSON.parse(row.value_json);
33
+ if (!value || Object.keys(value).sort().join() !== "before_documents,binding,plan_json,plan_sha256" || hybridDigest(value.binding) !== hybridDigest(binding)
34
+ || typeof value.plan_json !== "string")
35
+ refuse("binding_changed");
36
+ const plan = RetainedMemoryRepairPublicationPlan.restore(Buffer.from(value.plan_json), value.plan_sha256);
37
+ const beforeDocuments = validateDistillBeforeDocuments(plan, value.before_documents);
38
+ if (hybridDigest(JSON.parse(intents[0].payload_json)) !== hybridDigest(receipt(binding, plan))
39
+ || completions.length && hybridDigest(JSON.parse(completions[0].payload_json)) !== hybridDigest(receipt(binding, plan, true)))
40
+ refuse("receipt_changed");
41
+ return { plan, beforeDocuments, completed: completions.length === 1 };
42
+ }
43
+ catch (error) {
44
+ if (error instanceof AppError)
45
+ throw error;
46
+ return refuse("invalid_retained_publication");
47
+ }
48
+ });
49
+ }
50
+ export function retainDistillPublication(context, binding, plan, assertAuthority, beforeDocuments = []) {
51
+ if (!(plan instanceof RetainedMemoryRepairPublicationPlan) || typeof assertAuthority !== "function")
52
+ refuse("prepared_plan_required");
53
+ const paths = JSON.parse(Buffer.from(JSON.parse(plan.bytes().toString("utf8")).before_base64, "base64").toString("utf8")).write_paths;
54
+ if (hybridDigest([...paths].sort()) !== hybridDigest([...binding.write_scope.paths].sort()))
55
+ refuse("plan_scope_changed");
56
+ const originals = validateDistillBeforeDocuments(plan, beforeDocuments);
57
+ return context.db.writeTransaction(() => {
58
+ assertAuthority();
59
+ const prior = readDistillPublication(context, binding);
60
+ if (prior) {
61
+ if (prior.plan.sha256 !== plan.sha256 || hybridDigest(prior.beforeDocuments) !== hybridDigest(originals))
62
+ refuse("original_plan_required");
63
+ return { changed: false, plan: prior.plan };
64
+ }
65
+ // A new intent must describe the original destination, not an already
66
+ // applied prefix or a different directory with coincidentally similar paths.
67
+ if (plan.classify(binding.write_scope.destination_root).completed !== 0)
68
+ refuse("original_destination_required");
69
+ const now = context.now();
70
+ const value = JSON.stringify({ binding, plan_json: plan.bytes().toString("utf8"), plan_sha256: plan.sha256, before_documents: originals });
71
+ if (Buffer.byteLength(value) > 180 * 1024 * 1024)
72
+ refuse("unbounded_publication_custody");
73
+ context.db.run("INSERT INTO runtime_config(key,value_json,created_at,updated_at) VALUES (?,?,?,?)", [key(binding), value, now, now]);
74
+ appendAudit(context, { projectId: binding.project_id, eventType: intentEvent, payload: receipt(binding, plan) });
75
+ if (readDistillPublication(context, binding)?.plan.sha256 !== plan.sha256)
76
+ refuse("intent_not_retained");
77
+ return { changed: true, plan };
78
+ });
79
+ }
80
+ /** Physical readback precedes the short SQL writer; the owner fences its
81
+ * accepted inputs in that writer. This receipt is not independent verification
82
+ * or terminal RUN acceptance. */
83
+ export function completeDistillPublication(context, binding, assertAuthority) {
84
+ if (typeof assertAuthority !== "function")
85
+ refuse("authority_fence_required");
86
+ const retained = readDistillPublication(context, binding);
87
+ if (!retained)
88
+ refuse("intent_required");
89
+ const actual = retained.plan.classify(binding.write_scope.destination_root);
90
+ if (actual.completed !== actual.total)
91
+ refuse("publication_not_applied");
92
+ return context.db.writeTransaction(() => {
93
+ assertAuthority();
94
+ const current = readDistillPublication(context, binding);
95
+ if (!current || current.plan.sha256 !== retained.plan.sha256)
96
+ refuse("intent_changed");
97
+ // The authority fence may perform reads/callbacks. Never certify an earlier
98
+ // filesystem observation if the destination changed while it ran.
99
+ const observed = current.plan.classify(binding.write_scope.destination_root);
100
+ if (observed.completed !== observed.total)
101
+ refuse("publication_not_applied");
102
+ if (!current.completed)
103
+ appendAudit(context, { projectId: binding.project_id, eventType: completeEvent, payload: receipt(binding, current.plan, true) });
104
+ if (!readDistillPublication(context, binding)?.completed)
105
+ refuse("completion_not_retained");
106
+ return { changed: !current.completed, plan: current.plan, receipt: receipt(binding, current.plan, true) };
107
+ });
108
+ }
109
+ /** Read original completed intent for downstream Stage handoff, never infer a
110
+ * write scope from its result proposal or recapture today's destination. */
111
+ export function readCompletedDistillPublication(context, expected) {
112
+ return context.db.readSnapshot(() => {
113
+ const row = context.db.get("SELECT value_json FROM runtime_config WHERE key=?", [key(expected)]);
114
+ if (!row || Buffer.byteLength(row.value_json) > 180 * 1024 * 1024)
115
+ refuse("completed_intent_required");
116
+ let binding;
117
+ try {
118
+ binding = JSON.parse(row.value_json).binding;
119
+ }
120
+ catch {
121
+ return refuse("invalid_retained_publication");
122
+ }
123
+ if (!binding || !binding.write_scope || !Array.isArray(binding.write_scope.paths)
124
+ || typeof binding.user_inputs_sha256 !== "string" || !/^[a-f0-9]{64}$/.test(binding.user_inputs_sha256)
125
+ || hybridDigest(binding) !== hybridDigest({ ...expected, write_scope: binding.write_scope, user_inputs_sha256: binding.user_inputs_sha256 }))
126
+ refuse("binding_changed");
127
+ const retained = readDistillPublication(context, binding);
128
+ if (!retained?.completed)
129
+ refuse("publication_not_complete");
130
+ return { binding: structuredClone(binding), plan: retained.plan, beforeDocuments: retained.beforeDocuments };
131
+ });
132
+ }
133
+ /** Serialization only, not proof of effects or Stage acceptance. Both publisher
134
+ * and handoff reader use exactly these bytes selected from committed intent. */
135
+ export function distillPublicationArtifact(binding, plan) {
136
+ const encoded = JSON.parse(plan.bytes().toString("utf8"));
137
+ const after = JSON.parse(Buffer.from(encoded.after_base64, "base64").toString("utf8"));
138
+ return Buffer.from(JSON.stringify({ schema_id: "dd-flow/distill-publication@1", binding, plan_sha256: plan.sha256,
139
+ documents: after.entries.filter((entry) => binding.write_scope.paths.includes(entry.path)),
140
+ before_documents: distillBeforeDocumentReferences(plan), receipt: receipt(binding, plan, true) }));
141
+ }
142
+ /** Retained packet custody, not a file with a persuasive name. Return null only
143
+ * when publication has not retained this projection; conflicting custody refuses.
144
+ * Identical old retries remain readable, but publishers must not append more. */
145
+ export function readDistillPublicationReference(context, binding, plan, logicalPath) {
146
+ if (!logicalPath || path.isAbsolute(logicalPath) || logicalPath.includes("\\") || logicalPath.split("/").some(part => !part || part === "." || part === ".."))
147
+ refuse("invalid_publication_locator");
148
+ const run = context.db.get("SELECT run_root FROM runs WHERE project_id=? AND id=?", [binding.project_id, binding.run_id]);
149
+ if (!run?.run_root)
150
+ refuse("run_home_required");
151
+ const file = path.join(run.run_root, logicalPath), bytes = distillPublicationArtifact(binding, plan), sha256 = createHash("sha256").update(bytes).digest("hex");
152
+ const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type='flow_run.stage_packet' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.stage')='promote' AND json_extract(payload_json,'$.attempt')=? LIMIT 33", [binding.project_id, binding.run_id, binding.attempt]);
153
+ if (rows.length > 32 || rows.some(row => Buffer.byteLength(row.payload_json) > 8 * 1024 * 1024))
154
+ refuse("unbounded_publication_packets");
155
+ const packets = rows.map(row => JSON.parse(row.payload_json)).filter(packet => typeof packet.file === "string"
156
+ && resolveStagePublicationPath(context, binding.project_id, binding.run_id, packet.file) === file);
157
+ if (!packets.length)
158
+ return null;
159
+ if (packets.some(packet => packet.sha256 !== sha256 || packet.base64 !== bytes.toString("base64")))
160
+ refuse("publication_packet_changed");
161
+ return { root: "run", path: logicalPath, sha256 };
162
+ }
@@ -0,0 +1,249 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs";
3
+ import { AppError } from "../shared/errors.js";
4
+ import { entityReferenceVariants } from "../shared/entity-references.js";
5
+ import { distillStages } from "../domain/distill-flow.js";
6
+ import { requireProjectByRoot } from "./projects.js";
7
+ import { resolveProjectRoot } from "../storage/paths.js";
8
+ import { readFlowRunExecutionBundle } from "./runs.js";
9
+ import { readDiagnosticSnapshot, readDiagnosticSnapshotCustody, diagnosticSnapshotPaths } from "./diagnostic-snapshot.js";
10
+ import { PreparedDescriptorStageStart } from "./descriptor-stage-start.js";
11
+ import { PreparedDescriptorStageCoordinator, readDescriptorStageInput } from "./descriptor-stage-coordinator.js";
12
+ import { PreparedDescriptorStageSettlement, readAcceptedDescriptorStageSettlement } from "./descriptor-stage-settlement.js";
13
+ import { PreparedDescriptorRunSettlement } from "./descriptor-run-settlement.js";
14
+ import { readAcceptedStageData, prepareStageReport, acceptStageReport, publishStageReport, publishStagePacket, resolveStagePublicationPath } from "./stage-report-renderer.js";
15
+ import { publishDistillBeforeDocuments } from "./distill-document-publication.js";
16
+ import { validateDistillStageResult, assertDistillStageScope } from "./distill-stage-results.js";
17
+ import { registerDistillStageGraph } from "./distill-work-registration.js";
18
+ import { PreparedDistillWorkCollection } from "./distill-work-collection.js";
19
+ import { collectDistillCandidates } from "./distill-work-results.js";
20
+ import { readCompletedDistillPublication, readDistillPublicationReference, distillPublicationArtifact } from "./distill-publication-custody.js";
21
+ import { verifyDiagnosticSources } from "./diagnostic-evidence.js";
22
+ import { readDistillUserInputs } from "./distill-user-inputs.js";
23
+ import { readPreparedMemoryFlowUserInputs } from "./memory-flow-user-inputs.js";
24
+ import { assertDiagnosticSessionIndependence } from "./diagnostic-work-collection.js";
25
+ import { stageLifecycleMatchKey } from "./hooks.js";
26
+ import { hybridDigest } from "./hybrid-contracts.js";
27
+ function refuse(reason) {
28
+ const correctable = reason.startsWith("changed_") || ["invalid_result_json", "invalid_result_details", "result_exceeds_bound",
29
+ "unsupported_critic_resolution", "unowned_publication_claim", "preservation_evidence_required", "destination_evidence_required"].includes(reason);
30
+ throw new AppError("distill_stage_invalid", "Distillation stage requires accepted research, scope and independent observed verification", 2, { reason, effect: "no_effect", ...(correctable ? { phase: "prepare", recoverable: true } : {}) });
31
+ }
32
+ function select(context, input) {
33
+ const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot)), bundle = readFlowRunExecutionBundle(context, input);
34
+ if (!bundle || bundle.manifest.flow_id !== "mb-distill" || !distillStages.includes(input.stage))
35
+ refuse("registered_distill_stage_required");
36
+ const frozen = bundle.execution.snapshot().scope, snapshotSelection = { projectId: project.id, projectRoot: frozen.projectRoot,
37
+ sourceRoot: frozen.projectRoot, requestId: frozen.requestId, flowId: "mb-distill" };
38
+ const snapshot = readDiagnosticSnapshot(context, snapshotSelection, input.runId);
39
+ if (!snapshot || snapshot.root !== frozen.workspace)
40
+ refuse("snapshot_required");
41
+ return { bundle, snapshot, snapshotSelection, scope: { projectId: project.id, runId: input.runId, stage: input.stage } };
42
+ }
43
+ function observeSelectedPublication(publication) {
44
+ const root = publication.binding.write_scope.destination_root;
45
+ if (fs.realpathSync(root) !== root || !fs.lstatSync(root).isDirectory())
46
+ refuse("destination_changed");
47
+ const documents = JSON.parse(distillPublicationArtifact(publication.binding, publication.plan).toString("utf8")).documents;
48
+ verifyDiagnosticSources(root, documents);
49
+ for (const document of documents) {
50
+ let parent = root;
51
+ for (const part of document.path.split("/").slice(0, -1)) {
52
+ parent = path.join(parent, part);
53
+ if (!fs.lstatSync(parent).isDirectory())
54
+ refuse("destination_parent_changed");
55
+ }
56
+ const stat = fs.lstatSync(path.join(root, document.path));
57
+ if (!stat.isFile() || stat.nlink !== 1 || (stat.mode & 0o7777) !== document.mode)
58
+ refuse("published_output_changed");
59
+ }
60
+ }
61
+ /** Internal handlers until descriptor, launch and installed family gates close.
62
+ * Existing generic lifecycle owns all allocation and physical settlement. */
63
+ export const distillStageHandlers = Object.freeze(distillStages.map(stage => Object.freeze({ flowId: "mb-distill", stage,
64
+ entryId: "distill-stage-entry", finishId: "distill-stage-finish", entry: startDistillStage, finish: finishDistillStage })));
65
+ export function startDistillStage(context, input) {
66
+ const selected = select(context, input);
67
+ const start = PreparedDescriptorStageStart.prepare(context, input).apply(context);
68
+ const coordinator = PreparedDescriptorStageCoordinator.prepare(context, input).apply(context, writer => {
69
+ if (hybridDigest(readDiagnosticSnapshotCustody(writer, selected.snapshotSelection, input.runId)) !== hybridDigest(selected.snapshot))
70
+ refuse("snapshot_changed");
71
+ });
72
+ if (["extract", "review"].includes(input.stage))
73
+ registerDistillStageGraph(context, input);
74
+ return { ...start, ...coordinator };
75
+ }
76
+ /** Projection fills CLI-owned registers, never new semantic findings. Explicit
77
+ * conflicting proposals refuse instead of being silently promoted. */
78
+ export function prepareDistillStageProjection(bytes, stage, prior, collection) {
79
+ if (!bytes.length || bytes.length > 1024 * 1024)
80
+ refuse("result_exceeds_bound");
81
+ let value;
82
+ try {
83
+ value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
84
+ }
85
+ catch {
86
+ return refuse("invalid_result_json");
87
+ }
88
+ if (!value || typeof value !== "object" || !value.details || typeof value.details !== "object" || Array.isArray(value.details)
89
+ || !Array.isArray(value.details.limitations) || value.details.limitations.some(item => typeof item !== "string"))
90
+ refuse("invalid_result_details");
91
+ const details = value.details;
92
+ const insert = (key, expected) => {
93
+ const proposed = details[key];
94
+ if (proposed !== undefined && (!Array.isArray(proposed) || proposed.length && hybridDigest(proposed) !== hybridDigest(expected)))
95
+ refuse(`changed_${key}`);
96
+ Object.assign(details, { [key]: structuredClone(expected) });
97
+ };
98
+ if (prior)
99
+ insert("coverage", prior.details.coverage);
100
+ if (stage === "preflight") {
101
+ insert("candidates", []);
102
+ insert("decisions", []);
103
+ }
104
+ else if (stage === "extract") {
105
+ if (!collection)
106
+ refuse("author_work_required");
107
+ insert("candidates", collectDistillCandidates(collection).candidates);
108
+ insert("decisions", []);
109
+ }
110
+ else {
111
+ if (!prior)
112
+ refuse("predecessor_required");
113
+ insert("candidates", prior.details.candidates);
114
+ if (stage !== "review")
115
+ insert("decisions", prior.details.decisions);
116
+ }
117
+ if (stage === "finish") {
118
+ if (!prior)
119
+ refuse("verified_predecessor_required");
120
+ insert("verification", prior.details.verification);
121
+ }
122
+ details.limitations = [...new Set([...(prior?.details.limitations ?? []), ...(collection ?? []).flatMap(work => work.result.limitations), ...details.limitations])];
123
+ return Buffer.from(JSON.stringify(value));
124
+ }
125
+ function verificationOwner(context, input, selected, settlement) {
126
+ const receipt = (writer, stage) => readAcceptedDescriptorStageSettlement(writer, { ...selected.scope, stage }, selected.bundle, entityReferenceVariants(input.runId).map(reference => stageLifecycleMatchKey("stage_finish", reference, stage, input.projectRoot)));
127
+ const read = (writer) => {
128
+ const promotion = receipt(writer, "promote"), verification = input.stage === "finish" ? receipt(writer, "verify") : null;
129
+ const physical = (sessionId) => {
130
+ const row = writer.db.get("SELECT harness,provider_session_id FROM sessions WHERE project_id=? AND run_id=? AND session_id=?", [selected.scope.projectId, input.runId, sessionId]);
131
+ if (!row?.harness || !row.provider_session_id)
132
+ refuse("verification_physical_owner_missing");
133
+ return row;
134
+ };
135
+ const verifier = physical(verification?.binding.sessionId ?? settlement.coordinatorSessionId()), author = physical(promotion.binding.sessionId);
136
+ assertDiagnosticSessionIndependence([verifier], [author]);
137
+ return { promotion, verification, verifier, author };
138
+ };
139
+ const original = context.db.readSnapshot(() => read(context));
140
+ return (writer) => { if (hybridDigest(read(writer)) !== hybridDigest(original))
141
+ refuse("verification_owner_changed"); };
142
+ }
143
+ export function finishDistillStage(context, input) {
144
+ const selected = select(context, input), { bundle, snapshot, scope } = selected, stage = input.stage;
145
+ const packet = readDescriptorStageInput(context, scope, bundle, true, true), previousStage = packet.input.predecessor?.stage;
146
+ const previous = previousStage ? readAcceptedStageData(context, { ...scope, stage: previousStage }) : null;
147
+ const prior = previous ? JSON.parse(previous.bytes.toString("utf8")) : null;
148
+ const collection = ["extract", "review"].includes(stage) ? PreparedDistillWorkCollection.prepare(context, input) : null;
149
+ const works = collection?.results() ?? null;
150
+ let bytes = prepareDistillStageProjection(input.bytes, stage, prior, works);
151
+ const binding = { runId: input.runId, stage, inputSha256: packet.inputSha256, snapshotSha256: snapshot.source_sha256,
152
+ snapshotRoot: diagnosticSnapshotPaths(context, snapshot, input.runId).root, runRoot: packet.runRoot,
153
+ support: bundle.manifest.inventory.filter(ref => ref.path.startsWith("support/")),
154
+ predecessors: packet.input.predecessor ? [{ stage: packet.input.predecessor.stage, ...packet.input.predecessor.result }] : [],
155
+ writeScope: prior?.details.write_scope ?? null, promotionReceipt: prior?.details.promotion_receipt ?? null };
156
+ let publication = null;
157
+ if (["promote", "verify", "finish"].includes(stage) && prior?.details.decisions.some(decision => ["adopt", "adapt"].includes(decision.disposition))) {
158
+ const review = readAcceptedStageData(context, { ...scope, stage: "review" });
159
+ const promotion = stage === "promote" ? packet : readDescriptorStageInput(context, { ...scope, stage: "promote" }, bundle, true, true);
160
+ publication = readCompletedDistillPublication(context, { project_id: scope.projectId, run_id: input.runId, attempt: promotion.attempt,
161
+ input_sha256: promotion.inputSha256, bundle_sha256: bundle.sha256, review_sha256: review.result.sha256 });
162
+ if (stage === "promote" && publication.binding.user_inputs_sha256 !== hybridDigest(readDistillUserInputs(context, input, bundle)))
163
+ refuse("user_inputs_changed");
164
+ binding.writeScope = publication.binding.write_scope;
165
+ binding.promotionReceipt = readDistillPublicationReference(context, publication.binding, publication.plan, `${bundle.descriptor.stage("promote").directory}/publication.json`);
166
+ if (!binding.promotionReceipt)
167
+ refuse("publication_reference_required");
168
+ publishDistillBeforeDocuments(packet.runRoot, publication.plan, publication.beforeDocuments);
169
+ publishStagePacket(context, scope.projectId, input.runId, "promote", path.join(packet.runRoot, binding.promotionReceipt.path));
170
+ const value = JSON.parse(bytes.toString("utf8"));
171
+ if (value.details.write_scope != null && hybridDigest(value.details.write_scope) !== hybridDigest(binding.writeScope)
172
+ || value.details.promotion_receipt != null && hybridDigest(value.details.promotion_receipt) !== hybridDigest(binding.promotionReceipt))
173
+ refuse("unowned_publication_claim");
174
+ Object.assign(value.details, { write_scope: binding.writeScope, promotion_receipt: binding.promotionReceipt });
175
+ bytes = Buffer.from(JSON.stringify(value));
176
+ if (stage === "promote") {
177
+ const actual = publication.plan.classify(binding.writeScope.destination_root);
178
+ if (actual.completed !== actual.total)
179
+ refuse("publication_not_observed");
180
+ }
181
+ else
182
+ observeSelectedPublication(publication);
183
+ }
184
+ const result = validateDistillStageResult(bytes, bundle.descriptor.stageBlob(stage, "result_schema"), binding);
185
+ if (publication && ["verify", "finish"].includes(stage)) {
186
+ const artifact = JSON.parse(distillPublicationArtifact(publication.binding, publication.plan).toString("utf8"));
187
+ const evidence = result.details.verification.flatMap(check => check.evidence);
188
+ if (artifact.before_documents.some((document) => !evidence.some(ref => ref.root === "run" && ref.path === document.source.path && ref.sha256 === document.source.sha256)))
189
+ refuse("preservation_evidence_required");
190
+ if (artifact.documents.some((document) => !evidence.some(ref => ref.root === "destination" && ref.path === document.path && ref.sha256 === document.sha256))
191
+ || result.details.verification.some(check => !check.evidence.some(ref => ref.root === "destination" && binding.writeScope.paths.includes(ref.path))))
192
+ refuse("destination_evidence_required");
193
+ }
194
+ if (prior)
195
+ assertDistillStageScope(result, prior);
196
+ if (stage === "review")
197
+ for (const decision of result.details.decisions) {
198
+ if (!works?.some(work => work.result.decisions.some(vote => vote.candidate_id === decision.candidate_id && vote.disposition === decision.disposition)))
199
+ refuse("unsupported_critic_resolution");
200
+ }
201
+ const settlement = PreparedDescriptorStageSettlement.prepare(context, { ...input, bytes });
202
+ const independent = publication && ["verify", "finish"].includes(stage) ? verificationOwner(context, input, selected, settlement) : null;
203
+ const authors = stage === "finish" ? PreparedDistillWorkCollection.prepare(context, { ...input, stage: "extract" }) : null;
204
+ const critics = stage === "finish" ? PreparedDistillWorkCollection.prepare(context, { ...input, stage: "review" }) : null;
205
+ const report = authors && critics ? prepareStageReport({ run_id: input.runId, stage: "finish", verdict: "completed", summary: result.summary,
206
+ distillation: { result, origins: collectDistillCandidates(authors.results()).origins, critics: critics.results() },
207
+ semantic: { acceptance: result.details.decisions.map(decision => `${decision.candidate_id}: ${decision.disposition}; publication=${binding.writeScope?.candidate_ids.includes(decision.candidate_id) ? "observed" : "not_selected"}; ${decision.reason}`),
208
+ checks: result.details.verification.map(check => `${check.candidate_id}: ${check.verdict}; ${check.summary}`), evidence: result.details.verification.flatMap(check => check.evidence.map(ref => `${ref.root}:${ref.path}@${ref.sha256}`)),
209
+ next_action: "Completed only the accepted scope; rejected, duplicate and unselected candidates remain in the register." } }) : null;
210
+ const reportRoot = path.join(packet.runRoot, bundle.descriptor.stage(stage).directory);
211
+ const fence = (writer) => {
212
+ if (hybridDigest(readDiagnosticSnapshotCustody(writer, selected.snapshotSelection, input.runId)) !== hybridDigest(snapshot))
213
+ refuse("snapshot_changed");
214
+ if (previous && hybridDigest(readAcceptedStageData(writer, { ...scope, stage: previousStage })) !== hybridDigest(previous))
215
+ refuse("predecessor_changed");
216
+ collection?.assertCustody(writer);
217
+ authors?.assertCustody(writer);
218
+ critics?.assertCustody(writer);
219
+ independent?.(writer);
220
+ if (publication) {
221
+ const { write_scope, user_inputs_sha256, ...identity } = publication.binding;
222
+ const current = readCompletedDistillPublication(writer, identity);
223
+ if (current.plan.sha256 !== publication.plan.sha256 || hybridDigest(current.binding) !== hybridDigest(publication.binding)
224
+ || stage === "promote" && hybridDigest(readPreparedMemoryFlowUserInputs(writer, scope, "mb-distill", bundle)) !== user_inputs_sha256
225
+ || hybridDigest(write_scope) !== hybridDigest(binding.writeScope)
226
+ || hybridDigest(readDistillPublicationReference(writer, current.binding, current.plan, `${bundle.descriptor.stage("promote").directory}/publication.json`)) !== hybridDigest(binding.promotionReceipt))
227
+ refuse("publication_changed");
228
+ }
229
+ if (report) {
230
+ const rows = writer.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type='flow_run.stage_report' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.stage')='finish' AND json_extract(payload_json,'$.attempt')=? LIMIT 2", [scope.projectId, input.runId, packet.attempt]);
231
+ if (rows.length > 1)
232
+ refuse("ambiguous_terminal_report");
233
+ if (rows.length) {
234
+ const retained = JSON.parse(rows[0].payload_json);
235
+ if (retained.sha256 !== report.sha256 || hybridDigest(retained.bytes) !== hybridDigest(report.bytes)
236
+ || resolveStagePublicationPath(writer, scope.projectId, input.runId, retained.root) !== reportRoot)
237
+ refuse("terminal_report_changed");
238
+ }
239
+ else
240
+ acceptStageReport(writer, { ...scope, root: reportRoot, prepared: report });
241
+ }
242
+ };
243
+ const accepted = settlement.apply(context, fence);
244
+ if (stage !== "finish")
245
+ return accepted;
246
+ const complete = PreparedDescriptorRunSettlement.prepare(context, input).apply(context, fence);
247
+ publishStageReport(context, scope.projectId, input.runId, "finish", reportRoot);
248
+ return { ...accepted, ...complete, reports: ["json", "md", "html"].map(extension => path.join(reportRoot, `stage-report.${extension}`)) };
249
+ }