@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.
- package/CHANGELOG.md +31 -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 +274 -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 +115 -17
- package/dist/harness-runtime/lib/dd-opencode.mjs +10 -7
- 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 +2 -1
- package/dist/harness-runtime/lib/delegation-instructions.mjs +15 -5
- 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 +45 -8
- package/dist/services/cli-operation-classifier.js +6 -1
- package/dist/services/code-checks.js +46 -29
- package/dist/services/codex-hook-delivery.js +11 -2
- package/dist/services/controller-child-repair.js +95 -0
- package/dist/services/controller-execution.js +4 -1
- package/dist/services/controller-fanout.js +19 -9
- 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 +250 -33
- 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 -7
- package/dist/services/retained-native-hook.js +274 -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 +25 -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 +257 -44
- 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 +8 -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 +255 -69
- package/dist/services/worktrees.js +7 -4
- package/dist/storage/database.js +2 -1
- package/dist/verification-components.json +82 -0
- package/package.json +3 -2
- package/tools/verification-jobs.mjs +35 -10
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { AppError } from "../shared/errors.js";
|
|
4
|
+
import { resolveProjectRoot } from "../storage/paths.js";
|
|
5
|
+
import { requireProjectByRoot } from "./projects.js";
|
|
6
|
+
import { readFlowRunExecutionBundle } from "./runs.js";
|
|
7
|
+
import { readDescriptorStageInput } from "./descriptor-stage-coordinator.js";
|
|
8
|
+
import { PreparedDescriptorStageSettlement } from "./descriptor-stage-settlement.js";
|
|
9
|
+
import { readAcceptedStageData, prepareStagePacket, acceptStagePacket, resolveStagePublicationPath } from "./stage-report-renderer.js";
|
|
10
|
+
import { readMemoryFixSourceIntake, assertMemoryFixSourceIntakeCustody } from "./memory-fix-intake-custody.js";
|
|
11
|
+
import { prepareMemoryFixScopeEvidence } from "./memory-fix-scope.js";
|
|
12
|
+
import { validateMemoryFixStageResult } from "./memory-fix-stage-results.js";
|
|
13
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
14
|
+
const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
15
|
+
function refuse(reason) {
|
|
16
|
+
throw new AppError("memory_fix_intake_settlement_invalid", "Repair intake requires accepted scope and original source observations", 2, { reason, effect: "no_effect" });
|
|
17
|
+
}
|
|
18
|
+
/** Historical SQL read only: repaired files and later user turns must not
|
|
19
|
+
* replace an already accepted intake baseline. This grants no new write scope.
|
|
20
|
+
* Reads accepted Stage bytes itself, never caller-provided result JSON. The
|
|
21
|
+
* caller still needs ordinary coordinator settlement for Stage replay. Retained
|
|
22
|
+
* workspace_root is provenance only after restore, never a fresh write target.
|
|
23
|
+
*/
|
|
24
|
+
export function readAcceptedMemoryFixIntakeEvidence(context, scope, file) {
|
|
25
|
+
const accepted = readAcceptedStageData(context, { ...scope, stage: "intake" });
|
|
26
|
+
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')='intake' LIMIT 33", [scope.projectId, scope.runId]);
|
|
27
|
+
if (rows.length > 32)
|
|
28
|
+
refuse("ambiguous_intake_evidence");
|
|
29
|
+
const packets = rows.map(row => {
|
|
30
|
+
if (Buffer.byteLength(row.payload_json) > 8 * 1024 * 1024)
|
|
31
|
+
refuse("intake_evidence_exceeds_bound");
|
|
32
|
+
return JSON.parse(row.payload_json);
|
|
33
|
+
}).filter(packet => packet && typeof packet.file === "string"
|
|
34
|
+
&& resolveStagePublicationPath(context, scope.projectId, scope.runId, packet.file) === file);
|
|
35
|
+
if (packets.length !== 1)
|
|
36
|
+
refuse("intake_evidence_missing");
|
|
37
|
+
const packet = packets[0];
|
|
38
|
+
if (typeof packet.base64 !== "string")
|
|
39
|
+
refuse("intake_evidence_changed");
|
|
40
|
+
const bytes = Buffer.from(packet.base64, "base64");
|
|
41
|
+
if (!bytes.length || bytes.length > 4 * 1024 * 1024 || bytes.toString("base64") !== packet.base64
|
|
42
|
+
|| hash(bytes) !== packet.sha256)
|
|
43
|
+
refuse("intake_evidence_changed");
|
|
44
|
+
let evidence, result;
|
|
45
|
+
try {
|
|
46
|
+
evidence = JSON.parse(bytes.toString("utf8"));
|
|
47
|
+
result = JSON.parse(accepted.bytes.toString("utf8"));
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return refuse("intake_evidence_changed");
|
|
51
|
+
}
|
|
52
|
+
if (!evidence || !result?.details || accepted.data_schema_id !== "dd-flow/mb-fix-stage-result@1"
|
|
53
|
+
|| result.schema_id !== "dd-flow/mb-fix-stage-result@1" || result.run_id !== scope.runId || result.stage !== "intake"
|
|
54
|
+
|| result.input_sha256 !== accepted.input_sha256 || evidence.intake_sha256 !== result.intake_sha256
|
|
55
|
+
|| evidence.source_observation_sha256 !== result.source_observation_sha256
|
|
56
|
+
|| hybridDigest(evidence.source_observation) !== result.source_observation_sha256
|
|
57
|
+
|| hybridDigest(evidence.routing) !== hybridDigest(result.details.routing)
|
|
58
|
+
|| hybridDigest(evidence.write_scope) !== hybridDigest(result.details.write_scope)
|
|
59
|
+
|| evidence.write_scope && evidence.user_inputs_sha256 !== evidence.write_scope.user_inputs_sha256)
|
|
60
|
+
refuse("accepted_intake_changed");
|
|
61
|
+
return structuredClone(evidence);
|
|
62
|
+
}
|
|
63
|
+
/** Internal family handler. Generic runtime owns hook/Session identity and
|
|
64
|
+
* atomic completion; the scope packet is retained in that same SQL writer.
|
|
65
|
+
* Public registration waits for the complete repair/verification family.
|
|
66
|
+
*/
|
|
67
|
+
export function finishMemoryFixIntake(context, input) {
|
|
68
|
+
if (input.stage !== "intake")
|
|
69
|
+
refuse("intake_stage_required");
|
|
70
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
71
|
+
const bundle = readFlowRunExecutionBundle(context, input);
|
|
72
|
+
if (!bundle || bundle.manifest.flow_id !== "mb-fix")
|
|
73
|
+
refuse("memory_fix_bundle_required");
|
|
74
|
+
const scope = { projectId: project.id, runId: input.runId, stage: "intake" };
|
|
75
|
+
const source = readMemoryFixSourceIntake(context, input, bundle);
|
|
76
|
+
const index = context.db.get("SELECT index_json FROM runs WHERE project_id=? AND id=?", [project.id, input.runId]);
|
|
77
|
+
const replay = JSON.parse(index.index_json).stage_runs.some((stage) => stage.stage === "intake" && stage.status === "done");
|
|
78
|
+
const packet = readDescriptorStageInput(context, scope, bundle, true, replay);
|
|
79
|
+
const file = path.join(packet.runRoot, bundle.descriptor.stage("intake").directory, "intake-evidence.json");
|
|
80
|
+
if (replay) {
|
|
81
|
+
const accepted = readAcceptedStageData(context, scope);
|
|
82
|
+
if (!accepted.bytes.equals(Buffer.from(input.bytes)))
|
|
83
|
+
refuse("accepted_result_changed");
|
|
84
|
+
const evidence = readAcceptedMemoryFixIntakeEvidence(context, scope, file);
|
|
85
|
+
if (evidence.intake_sha256 !== source.sha256)
|
|
86
|
+
refuse("source_intake_changed");
|
|
87
|
+
const settled = PreparedDescriptorStageSettlement.prepare(context, input);
|
|
88
|
+
return settled.apply(context, writer => {
|
|
89
|
+
assertMemoryFixSourceIntakeCustody(writer, scope, bundle, source.sha256);
|
|
90
|
+
const current = readAcceptedStageData(writer, scope);
|
|
91
|
+
if (!current.bytes.equals(accepted.bytes)
|
|
92
|
+
|| hybridDigest(readAcceptedMemoryFixIntakeEvidence(writer, scope, file)) !== hybridDigest(evidence))
|
|
93
|
+
refuse("accepted_intake_changed");
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
if (!input.bytes.length || input.bytes.length > 1024 * 1024)
|
|
97
|
+
refuse("result_exceeds_bound");
|
|
98
|
+
let proposed;
|
|
99
|
+
try {
|
|
100
|
+
proposed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(input.bytes));
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return refuse("invalid_result_json");
|
|
104
|
+
}
|
|
105
|
+
if (!proposed?.details || typeof proposed.details !== "object")
|
|
106
|
+
refuse("invalid_result_details");
|
|
107
|
+
const prepared = prepareMemoryFixScopeEvidence(context, { ...input, routing: proposed.details.routing, writeScope: proposed.details.write_scope });
|
|
108
|
+
const evidence = prepared.snapshot();
|
|
109
|
+
validateMemoryFixStageResult(input.bytes, bundle.descriptor.stageBlob("intake", "result_schema"), {
|
|
110
|
+
runId: input.runId, stage: "intake", inputSha256: packet.inputSha256, intakeSha256: source.sha256,
|
|
111
|
+
sourceObservationSha256: evidence.source_observation_sha256, findingIds: source.intake.selected_findings.map(finding => finding.id),
|
|
112
|
+
projectRoot: evidence.workspace_root, runRoot: packet.runRoot, predecessors: [], writeScope: evidence.write_scope, repairReceipt: null
|
|
113
|
+
});
|
|
114
|
+
const evidenceBytes = Buffer.from(JSON.stringify(evidence));
|
|
115
|
+
if (evidenceBytes.length > 4 * 1024 * 1024)
|
|
116
|
+
refuse("intake_evidence_exceeds_bound");
|
|
117
|
+
const retained = prepareStagePacket(file, evidenceBytes);
|
|
118
|
+
const settled = PreparedDescriptorStageSettlement.prepare(context, input);
|
|
119
|
+
prepared.assertSourcesCurrent();
|
|
120
|
+
return settled.apply(context, writer => {
|
|
121
|
+
prepared.assertCustody(writer);
|
|
122
|
+
const current = writer.db.get("SELECT index_json FROM runs WHERE project_id=? AND id=?", [project.id, input.runId]);
|
|
123
|
+
if (JSON.parse(current.index_json).stage_runs.some((stage) => stage.stage === "intake" && stage.status === "done"))
|
|
124
|
+
refuse("intake_already_accepted");
|
|
125
|
+
acceptStagePacket(writer, { ...scope, prepared: retained });
|
|
126
|
+
});
|
|
127
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { AppError } from "../shared/errors.js";
|
|
5
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
6
|
+
import { appendAudit } from "./audit.js";
|
|
7
|
+
import { projectCheckoutRoot } from "../storage/paths.js";
|
|
8
|
+
import { prepareMemoryFixRepairAdmission } from "./memory-fix-repair-admission.js";
|
|
9
|
+
import { RetainedMemoryRepairPublicationPlan } from "./memory-repair-publication-plan.js";
|
|
10
|
+
import { prepareMemoryFixDocumentPlan, applyMemoryFixDocumentStep, captureDistillBeforeDocuments, validateDistillBeforeDocuments, publishMemoryFixBeforeDocuments, memoryFixBeforeDocumentReferences } from "./distill-document-publication.js";
|
|
11
|
+
import { readFlowRunExecutionBundle } from "./runs.js";
|
|
12
|
+
import { prepareStagePacket, acceptStagePacket, resolveStagePublicationPath } from "./stage-report-renderer.js";
|
|
13
|
+
const intent = "memory_fix.publication_intent", complete = "memory_fix.publication_complete";
|
|
14
|
+
const key = (scope) => `memory_fix_publication:${hybridDigest({ project_id: scope.project_id, run_id: scope.run_id })}`;
|
|
15
|
+
function refuse(reason) {
|
|
16
|
+
throw new AppError("memory_fix_publication_invalid", "Repair publication requires its original admitted output and retained plan", 2, { reason, effect: "no_effect" });
|
|
17
|
+
}
|
|
18
|
+
const receipt = (binding, plan, completed = false) => ({
|
|
19
|
+
schema_id: completed ? "dd-flow/mb-fix-publication-complete@1" : "dd-flow/mb-fix-publication-intent@1",
|
|
20
|
+
run_id: binding.run_id, binding_sha256: hybridDigest(binding), plan_sha256: plan.sha256
|
|
21
|
+
});
|
|
22
|
+
/** SQL custody only, never current write admission or proof of independent
|
|
23
|
+
* verification. Consumers must match this binding to their own frozen RUN.
|
|
24
|
+
* Missing either the retained value or independent receipt is not a new intent.
|
|
25
|
+
*/
|
|
26
|
+
export function readMemoryFixPublication(context, scope) {
|
|
27
|
+
return context.db.readSnapshot(() => {
|
|
28
|
+
const row = context.db.get("SELECT value_json FROM runtime_config WHERE key=?", [key(scope)]);
|
|
29
|
+
const anchor = context.db.get("SELECT json_extract(index_json,'$.variables.\"memory_fix.publication\"') anchor FROM runs WHERE project_id=? AND id=?", [scope.project_id, scope.run_id])?.anchor;
|
|
30
|
+
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", [scope.project_id, type, scope.run_id]);
|
|
31
|
+
const starts = events(intent), ends = events(complete);
|
|
32
|
+
if (!row && !starts.length && !ends.length && !anchor)
|
|
33
|
+
return null;
|
|
34
|
+
if (!row || !anchor || starts.length !== 1 || ends.length > 1 || Buffer.byteLength(row.value_json) > 180 * 1024 * 1024
|
|
35
|
+
|| [...starts, ...ends].some(event => Buffer.byteLength(event.payload_json) > 32768))
|
|
36
|
+
refuse("publication_custody_missing");
|
|
37
|
+
try {
|
|
38
|
+
const value = JSON.parse(row.value_json), binding = value.binding;
|
|
39
|
+
if (Object.keys(value).sort().join() !== "before_documents,binding,plan_json,plan_sha256" || !binding
|
|
40
|
+
|| binding.project_id !== scope.project_id || binding.run_id !== scope.run_id || typeof value.plan_json !== "string")
|
|
41
|
+
refuse("publication_binding_changed");
|
|
42
|
+
const plan = RetainedMemoryRepairPublicationPlan.restore(Buffer.from(value.plan_json), value.plan_sha256);
|
|
43
|
+
const beforeDocuments = validateDistillBeforeDocuments(plan, value.before_documents);
|
|
44
|
+
const paths = JSON.parse(plan.state(0).bytes().toString("utf8")).write_paths;
|
|
45
|
+
if (hybridDigest([...paths].sort()) !== hybridDigest([...binding.write_scope.paths].sort())
|
|
46
|
+
|| anchor !== hybridDigest(receipt(binding, plan))
|
|
47
|
+
|| hybridDigest(JSON.parse(starts[0].payload_json)) !== hybridDigest(receipt(binding, plan))
|
|
48
|
+
|| ends.length && hybridDigest(JSON.parse(ends[0].payload_json)) !== hybridDigest(receipt(binding, plan, true)))
|
|
49
|
+
refuse("publication_receipt_changed");
|
|
50
|
+
return { binding: structuredClone(binding), plan, beforeDocuments, completed: ends.length === 1 };
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (error instanceof AppError)
|
|
54
|
+
throw error;
|
|
55
|
+
return refuse("invalid_publication_custody");
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
/** Exact handoff bytes include removed paths and original retained text refs.
|
|
60
|
+
* Serialization alone is not publication, Stage acceptance or finding closure. */
|
|
61
|
+
export function memoryFixPublicationArtifact(binding, plan) {
|
|
62
|
+
const entries = new Map(plan.state(plan.actions().length).entries().map(entry => [entry.path, entry]));
|
|
63
|
+
return Buffer.from(JSON.stringify({ schema_id: "dd-flow/mb-fix-publication@1", binding, plan_sha256: plan.sha256,
|
|
64
|
+
documents: binding.write_scope.paths.map(logical => ({ path: logical, after: entries.get(logical) ?? null })),
|
|
65
|
+
before_documents: memoryFixBeforeDocumentReferences(plan), receipt: receipt(binding, plan, true) }));
|
|
66
|
+
}
|
|
67
|
+
export function readMemoryFixPublicationReference(context, retained, logical) {
|
|
68
|
+
if (!retained.completed)
|
|
69
|
+
refuse("publication_not_complete");
|
|
70
|
+
if (!logical || path.isAbsolute(logical) || logical.includes("\\") || logical.split("/").some(part => !part || part === "." || part === ".."))
|
|
71
|
+
refuse("invalid_publication_locator");
|
|
72
|
+
const { binding, plan } = retained;
|
|
73
|
+
const run = context.db.get("SELECT run_root FROM runs WHERE project_id=? AND id=?", [binding.project_id, binding.run_id]);
|
|
74
|
+
if (!run?.run_root)
|
|
75
|
+
refuse("run_home_required");
|
|
76
|
+
const file = path.join(run.run_root, logical), bytes = memoryFixPublicationArtifact(binding, plan);
|
|
77
|
+
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')='repair' AND json_extract(payload_json,'$.attempt')=? LIMIT 33", [binding.project_id, binding.run_id, binding.attempt]);
|
|
78
|
+
if (rows.length > 32 || rows.some(row => Buffer.byteLength(row.payload_json) > 8 * 1024 * 1024))
|
|
79
|
+
refuse("unbounded_publication_packets");
|
|
80
|
+
const packets = rows.map(row => JSON.parse(row.payload_json)).filter(packet => typeof packet.file === "string"
|
|
81
|
+
&& resolveStagePublicationPath(context, binding.project_id, binding.run_id, packet.file) === file);
|
|
82
|
+
if (!packets.length)
|
|
83
|
+
return null;
|
|
84
|
+
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
85
|
+
if (packets.some(packet => packet.sha256 !== sha256 || packet.base64 !== bytes.toString("base64")))
|
|
86
|
+
refuse("publication_packet_changed");
|
|
87
|
+
return { root: "run", path: logical, sha256 };
|
|
88
|
+
}
|
|
89
|
+
/** Internal publisher: actual active author + accepted intake, SQL intent before
|
|
90
|
+
* effects, exact retained prefix and effect-aware retry. No public registration,
|
|
91
|
+
* new Session allocation, independent verification or finding closure here. */
|
|
92
|
+
export function prepareMemoryFixPublication(context, input) {
|
|
93
|
+
const admission = prepareMemoryFixRepairAdmission(context, input), selected = admission.snapshot();
|
|
94
|
+
const proposal = structuredClone(input.proposal), scope = selected.intake.write_scope;
|
|
95
|
+
if (!proposal || Object.keys(proposal).join() !== "outputs" || !Array.isArray(proposal.outputs) || !proposal.outputs.length || proposal.outputs.length > 512
|
|
96
|
+
|| proposal.outputs.some(output => !output || Object.keys(output).sort().join() !== "content,finding_ids,path"
|
|
97
|
+
|| typeof output.path !== "string" || output.content !== null && typeof output.content !== "string"
|
|
98
|
+
|| !Array.isArray(output.finding_ids) || !output.finding_ids.length || new Set(output.finding_ids).size !== output.finding_ids.length
|
|
99
|
+
|| output.finding_ids.some(id => !scope.finding_ids.includes(id) || !selected.intake.routing.some(decision => decision.finding_id === id && decision.route === "memory_repair" && decision.write_paths.includes(output.path))))
|
|
100
|
+
|| hybridDigest(proposal.outputs.map(output => output.path).sort()) !== hybridDigest([...scope.paths].sort())
|
|
101
|
+
|| scope.finding_ids.some(id => !proposal.outputs.some(output => output.finding_ids.includes(id))))
|
|
102
|
+
refuse("output_scope_changed");
|
|
103
|
+
if (proposal.outputs.some(output => output.content !== null && Buffer.byteLength(output.content) > 16 * 1024 * 1024)
|
|
104
|
+
|| proposal.outputs.reduce((total, output) => total + (output.content === null ? 0 : Buffer.byteLength(output.content)), 0) > 64 * 1024 * 1024)
|
|
105
|
+
refuse("unbounded_output");
|
|
106
|
+
const { intake, run_root: runRoot, ...identity } = selected;
|
|
107
|
+
const checkoutRelative = path.relative(projectCheckoutRoot(context.ddFlowHome, selected.project_id), selected.workspace_root);
|
|
108
|
+
const managedCheckout = !!checkoutRelative && !path.isAbsolute(checkoutRelative)
|
|
109
|
+
&& checkoutRelative !== ".." && !checkoutRelative.startsWith(`..${path.sep}`);
|
|
110
|
+
// The bound project checkout is productive workspace, even when its owner
|
|
111
|
+
// stores it in Home. Other Home areas and every RUN root remain protected.
|
|
112
|
+
for (const protectedRoot of [...(managedCheckout ? [] : [context.ddFlowHome]), runRoot]) {
|
|
113
|
+
const root = fs.realpathSync(protectedRoot);
|
|
114
|
+
for (const output of proposal.outputs) {
|
|
115
|
+
const destination = path.join(selected.workspace_root, output.path), relative = path.relative(root, destination);
|
|
116
|
+
if (!relative || !path.isAbsolute(relative) && relative !== ".." && !relative.startsWith(`..${path.sep}`))
|
|
117
|
+
refuse("output_overlaps_runtime");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const binding = { ...identity, intake_sha256: intake.intake_sha256, source_observation_sha256: intake.source_observation_sha256,
|
|
121
|
+
user_inputs_sha256: intake.user_inputs_sha256, write_scope: scope, proposal_sha256: hybridDigest(proposal),
|
|
122
|
+
assignments: proposal.outputs.map(output => ({ path: output.path, finding_ids: [...output.finding_ids] })) };
|
|
123
|
+
const selection = { project_id: binding.project_id, run_id: binding.run_id };
|
|
124
|
+
const prior = readMemoryFixPublication(context, selection);
|
|
125
|
+
if (prior && hybridDigest(prior.binding) !== hybridDigest(binding))
|
|
126
|
+
refuse("original_publication_required");
|
|
127
|
+
if (!prior)
|
|
128
|
+
admission.assertOriginalSources();
|
|
129
|
+
const plan = prior?.plan ?? prepareMemoryFixDocumentPlan(selected.workspace_root, proposal.outputs.map(output => ({ path: output.path, bytes: output.content === null ? null : Buffer.from(output.content) })));
|
|
130
|
+
const beforeDocuments = prior?.beforeDocuments ?? captureDistillBeforeDocuments(selected.workspace_root, plan);
|
|
131
|
+
const bundle = readFlowRunExecutionBundle(context, input);
|
|
132
|
+
if (!bundle || bundle.sha256 !== selected.bundle_sha256)
|
|
133
|
+
refuse("bundle_changed");
|
|
134
|
+
const logical = `${bundle.descriptor.stage("repair").directory}/publication.json`, file = path.join(runRoot, logical);
|
|
135
|
+
const assertIntent = () => {
|
|
136
|
+
const current = readMemoryFixPublication(context, selection);
|
|
137
|
+
if (!current || hybridDigest(current.binding) !== hybridDigest(binding) || current.plan.sha256 !== plan.sha256)
|
|
138
|
+
refuse("publication_intent_changed");
|
|
139
|
+
return current;
|
|
140
|
+
};
|
|
141
|
+
const fence = () => { admission.assertCustody(context); admission.assertPlanSources(plan); assertIntent(); };
|
|
142
|
+
const applyStep = () => {
|
|
143
|
+
admission.assertPhysical();
|
|
144
|
+
admission.assertPlanSources(plan);
|
|
145
|
+
context.db.writeTransaction(() => {
|
|
146
|
+
admission.assertCustody(context);
|
|
147
|
+
const current = readMemoryFixPublication(context, selection);
|
|
148
|
+
if (current) {
|
|
149
|
+
assertIntent();
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (prior)
|
|
153
|
+
refuse("publication_custody_missing");
|
|
154
|
+
const value = JSON.stringify({ binding, plan_json: plan.bytes().toString("utf8"), plan_sha256: plan.sha256, before_documents: beforeDocuments });
|
|
155
|
+
if (Buffer.byteLength(value) > 180 * 1024 * 1024)
|
|
156
|
+
refuse("unbounded_publication_custody");
|
|
157
|
+
context.db.run("INSERT INTO runtime_config(key,value_json,created_at,updated_at) VALUES (?,?,?,?)", [key(selection), value, context.now(), context.now()]);
|
|
158
|
+
appendAudit(context, { projectId: binding.project_id, eventType: intent, payload: receipt(binding, plan) });
|
|
159
|
+
context.db.run("UPDATE runs SET index_json=json_set(index_json,'$.variables.\"memory_fix.publication\"',?) WHERE project_id=? AND id=?", [hybridDigest(receipt(binding, plan)), binding.project_id, binding.run_id]);
|
|
160
|
+
assertIntent();
|
|
161
|
+
});
|
|
162
|
+
publishMemoryFixBeforeDocuments(runRoot, plan, beforeDocuments);
|
|
163
|
+
const result = applyMemoryFixDocumentStep(selected.workspace_root, plan, fence);
|
|
164
|
+
if (result.status !== "applied")
|
|
165
|
+
return result;
|
|
166
|
+
try {
|
|
167
|
+
fence();
|
|
168
|
+
const packet = prepareStagePacket(file, memoryFixPublicationArtifact(binding, plan));
|
|
169
|
+
context.db.writeTransaction(() => {
|
|
170
|
+
admission.assertCustody(context);
|
|
171
|
+
const current = assertIntent();
|
|
172
|
+
if (!current.completed)
|
|
173
|
+
appendAudit(context, { projectId: binding.project_id, eventType: complete, payload: receipt(binding, plan, true) });
|
|
174
|
+
const completed = assertIntent();
|
|
175
|
+
if (!readMemoryFixPublicationReference(context, completed, logical))
|
|
176
|
+
acceptStagePacket(context, { projectId: binding.project_id, runId: binding.run_id, stage: "repair", prepared: packet });
|
|
177
|
+
});
|
|
178
|
+
fence();
|
|
179
|
+
const reference = readMemoryFixPublicationReference(context, assertIntent(), logical);
|
|
180
|
+
if (!reference)
|
|
181
|
+
refuse("publication_reference_missing");
|
|
182
|
+
return { ...result, publication: reference };
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
throw new AppError("memory_fix_publication_pending", "Reconcile this retained publication before claiming repair completion", 1, { effect: "publication_pending", plan_sha256: plan.sha256, cause: error instanceof Error ? error.message : String(error) });
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
return { binding: () => structuredClone(binding), applyStep, apply: () => {
|
|
189
|
+
for (let step = 0; step <= plan.actions().length; step++) {
|
|
190
|
+
const result = applyStep();
|
|
191
|
+
if (result.status === "applied")
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
return refuse("publication_did_not_progress");
|
|
195
|
+
} };
|
|
196
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { AppError } from "../shared/errors.js";
|
|
4
|
+
import { entityReferenceVariants } from "../shared/entity-references.js";
|
|
5
|
+
import { resolveProjectRoot } from "../storage/paths.js";
|
|
6
|
+
import { requireProjectByRoot } from "./projects.js";
|
|
7
|
+
import { readFlowRunExecutionBundle, assertPreparedRunBundleSqlAuthority } from "./runs.js";
|
|
8
|
+
import { readDescriptorStageInput } from "./descriptor-stage-coordinator.js";
|
|
9
|
+
import { readAcceptedDescriptorStageSettlement, readActiveDescriptorStageCoordinator } from "./descriptor-stage-settlement.js";
|
|
10
|
+
import { readAcceptedStageData } from "./stage-report-renderer.js";
|
|
11
|
+
import { stageLifecycleMatchKey } from "./hooks.js";
|
|
12
|
+
import { readMemoryFixSourceIntake, assertMemoryFixSourceIntakeCustody } from "./memory-fix-intake-custody.js";
|
|
13
|
+
import { readAcceptedMemoryFixIntakeEvidence } from "./memory-fix-intake-settlement.js";
|
|
14
|
+
import { readPreparedMemoryFlowUserInputs } from "./memory-flow-user-inputs.js";
|
|
15
|
+
import { observeRetainedMemoryFixSources, observeMemoryFixWriteTargets, memoryFixWriteTargetsFromBaseline } from "./memory-fix-sources.js";
|
|
16
|
+
import { RetainedMemoryRepairPublicationPlan } from "./memory-repair-publication-plan.js";
|
|
17
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
18
|
+
function refuse(reason) {
|
|
19
|
+
throw new AppError("memory_fix_repair_admission_invalid", "Repair requires its accepted intake and current delivered scope", 2, { reason, effect: "no_effect" });
|
|
20
|
+
}
|
|
21
|
+
/** Intake/Stage/user and active author Work custody, not publication permission.
|
|
22
|
+
* The publisher must also admit the author's exact output and retain its original
|
|
23
|
+
* plan before effects. Fresh plans require assertOriginalSources; resumed plans
|
|
24
|
+
* require assertPlanSources on that same SQL-retained plan, never a new baseline.
|
|
25
|
+
*/
|
|
26
|
+
export function prepareMemoryFixRepairAdmission(context, input) {
|
|
27
|
+
if (context.db.isOwnedWriteTransaction?.())
|
|
28
|
+
refuse("preparation_outside_writer_required");
|
|
29
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
30
|
+
const bundle = readFlowRunExecutionBundle(context, input);
|
|
31
|
+
if (!bundle || bundle.manifest.flow_id !== "mb-fix")
|
|
32
|
+
refuse("memory_fix_bundle_required");
|
|
33
|
+
const scope = { projectId: project.id, runId: input.runId, stage: "repair" };
|
|
34
|
+
const source = readMemoryFixSourceIntake(context, input, bundle);
|
|
35
|
+
const run = assertPreparedRunBundleSqlAuthority(context, scope, bundle);
|
|
36
|
+
const stage = readDescriptorStageInput(context, scope, bundle);
|
|
37
|
+
const accepted = readAcceptedStageData(context, { ...scope, stage: "intake" });
|
|
38
|
+
if (stage.input.predecessor?.stage !== "intake" || hybridDigest(stage.input.predecessor.result) !== hybridDigest(accepted.result))
|
|
39
|
+
refuse("accepted_intake_predecessor_required");
|
|
40
|
+
const keys = entityReferenceVariants(input.runId).map(reference => stageLifecycleMatchKey("stage_finish", reference, "intake", input.projectRoot));
|
|
41
|
+
const settlement = readAcceptedDescriptorStageSettlement(context, { ...scope, stage: "intake" }, bundle, keys);
|
|
42
|
+
const author = readActiveDescriptorStageCoordinator(context, scope, bundle);
|
|
43
|
+
const file = path.join(stage.runRoot, bundle.descriptor.stage("intake").directory, "intake-evidence.json");
|
|
44
|
+
const evidence = readAcceptedMemoryFixIntakeEvidence(context, scope, file);
|
|
45
|
+
if (evidence.intake_sha256 !== source.sha256 || !evidence.write_scope)
|
|
46
|
+
refuse("accepted_repair_scope_required");
|
|
47
|
+
const workspace = resolveProjectRoot(run.workspace_root);
|
|
48
|
+
const physical = () => {
|
|
49
|
+
if (fs.realpathSync(workspace) !== workspace)
|
|
50
|
+
refuse("workspace_identity_changed");
|
|
51
|
+
const stat = fs.lstatSync(workspace);
|
|
52
|
+
if (!stat.isDirectory())
|
|
53
|
+
refuse("workspace_identity_changed");
|
|
54
|
+
return { dev: stat.dev, ino: stat.ino };
|
|
55
|
+
};
|
|
56
|
+
const identity = physical();
|
|
57
|
+
const assertPhysical = () => {
|
|
58
|
+
if (context.db.isOwnedWriteTransaction?.())
|
|
59
|
+
refuse("physical_check_outside_writer_required");
|
|
60
|
+
if (hybridDigest(physical()) !== hybridDigest(identity))
|
|
61
|
+
refuse("workspace_identity_changed");
|
|
62
|
+
};
|
|
63
|
+
const assertCustody = (writer) => {
|
|
64
|
+
assertMemoryFixSourceIntakeCustody(writer, scope, bundle, source.sha256);
|
|
65
|
+
const current = assertPreparedRunBundleSqlAuthority(writer, scope, bundle);
|
|
66
|
+
if (current.workspace_root !== workspace || current.run_root !== stage.runRoot
|
|
67
|
+
|| hybridDigest(readDescriptorStageInput(writer, scope, bundle)) !== hybridDigest(stage)
|
|
68
|
+
|| hybridDigest(readAcceptedStageData(writer, { ...scope, stage: "intake" })) !== hybridDigest(accepted)
|
|
69
|
+
|| hybridDigest(readAcceptedDescriptorStageSettlement(writer, { ...scope, stage: "intake" }, bundle, keys)) !== hybridDigest(settlement)
|
|
70
|
+
|| hybridDigest(readActiveDescriptorStageCoordinator(writer, scope, bundle)) !== hybridDigest(author)
|
|
71
|
+
|| hybridDigest(readAcceptedMemoryFixIntakeEvidence(writer, scope, file)) !== hybridDigest(evidence)
|
|
72
|
+
|| hybridDigest(readPreparedMemoryFlowUserInputs(writer, scope, "mb-fix", bundle)) !== evidence.user_inputs_sha256)
|
|
73
|
+
refuse("accepted_scope_changed");
|
|
74
|
+
};
|
|
75
|
+
assertCustody(context);
|
|
76
|
+
const observe = () => { assertPhysical(); return observeRetainedMemoryFixSources(workspace, source.intake).snapshot(); };
|
|
77
|
+
return {
|
|
78
|
+
snapshot: () => structuredClone({ project_id: project.id, run_id: input.runId, workspace_root: workspace,
|
|
79
|
+
workspace_identity: identity, run_root: stage.runRoot, intake: evidence,
|
|
80
|
+
intake_result: accepted.result, bundle_sha256: bundle.sha256, author, attempt: stage.attempt, input_sha256: stage.inputSha256 }),
|
|
81
|
+
assertCustody, assertPhysical,
|
|
82
|
+
assertOriginalSources: () => {
|
|
83
|
+
const current = { ...observe(), write_targets: observeMemoryFixWriteTargets(workspace, evidence.write_scope.paths) };
|
|
84
|
+
if (hybridDigest(current) !== hybridDigest(evidence.source_observation))
|
|
85
|
+
refuse("sources_changed_since_intake");
|
|
86
|
+
},
|
|
87
|
+
assertPlanSources: (plan) => {
|
|
88
|
+
assertPhysical();
|
|
89
|
+
if (!(plan instanceof RetainedMemoryRepairPublicationPlan))
|
|
90
|
+
refuse("retained_plan_required");
|
|
91
|
+
const before = JSON.parse(plan.state(0).bytes().toString("utf8"));
|
|
92
|
+
if (hybridDigest([...before.write_paths].sort()) !== hybridDigest([...evidence.write_scope.paths].sort())
|
|
93
|
+
|| plan.state(0).canonicalWritePaths().size)
|
|
94
|
+
refuse("plan_scope_changed");
|
|
95
|
+
if (hybridDigest(memoryFixWriteTargetsFromBaseline(plan.state(0))) !== hybridDigest(evidence.source_observation.write_targets))
|
|
96
|
+
refuse("write_targets_changed_since_intake");
|
|
97
|
+
const current = plan.classify(workspace), entries = plan.state(current.completed).entries();
|
|
98
|
+
const actual = observe();
|
|
99
|
+
for (const original of evidence.source_observation.observations) {
|
|
100
|
+
const observed = actual.observations.find(item => item.path === original.path);
|
|
101
|
+
if (!observed)
|
|
102
|
+
refuse("source_inventory_changed");
|
|
103
|
+
if (before.write_paths.includes(original.path)) {
|
|
104
|
+
const baseline = before.entries.find((entry) => entry.path === original.path);
|
|
105
|
+
if ((baseline?.sha256 ?? null) !== original.current_sha256)
|
|
106
|
+
refuse("plan_rebased_after_intake");
|
|
107
|
+
const expected = entries.find(entry => entry.path === original.path);
|
|
108
|
+
if ((expected?.sha256 ?? null) !== observed.current_sha256)
|
|
109
|
+
refuse("source_differs_from_retained_prefix");
|
|
110
|
+
}
|
|
111
|
+
else if (hybridDigest(observed) !== hybridDigest(original))
|
|
112
|
+
refuse("sources_changed_since_intake");
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { AppError } from "../shared/errors.js";
|
|
2
|
+
import { requireProjectByRoot } from "./projects.js";
|
|
3
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
4
|
+
import { appendAudit } from "./audit.js";
|
|
5
|
+
import { prepareMemoryFixRunBundle } from "./memory-fix-run-bundle.js";
|
|
6
|
+
import { PreparedInstalledFlowSource } from "./installed-flow-source.js";
|
|
7
|
+
import { FrozenGenericExecution } from "./generic-execution.js";
|
|
8
|
+
import { AdmittedFrozenEngine } from "./frozen-engine.js";
|
|
9
|
+
import { PreparedDescriptorRunStart } from "./descriptor-run-start.js";
|
|
10
|
+
import { readRunStartRequest } from "./run-start-custody.js";
|
|
11
|
+
import { prepareMemoryFixGitPolicy } from "./memory-fix-finish-integration.js";
|
|
12
|
+
const event = "memory_fix.request";
|
|
13
|
+
function refuse(reason) {
|
|
14
|
+
throw new AppError("memory_fix_request_invalid", "Repair launch requires its original retained request", 2, { reason, effect: "no_effect" });
|
|
15
|
+
}
|
|
16
|
+
function selection(input) {
|
|
17
|
+
return hybridDigest({ source_sha256: input.source.sha256, execution_sha256: input.execution.sha256,
|
|
18
|
+
engine_manifest_sha256: input.engine.manifestSha256, request_text: input.requestText,
|
|
19
|
+
source_run_id: input.sourceRunId, finding_ids: input.findingIds, git_executable: input.gitExecutable, git_policy: input.gitPolicy,
|
|
20
|
+
receipts: [...input.receipts ?? []], receipt_bytes: [...input.receiptBytes ?? []].map(([key, bytes]) => [key, Buffer.from(bytes).toString("base64")]) });
|
|
21
|
+
}
|
|
22
|
+
/** SQL-only lookup, also used by the allocation writer. No neighboring audit,
|
|
23
|
+
* current canon/profile settings or filesystem observation is consulted. */
|
|
24
|
+
export function readMemoryFixRequest(context, { projectRoot, requestId }) {
|
|
25
|
+
const scope = { projectRoot, requestId };
|
|
26
|
+
const project = context.db.get("SELECT id FROM projects WHERE root=? AND status='active'", [scope.projectRoot]);
|
|
27
|
+
if (!project)
|
|
28
|
+
refuse("project_not_registered");
|
|
29
|
+
const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type=? AND json_extract(payload_json,'$.scope.requestId')=? LIMIT 2", [project.id, event, scope.requestId]);
|
|
30
|
+
if (!rows.length)
|
|
31
|
+
return null;
|
|
32
|
+
if (rows.length !== 1 || Buffer.byteLength(rows[0].payload_json) > 48 * 1024 * 1024)
|
|
33
|
+
refuse("ambiguous_request");
|
|
34
|
+
let value;
|
|
35
|
+
try {
|
|
36
|
+
value = JSON.parse(rows[0].payload_json);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return refuse("invalid_request");
|
|
40
|
+
}
|
|
41
|
+
if (value.schema_id !== "dd-flow/memory-fix-request@1" || value.project_id !== project.id
|
|
42
|
+
|| hybridDigest(value.scope) !== hybridDigest(scope) || !/^[a-f0-9]{64}$/.test(value.selection_sha256)
|
|
43
|
+
|| !Number.isFinite(Date.parse(value.bound_at)) || typeof value.request_text !== "string" || !value.request_text.trim()
|
|
44
|
+
|| Buffer.byteLength(value.request_text) > 64 * 1024 || !Array.isArray(value.artifacts) || value.artifacts.length !== 2
|
|
45
|
+
|| new Set(value.artifacts.map(item => item.path)).size !== 2
|
|
46
|
+
|| value.artifacts.some(item => !["runtime/repair-intake.json", "runtime/git-policy.json"].includes(item.path)
|
|
47
|
+
|| typeof item.base64 !== "string" || !item.base64 || item.base64.length > 2 * 1024 * 1024
|
|
48
|
+
|| Buffer.from(item.base64, "base64").toString("base64") !== item.base64)
|
|
49
|
+
|| !Array.isArray(value.receipts) || !Array.isArray(value.receipt_bytes)
|
|
50
|
+
|| value.launch_selection !== null && (!value.launch_selection || typeof value.launch_selection !== "object" || Array.isArray(value.launch_selection))
|
|
51
|
+
|| value.execution?.scope?.projectRoot !== scope.projectRoot || value.execution.scope.requestId !== scope.requestId
|
|
52
|
+
|| value.execution.settings.flow_id !== "mb-fix")
|
|
53
|
+
refuse("request_scope_changed");
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
export function readMemoryFixRequestArtifacts(context, input) {
|
|
57
|
+
const frozen = input.execution.snapshot(), request = readMemoryFixRequest(context, { projectRoot: frozen.scope.projectRoot, requestId: frozen.scope.requestId });
|
|
58
|
+
if (request && request.selection_sha256 !== selection(input))
|
|
59
|
+
refuse("request_changed");
|
|
60
|
+
return request;
|
|
61
|
+
}
|
|
62
|
+
/** Allocation fence: metadata and retained bytes only. Historical RUN replay
|
|
63
|
+
* owns its bundle independently and does not need the neighboring source RUN. */
|
|
64
|
+
export function assertMemoryFixRequestBundle(context, projectId, bundle) {
|
|
65
|
+
const frozen = bundle.execution.snapshot(), request = readMemoryFixRequest(context, { projectRoot: frozen.scope.projectRoot, requestId: frozen.scope.requestId });
|
|
66
|
+
if (!request || request.project_id !== projectId || bundle.manifest.flow_id !== "mb-fix" || bundle.manifest.bootstrap_receipts.length
|
|
67
|
+
|| bundle.execution.sha256 !== request.execution_sha256 || bundle.manifest.descriptor.sha256 !== request.source.descriptor.sha256
|
|
68
|
+
|| bundle.blob("runtime/source-request.md").toString("utf8") !== request.request_text
|
|
69
|
+
|| bundle.manifest.engine_manifest.sha256 !== request.engine.manifest_sha256
|
|
70
|
+
|| request.artifacts.some(item => bundle.blob(item.path).toString("base64") !== item.base64)
|
|
71
|
+
|| request.source.blobs.some(item => bundle.blob(item.path).toString("base64") !== item.base64))
|
|
72
|
+
refuse("request_bundle_changed");
|
|
73
|
+
}
|
|
74
|
+
/** Preserve prepared input before allocating a RUN. IDs are selected only by
|
|
75
|
+
* common RUN-start, so another request may allocate between retain and start. */
|
|
76
|
+
export function retainMemoryFixRequest(context, input, launchSelection) {
|
|
77
|
+
if (context.db.isOwnedWriteTransaction?.())
|
|
78
|
+
refuse("preparation_outside_writer_required");
|
|
79
|
+
const frozen = input.execution.snapshot(), scope = { projectRoot: frozen.scope.projectRoot, requestId: frozen.scope.requestId };
|
|
80
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_.-]{0,95}$/.test(scope.requestId))
|
|
81
|
+
refuse("invalid_request_id");
|
|
82
|
+
const project = requireProjectByRoot(context, scope.projectRoot), digest = selection(input), prior = readMemoryFixRequest(context, scope);
|
|
83
|
+
const raw = structuredClone(launchSelection ?? null);
|
|
84
|
+
const assertSame = (request) => {
|
|
85
|
+
if (request.selection_sha256 !== digest || launchSelection !== undefined && hybridDigest(request.launch_selection) !== hybridDigest(raw))
|
|
86
|
+
refuse("request_changed");
|
|
87
|
+
};
|
|
88
|
+
if (prior) {
|
|
89
|
+
assertSame(prior);
|
|
90
|
+
return prior;
|
|
91
|
+
}
|
|
92
|
+
if (readRunStartRequest(context, project.id, scope.requestId))
|
|
93
|
+
refuse("foreign_run_request");
|
|
94
|
+
const bundle = prepareMemoryFixRunBundle(context, input), manifest = input.engine.manifest();
|
|
95
|
+
const value = { schema_id: "dd-flow/memory-fix-request@1", project_id: project.id, scope, selection_sha256: digest,
|
|
96
|
+
bound_at: input.boundAt, request_text: input.requestText, source_run_id: input.sourceRunId, finding_ids: [...input.findingIds],
|
|
97
|
+
source: input.source.snapshot(), source_sha256: input.source.sha256, execution: frozen, execution_sha256: input.execution.sha256,
|
|
98
|
+
engine: { identity: { package_name: manifest.package_name, package_version: manifest.package_version, engine_version: manifest.engine_version,
|
|
99
|
+
integrity_checksum: manifest.integrity.checksum, snapshot_root: manifest.snapshot_root }, manifest_sha256: input.engine.manifestSha256 },
|
|
100
|
+
git_executable: input.gitExecutable, git_policy: structuredClone(input.gitPolicy),
|
|
101
|
+
launch_selection: raw,
|
|
102
|
+
artifacts: ["runtime/repair-intake.json", "runtime/git-policy.json"].map(path => ({ path, base64: bundle.blob(path).toString("base64") })),
|
|
103
|
+
receipts: [...input.receipts ?? []], receipt_bytes: [...input.receiptBytes ?? []].map(([key, bytes]) => [key, Buffer.from(bytes).toString("base64")]) };
|
|
104
|
+
if (Buffer.byteLength(JSON.stringify(value)) > 48 * 1024 * 1024)
|
|
105
|
+
refuse("request_exceeds_bound");
|
|
106
|
+
return context.db.writeTransaction(() => {
|
|
107
|
+
const current = readMemoryFixRequest(context, scope);
|
|
108
|
+
if (current) {
|
|
109
|
+
assertSame(current);
|
|
110
|
+
return current;
|
|
111
|
+
}
|
|
112
|
+
if (readRunStartRequest(context, project.id, scope.requestId))
|
|
113
|
+
refuse("foreign_run_request");
|
|
114
|
+
appendAudit(context, { projectId: project.id, eventType: event, payload: value });
|
|
115
|
+
return value;
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
/** Existing allocations resume the common publisher. Before allocation only
|
|
119
|
+
* retained source/execution and input artifacts are restored, never refreshed. */
|
|
120
|
+
export function startMemoryFixRun(context, scope) {
|
|
121
|
+
const request = readMemoryFixRequest(context, scope);
|
|
122
|
+
if (!request)
|
|
123
|
+
refuse("original_request_required");
|
|
124
|
+
const allocation = readRunStartRequest(context, request.project_id, scope.requestId);
|
|
125
|
+
if (allocation) {
|
|
126
|
+
if (allocation.payload.initial_index.flow_kind !== "mb-fix")
|
|
127
|
+
refuse("foreign_run_request");
|
|
128
|
+
return PreparedDescriptorRunStart.restore(context, scope).apply(context);
|
|
129
|
+
}
|
|
130
|
+
const source = PreparedInstalledFlowSource.restoreRetained(request.source, request.source_sha256), receipts = new Map(request.receipts);
|
|
131
|
+
const execution = FrozenGenericExecution.restore(request.execution, request.execution_sha256, source.resolveRegisteredFlow(), request.execution.scope, receipts);
|
|
132
|
+
const engine = AdmittedFrozenEngine.admitSelection(request.engine.identity, request.engine.manifest_sha256);
|
|
133
|
+
const actualGit = prepareMemoryFixGitPolicy(context.env, { scope: { ...scope, workspaceRoot: request.execution.scope.workspace },
|
|
134
|
+
gitExecutable: request.git_executable, selection: request.git_policy });
|
|
135
|
+
if (actualGit.bytes.toString("base64") !== request.artifacts.find(item => item.path === actualGit.path)?.base64)
|
|
136
|
+
refuse("git_selection_changed_before_start");
|
|
137
|
+
const bundle = prepareMemoryFixRunBundle(context, { source, execution, engine, boundAt: request.bound_at, requestText: request.request_text,
|
|
138
|
+
sourceRunId: request.source_run_id, findingIds: request.finding_ids, gitExecutable: request.git_executable, gitPolicy: request.git_policy,
|
|
139
|
+
receipts, receiptBytes: new Map(request.receipt_bytes.map(([key, bytes]) => [key, Buffer.from(bytes, "base64")])) });
|
|
140
|
+
return PreparedDescriptorRunStart.prepare(context, { bundle }).apply(context);
|
|
141
|
+
}
|