@deksden-com/dd-flow-cli 0.9.0-beta.151 → 0.9.0-beta.155
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.
- package/CHANGELOG.md +23 -0
- package/dist/build-info.json +8 -8
- package/dist/cli/command-inputs.js +33 -3
- package/dist/cli/command-metadata.js +73 -2
- package/dist/cli/delivery.js +108 -0
- package/dist/cli/diagnostic-invocation.js +23 -7
- package/dist/cli/diagnostic.js +14 -3
- package/dist/cli/help.js +117 -1
- package/dist/cli/hook-ingress.js +36 -49
- package/dist/cli/interactive-invocation.js +33 -0
- package/dist/cli/interactive.js +114 -0
- package/dist/cli/memory-fix-invocation.js +52 -0
- package/dist/cli/memory-fix.js +100 -0
- package/dist/cli/memory-flow-admission.js +7 -3
- package/dist/cli/memory-init.js +5 -4
- package/dist/cli/native-hook-ingress.js +5 -1
- package/dist/cli/native-hook-preparation.js +31 -4
- package/dist/cli/run-cli.js +273 -19
- package/dist/cli.js +17 -3
- package/dist/domain/delivery-flows.js +8 -0
- package/dist/domain/distill-flow.js +20 -0
- package/dist/domain/interactive-flow.js +1 -0
- package/dist/domain/memory-fix-flow.js +2 -0
- package/dist/domain/review-fix-flow.js +2 -0
- package/dist/domain/source-snapshot-flows.js +4 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +18 -3
- package/dist/harness-runtime/bin/dd-grok.mjs +15 -5
- package/dist/harness-runtime/bin/dd-zcode.mjs +15 -7
- package/dist/harness-runtime/lib/agy-error-boundary.mjs +18 -10
- package/dist/harness-runtime/lib/agy-native-inspection.mjs +12 -3
- package/dist/harness-runtime/lib/agy-native-inventory.mjs +19 -0
- package/dist/harness-runtime/lib/agy-terminal-envelope.mjs +13 -6
- package/dist/harness-runtime/lib/agy-transcript-stream.mjs +54 -0
- package/dist/harness-runtime/lib/daemon-observation.d.mts +1 -0
- package/dist/harness-runtime/lib/daemon-observation.mjs +12 -4
- package/dist/harness-runtime/lib/daemon-operations.mjs +20 -5
- package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +371 -114
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +43 -3
- package/dist/harness-runtime/lib/dd-codex.mjs +36 -4
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +25 -11
- package/dist/harness-runtime/lib/dd-droid.mjs +124 -11
- package/dist/harness-runtime/lib/dd-grok-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +137 -25
- package/dist/harness-runtime/lib/dd-grok.mjs +4 -2
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +170 -25
- package/dist/harness-runtime/lib/dd-opencode.mjs +40 -19
- package/dist/harness-runtime/lib/dd-zcode-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +115 -22
- package/dist/harness-runtime/lib/dd-zcode.mjs +75 -36
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +1 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +10 -3
- package/dist/harness-runtime/lib/delivery-command.mjs +84 -0
- package/dist/harness-runtime/lib/driver-recovery.mjs +0 -19
- package/dist/harness-runtime/lib/droid-observation.mjs +9 -5
- package/dist/harness-runtime/lib/lifecycle-contract.d.mts +1 -0
- package/dist/harness-runtime/lib/lifecycle-contract.mjs +2 -1
- package/dist/harness-runtime/lib/native-hook-command.d.mts +3 -2
- package/dist/harness-runtime/lib/native-hook-command.mjs +63 -8
- package/dist/harness-runtime/lib/native-streams.mjs +3 -10
- package/dist/harness-runtime/lib/opencode-plugin.mjs +36 -9
- package/dist/harness-runtime/lib/operation-errors.mjs +13 -0
- package/dist/harness-runtime/lib/operation-progress.mjs +6 -2
- package/dist/harness-runtime/lib/process-json.mjs +4 -5
- package/dist/harness-runtime/lib/tool-observations.mjs +20 -10
- package/dist/harness-runtime/lib/zcode-native-hook-ownership.mjs +109 -0
- package/dist/schemas/code-work-result.schema.json +2 -1
- package/dist/schemas/delivery-stage-result.schema.json +286 -0
- package/dist/schemas/distill-stage-result.schema.json +667 -0
- package/dist/schemas/distill-work-result.schema.json +428 -0
- package/dist/schemas/execution-bundle.schema.json +6 -2
- package/dist/schemas/interactive-stage-result.schema.json +47 -0
- package/dist/schemas/interactive-work-result.schema.json +63 -0
- package/dist/schemas/mb-fix-stage-result.schema.json +93 -0
- package/dist/schemas/project-flow-pack-manifest.schema.json +2 -2
- package/dist/schemas/review-fix-stage-result.schema.json +210 -0
- package/dist/schemas/run-control-receipt.schema.json +1 -0
- package/dist/services/assigned-lifecycle.js +382 -0
- package/dist/services/check-runner-evidence.js +77 -10
- package/dist/services/check-runner-proof.js +69 -1
- package/dist/services/cli-operation-classifier.js +6 -1
- package/dist/services/code-checks.js +56 -23
- package/dist/services/codex-hook-delivery.js +11 -2
- package/dist/services/controller-execution.js +4 -1
- package/dist/services/controller-fanout.js +9 -4
- package/dist/services/controller-memory-worker.js +6 -4
- package/dist/services/controller-result-authority.js +122 -0
- package/dist/services/dashboard.js +4 -2
- package/dist/services/delivery-access.js +123 -0
- package/dist/services/delivery-approval.js +68 -0
- package/dist/services/delivery-artifact.js +86 -0
- package/dist/services/delivery-checks.js +178 -0
- package/dist/services/delivery-execution.js +181 -0
- package/dist/services/delivery-intents.js +212 -0
- package/dist/services/delivery-operation.js +81 -0
- package/dist/services/delivery-policy.js +91 -0
- package/dist/services/delivery-procedures.js +43 -0
- package/dist/services/delivery-request.js +99 -0
- package/dist/services/delivery-result.js +144 -0
- package/dist/services/delivery-stage-commands.js +26 -0
- package/dist/services/delivery-stage-entry.js +56 -0
- package/dist/services/delivery-stage-handlers.js +135 -0
- package/dist/services/delivery-stage-results.js +94 -0
- package/dist/services/descriptor-coordinator-session.js +3 -1
- package/dist/services/descriptor-run-start.js +24 -4
- package/dist/services/descriptor-stage-coordinator.js +78 -10
- package/dist/services/descriptor-stage-settlement.js +59 -14
- package/dist/services/diagnostic-repair-intake.js +123 -0
- package/dist/services/diagnostic-repair-source-intake.js +102 -0
- package/dist/services/diagnostic-report.js +30 -0
- package/dist/services/diagnostic-run-bundle.js +48 -9
- package/dist/services/diagnostic-snapshot.js +2 -2
- package/dist/services/diagnostic-stage-handlers.js +3 -2
- package/dist/services/diagnostic-stage-results.js +26 -0
- package/dist/services/diagnostic-start.js +56 -6
- package/dist/services/diagnostic-work-collection.js +3 -3
- package/dist/services/distill-document-publication.js +280 -0
- package/dist/services/distill-promotion.js +197 -0
- package/dist/services/distill-publication-custody.js +162 -0
- package/dist/services/distill-stage-handlers.js +249 -0
- package/dist/services/distill-stage-results.js +105 -0
- package/dist/services/distill-user-inputs.js +4 -0
- package/dist/services/distill-work-batch.js +42 -0
- package/dist/services/distill-work-collection.js +74 -0
- package/dist/services/distill-work-registration.js +93 -0
- package/dist/services/distill-work-results.js +98 -0
- package/dist/services/eval-snapshots.js +17 -7
- package/dist/services/external-work-launch.js +8 -2
- package/dist/services/flow-catalog.js +24 -0
- package/dist/services/frozen-engine.js +19 -2
- package/dist/services/generic-execution.js +4 -3
- package/dist/services/harness-adapter.js +2 -14
- package/dist/services/hook-memory-routing.js +1 -1
- package/dist/services/hooks.js +31 -6
- package/dist/services/hybrid-contracts.js +6 -1
- package/dist/services/installed-flow-source.js +37 -2
- package/dist/services/interactive-attached-session.js +215 -0
- package/dist/services/interactive-baseline-custody.js +54 -0
- package/dist/services/interactive-before-versions.js +148 -0
- package/dist/services/interactive-bundle-data.js +85 -0
- package/dist/services/interactive-checks.js +179 -0
- package/dist/services/interactive-execution.js +33 -0
- package/dist/services/interactive-finish-integration.js +236 -0
- package/dist/services/interactive-git-baseline.js +230 -0
- package/dist/services/interactive-git-dispatch.js +278 -0
- package/dist/services/interactive-input-delivery.js +82 -0
- package/dist/services/interactive-mutation-scope.js +231 -0
- package/dist/services/interactive-request.js +287 -0
- package/dist/services/interactive-run-bundle.js +109 -0
- package/dist/services/interactive-stage-entry.js +41 -0
- package/dist/services/interactive-stage-handlers.js +289 -0
- package/dist/services/interactive-stage-observation.js +136 -0
- package/dist/services/interactive-stage-results.js +111 -0
- package/dist/services/interactive-user-inputs.js +142 -0
- package/dist/services/interactive-work-batch.js +43 -0
- package/dist/services/interactive-work-collection.js +119 -0
- package/dist/services/interactive-work-completion.js +180 -0
- package/dist/services/interactive-work-context.js +249 -0
- package/dist/services/interactive-work-history.js +112 -0
- package/dist/services/interactive-work-registration.js +124 -0
- package/dist/services/interactive-work-results.js +115 -0
- package/dist/services/interactive-work-start.js +155 -0
- package/dist/services/lifecycle-invocations.js +214 -24
- package/dist/services/managed-daemon-binding.js +52 -1
- package/dist/services/memory-compose-handler.js +2 -22
- package/dist/services/memory-finish-integration.js +58 -17
- package/dist/services/memory-fix-finish-integration.js +129 -0
- package/dist/services/memory-fix-git-dispatch.js +139 -0
- package/dist/services/memory-fix-intake-custody.js +11 -0
- package/dist/services/memory-fix-intake-settlement.js +127 -0
- package/dist/services/memory-fix-publication.js +196 -0
- package/dist/services/memory-fix-repair-admission.js +116 -0
- package/dist/services/memory-fix-request.js +141 -0
- package/dist/services/memory-fix-run-bundle.js +83 -0
- package/dist/services/memory-fix-scope.js +78 -0
- package/dist/services/memory-fix-sources.js +85 -0
- package/dist/services/memory-fix-stage-entry.js +24 -0
- package/dist/services/memory-fix-stage-handlers.js +282 -0
- package/dist/services/memory-fix-stage-results.js +106 -0
- package/dist/services/memory-fix-workspace.js +127 -0
- package/dist/services/memory-flow-user-inputs.js +88 -0
- package/dist/services/memory-init-contexts.js +55 -5
- package/dist/services/memory-init-controller-start.js +20 -4
- package/dist/services/memory-layer.js +7 -3
- package/dist/services/memory-native-review-copy.js +27 -3
- package/dist/services/memory-repair-document-delta.js +1 -0
- package/dist/services/project-flow-source.js +195 -0
- package/dist/services/project-git-integration.js +92 -0
- package/dist/services/repair-intents.js +2 -4
- package/dist/services/retained-native-hook.js +245 -0
- package/dist/services/review-fix-handoff.js +82 -0
- package/dist/services/review-fix-intake-custody.js +17 -0
- package/dist/services/review-fix-stage-entry.js +43 -0
- package/dist/services/review-fix-stage-handlers.js +176 -0
- package/dist/services/review-fix-stage-results.js +86 -0
- package/dist/services/run-control-worker.js +42 -1
- package/dist/services/run-control.js +40 -5
- package/dist/services/run-controller-adapter.js +37 -3
- package/dist/services/run-controller-process.js +12 -4
- package/dist/services/run-controller-recovery.js +46 -6
- package/dist/services/run-controller-state.js +3 -2
- package/dist/services/run-controller.js +209 -43
- package/dist/services/run-recovery.js +31 -2
- package/dist/services/run-timeline.js +59 -0
- package/dist/services/runs.js +18 -31
- package/dist/services/sessions.js +2 -1
- package/dist/services/stage-handlers.js +55 -3
- package/dist/services/stage-lifecycle.js +4 -3
- package/dist/services/stage-pause.js +26 -9
- package/dist/services/stage-report-renderer.js +2 -6
- package/dist/services/technical-notes.js +31 -0
- package/dist/services/verification-matrix.js +1 -1
- package/dist/services/version-status.js +1 -1
- package/dist/services/vnext-code-review.js +74 -5
- package/dist/services/vnext-code.js +9 -32
- package/dist/services/vnext-fanout.js +53 -8
- package/dist/services/vnext-merge.js +10 -5
- package/dist/services/vnext-plan-review.js +1 -1
- package/dist/services/vnext-plan.js +1 -1
- package/dist/services/vnext-protocolize.js +1 -1
- package/dist/services/vnext-specify.js +10 -5
- package/dist/services/work-registry.js +252 -66
- package/dist/services/worktrees.js +7 -4
- package/dist/storage/database.js +2 -1
- package/dist/verification-components.json +82 -0
- package/package.json +4 -3
- package/tools/verification-jobs.mjs +342 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { AppError } from "../shared/errors.js";
|
|
2
|
-
import {
|
|
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) || !
|
|
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) || !
|
|
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
|
-
|
|
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
|
|
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 =>
|
|
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 =>
|
|
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
|
+
}
|
|
@@ -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
|
+
}
|