@deksden-com/dd-flow-cli 0.9.0-beta.144 → 0.9.0-beta.149
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 +130 -0
- package/README.md +13 -8
- package/dist/build-info.json +7 -7
- package/dist/cli/command-inputs.js +13 -3
- package/dist/cli/command-metadata.js +51 -16
- package/dist/cli/diagnostic-invocation.js +46 -0
- package/dist/cli/diagnostic.js +120 -0
- package/dist/cli/help.js +67 -1
- package/dist/cli/hook-ingress.js +27 -3
- package/dist/cli/memory-flow-admission.js +9 -3
- package/dist/cli/memory-init-invocation.js +10 -3
- package/dist/cli/memory-init.js +77 -68
- package/dist/cli/memory-storage-admission.js +3 -2
- package/dist/cli/memory-storage-qualification.js +105 -0
- package/dist/cli/memory-storage-transition.js +123 -0
- package/dist/cli/memory-upgrade.js +189 -0
- package/dist/cli/native-hook-ingress.js +14 -4
- package/dist/cli/native-hook-preparation.js +55 -0
- package/dist/cli/response-file.js +2 -1
- package/dist/cli/run-cli.js +79 -40
- package/dist/cli/setup.js +2 -1
- package/dist/cli.js +18 -0
- package/dist/domain/diagnostic-aspects.js +103 -0
- package/dist/domain/diagnostic-flows.js +8 -0
- package/dist/domain/stage-handler-identities.js +8 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +3 -3
- package/dist/harness-runtime/bin/dd-grok.mjs +3 -3
- package/dist/harness-runtime/bin/dd-zcode.mjs +5 -5
- package/dist/harness-runtime/lib/agy-error-boundary.mjs +58 -0
- package/dist/harness-runtime/lib/agy-native-inspection.mjs +216 -0
- package/dist/harness-runtime/lib/agy-native_pb.js +117 -0
- package/dist/harness-runtime/lib/daemon-operations.d.mts +1 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +84 -10
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +366 -52
- package/dist/harness-runtime/lib/dd-agy.mjs +17 -3
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +27 -13
- package/dist/harness-runtime/lib/dd-codex.mjs +4 -0
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +4 -3
- package/dist/harness-runtime/lib/dd-droid.mjs +71 -29
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +73 -16
- package/dist/harness-runtime/lib/dd-grok.mjs +6 -3
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +66 -20
- package/dist/harness-runtime/lib/dd-opencode.mjs +30 -3
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +7 -6
- package/dist/harness-runtime/lib/dd-zcode.mjs +44 -14
- package/dist/harness-runtime/lib/delegation-instructions.mjs +1 -1
- package/dist/harness-runtime/lib/driver-recovery.mjs +9 -8
- package/dist/harness-runtime/lib/entity-references.d.mts +2 -0
- package/dist/harness-runtime/lib/entity-references.mjs +9 -0
- package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -1
- package/dist/harness-runtime/lib/managed-daemon.mjs +24 -9
- package/dist/harness-runtime/lib/native-children.d.mts +8 -1
- package/dist/harness-runtime/lib/native-children.mjs +114 -8
- package/dist/harness-runtime/lib/native-hook-command.mjs +4 -4
- package/dist/harness-runtime/lib/native-request-proof.mjs +1 -1
- package/dist/harness-runtime/lib/observation-clock.d.mts +2 -2
- package/dist/harness-runtime/lib/observation-clock.mjs +7 -4
- package/dist/harness-runtime/lib/opencode-plugin.mjs +3 -3
- package/dist/harness-runtime/lib/operation-errors.mjs +1 -1
- package/dist/harness-runtime/lib/provider-overload-policy.d.mts +5 -3
- package/dist/harness-runtime/lib/provider-overload-policy.mjs +20 -7
- package/dist/harness-runtime/lib/runner-events.mjs +3 -1
- package/dist/harness-runtime/lib/setup-progress.mjs +8 -6
- package/dist/harness-runtime/lib/tool-observations.mjs +1 -1
- package/dist/schemas/diagnostic-stage-result.schema.json +354 -0
- package/dist/schemas/diagnostic-work-result.schema.json +296 -0
- package/dist/schemas/execution-bundle.schema.json +7 -1
- package/dist/schemas/mb-upgrade-stage-result.schema.json +863 -0
- package/dist/schemas/memory-repair-work-result.schema.json +9 -2
- package/dist/schemas/project-flow-pack-manifest.schema.json +1 -1
- package/dist/schemas/release-impact.schema.json +3 -3
- package/dist/schemas/stage-start-response.schema.json +4 -0
- package/dist/services/accepted-intent.js +13 -1
- package/dist/services/canon.js +43 -7
- package/dist/services/check-environment.js +21 -0
- package/dist/services/check-runner-evidence.js +267 -15
- package/dist/services/cli-operation-classifier.js +1 -1
- package/dist/services/cli-upgrade-allowlist.js +1 -1
- package/dist/services/code-checks.js +34 -19
- package/dist/services/compatibility-preflight.js +5 -2
- package/dist/services/controller-execution.js +10 -4
- package/dist/services/controller-fanout.js +34 -10
- package/dist/services/controller-memory-task.js +45 -0
- package/dist/services/controller-memory-worker.js +49 -0
- package/dist/services/dashboard.js +2 -2
- package/dist/services/descriptor-run-settlement.js +5 -3
- package/dist/services/descriptor-run-start.js +46 -20
- package/dist/services/descriptor-stage-coordinator.js +66 -13
- package/dist/services/descriptor-stage-settlement.js +2 -1
- package/dist/services/descriptor-stage-start.js +40 -4
- package/dist/services/diagnostic-evidence.js +73 -0
- package/dist/services/diagnostic-report-publication.js +72 -0
- package/dist/services/diagnostic-report.js +67 -0
- package/dist/services/diagnostic-run-bundle.js +74 -0
- package/dist/services/diagnostic-snapshot.js +159 -0
- package/dist/services/diagnostic-stage-handlers.js +108 -0
- package/dist/services/diagnostic-stage-results.js +132 -0
- package/dist/services/diagnostic-start.js +84 -0
- package/dist/services/diagnostic-work-batch.js +34 -0
- package/dist/services/diagnostic-work-collection.js +97 -0
- package/dist/services/diagnostic-work-registration.js +101 -0
- package/dist/services/diagnostic-work-results.js +93 -0
- package/dist/services/engines.js +64 -14
- package/dist/services/eval-snapshots.js +186 -119
- package/dist/services/execution-bundles.js +19 -7
- package/dist/services/external-work-launch.js +5 -3
- package/dist/services/flow-catalog.js +13 -4
- package/dist/services/frozen-engine.js +148 -11
- package/dist/services/hook-memory-routing.js +188 -0
- package/dist/services/hook-recovery-guard.js +14 -0
- package/dist/services/hook-session-observation.js +24 -0
- package/dist/services/hooks.js +27 -8
- package/dist/services/hybrid-contracts.js +4 -13
- package/dist/services/hybrid-digest.js +14 -0
- package/dist/services/installed-flow-source.js +47 -4
- package/dist/services/lifecycle-invocations.js +179 -38
- package/dist/services/memory-accepted-composition.js +5 -4
- package/dist/services/memory-completed-correction-wave.js +23 -15
- package/dist/services/memory-compose-handler.js +163 -21
- package/dist/services/memory-composition-acceptance.js +186 -10
- package/dist/services/memory-composition-graph.js +42 -11
- package/dist/services/memory-composition-registration.js +23 -8
- package/dist/services/memory-composition-work-custody.js +14 -9
- package/dist/services/memory-composition-work-results.js +15 -3
- package/dist/services/memory-corrected-review-batch.js +10 -3
- package/dist/services/memory-corrected-review-findings.js +4 -2
- package/dist/services/memory-corrected-review-inputs.js +10 -5
- package/dist/services/memory-corrected-review-receipt.js +17 -9
- package/dist/services/memory-corrected-review-registration.js +3 -2
- package/dist/services/memory-correction-check-receipt.js +20 -4
- package/dist/services/memory-custody-snapshot.js +36 -0
- package/dist/services/memory-finish-evidence.js +44 -22
- package/dist/services/memory-finish-handler.js +37 -23
- package/dist/services/memory-finish-integration.js +218 -48
- package/dist/services/memory-finish-readiness.js +66 -19
- package/dist/services/memory-git-integration.js +87 -18
- package/dist/services/memory-historical-corrected-review.js +21 -15
- package/dist/services/memory-historical-original-review.js +22 -9
- package/dist/services/memory-historical-repair-registration.js +14 -8
- package/dist/services/memory-historical-second-review.js +30 -11
- package/dist/services/memory-init-contexts.js +44 -2
- package/dist/services/memory-init-controller-start.js +27 -8
- package/dist/services/memory-init-git-policy.js +50 -21
- package/dist/services/memory-init-lint-capability.js +24 -6
- package/dist/services/memory-init-native-capacity.js +17 -4
- package/dist/services/memory-init-preflight.js +21 -17
- package/dist/services/memory-init-request-candidate.js +73 -17
- package/dist/services/memory-init-request-input.js +23 -7
- package/dist/services/memory-init-run-bundle.js +49 -12
- package/dist/services/memory-init-storage.js +17 -7
- package/dist/services/memory-init-worktrunk-capability.js +3 -7
- package/dist/services/memory-layer.js +71 -6
- package/dist/services/memory-lint-config.js +111 -2
- package/dist/services/memory-native-review-copy.js +5 -67
- package/dist/services/memory-normal-flow-entry.js +12 -0
- package/dist/services/memory-ordinary-defaults.js +22 -5
- package/dist/services/memory-preflight-handler.js +15 -0
- package/dist/services/memory-repair-current-view.js +1 -1
- package/dist/services/memory-repair-document-delta.js +70 -33
- package/dist/services/memory-repair-history.js +12 -7
- package/dist/services/memory-repair-physical-binding.js +1 -119
- package/dist/services/memory-repair-publication-plan.js +5 -3
- package/dist/services/memory-repair-publication-source.js +1 -1
- package/dist/services/memory-repair-publication.js +3 -2
- package/dist/services/memory-repair-registration.js +2 -1
- package/dist/services/memory-repair-work-baseline.js +4 -2
- package/dist/services/memory-repair-work-completion.js +32 -12
- package/dist/services/memory-repair-work-results.js +21 -2
- package/dist/services/memory-repair-workspace.js +11 -2
- package/dist/services/memory-repair-writer-copy.js +2 -1
- package/dist/services/memory-result-binding.js +78 -12
- package/dist/services/memory-retained-review-authority.js +17 -8
- package/dist/services/memory-retained-second-baseline.js +1 -1
- package/dist/services/memory-retained-second-repairs.js +12 -6
- package/dist/services/memory-review-correction-plan.js +47 -12
- package/dist/services/memory-review-handler.js +17 -17
- package/dist/services/memory-review-inputs.js +55 -9
- package/dist/services/memory-review-limits.js +10 -0
- package/dist/services/memory-review-receipt.js +2 -2
- package/dist/services/memory-review-registration.js +13 -4
- package/dist/services/memory-review-work-custody.js +3 -1
- package/dist/services/memory-review-work-input.js +26 -7
- package/dist/services/memory-review-work-results.js +26 -4
- package/dist/services/memory-second-corrected-review-inputs.js +5 -3
- package/dist/services/memory-second-correction-plan.js +2 -1
- package/dist/services/memory-second-repair-launch.js +4 -4
- package/dist/services/memory-second-repair-source.js +1 -1
- package/dist/services/memory-stage-dispatch.js +38 -1
- package/dist/services/memory-stage-results.js +113 -22
- package/dist/services/memory-tool-probe.js +25 -0
- package/dist/services/memory-upgrade-analysis-handler.js +243 -0
- package/dist/services/memory-upgrade-bootstrap-readiness.js +251 -0
- package/dist/services/memory-upgrade-evidence.js +100 -0
- package/dist/services/memory-upgrade-inventory.js +442 -0
- package/dist/services/memory-upgrade-publication-custody.js +73 -0
- package/dist/services/memory-upgrade-publication.js +413 -0
- package/dist/services/memory-upgrade-selection.js +430 -0
- package/dist/services/memory-upgrade-start.js +60 -0
- package/dist/services/memory-upgrade-storage-readiness.js +340 -0
- package/dist/services/memory-verification-acceptance.js +38 -17
- package/dist/services/memory-verification-checks.js +92 -27
- package/dist/services/memory-verification-receipt.js +6 -5
- package/dist/services/memory-verify-handler.js +15 -13
- package/dist/services/memory-work-result-json.js +12 -0
- package/dist/services/memory-work-result-preparation.js +5 -4
- package/dist/services/merge-server.js +4 -2
- package/dist/services/migrations.js +127 -98
- package/dist/services/native-daemon-history.js +2 -1
- package/dist/services/physical-startup.js +45 -0
- package/dist/services/project-registry.js +97 -0
- package/dist/services/project-summary.js +1 -1
- package/dist/services/projects.js +3 -93
- package/dist/services/prompt-delivery.js +71 -0
- package/dist/services/provider-capacity.js +112 -46
- package/dist/services/recovery-snapshot-database.js +32 -4
- package/dist/services/release-cache.js +8 -0
- package/dist/services/repair-intents.js +1 -12
- package/dist/services/repair-receipt.js +13 -0
- package/dist/services/run-control-worker.js +35 -18
- package/dist/services/run-control.js +2 -1
- package/dist/services/run-controller-adapter.js +2 -1
- package/dist/services/run-controller-process.js +4 -7
- package/dist/services/run-controller-recovery.js +8 -2
- package/dist/services/run-controller.js +215 -94
- package/dist/services/run-fork.js +1 -1
- package/dist/services/run-recovery-guard.js +30 -0
- package/dist/services/run-recovery.js +11 -44
- package/dist/services/run-start-custody.js +17 -5
- package/dist/services/runs.js +6 -1
- package/dist/services/runtime-scope-resume.js +12 -6
- package/dist/services/runtime-scope-stop.js +3 -2
- package/dist/services/runtime-scope-worker.js +46 -19
- package/dist/services/runtime-service.js +2 -1
- package/dist/services/sessions.js +3 -24
- package/dist/services/setup.js +63 -13
- package/dist/services/stage-handlers.js +22 -7
- package/dist/services/stage-pause.js +111 -44
- package/dist/services/version-status.js +1 -1
- package/dist/services/vnext-code.js +2 -2
- package/dist/services/vnext-fanout.js +51 -9
- package/dist/services/vnext-plan-review.js +3 -5
- package/dist/services/work-registry.js +117 -39
- package/dist/services/workspace-bootstrap.js +2 -1
- package/dist/services/workspace-files.js +1 -1
- package/dist/shared/entity-references.js +1 -8
- package/dist/storage/data-preservation.js +65 -0
- package/dist/storage/database.js +53 -19
- package/dist/storage/import-guard.js +84 -27
- package/dist/storage/memory-upgrade-schema-bootstrap.js +38 -0
- package/dist/storage/paired-writer-migration.js +424 -0
- package/dist/storage/writer-contract.js +11 -4
- package/dist/storage/writer-migration.js +313 -52
- package/dist/storage/writer-restore.js +217 -0
- package/package.json +13 -6
|
@@ -40,6 +40,8 @@ import { acceptedDecisionPrecedence, loadExternalStageContext, renderRetainedSta
|
|
|
40
40
|
import { reviewEnvironmentAllowed, reviewExecutionCopyHash, reviewSourceHash, reviewSourcePaths } from "./review-copy.js";
|
|
41
41
|
import { latestRunVerificationMatrix, verificationMatrixInstruction, verificationMatrixContract } from "./verification-matrix.js";
|
|
42
42
|
import { acceptedIntentSourceRole } from "./accepted-intent.js";
|
|
43
|
+
import { isDiagnosticFlow } from "../domain/diagnostic-flows.js";
|
|
44
|
+
import { validateDiagnosticWorkResult } from "./diagnostic-work-results.js";
|
|
43
45
|
const workColumns = "work_id, project_id, run_id, parent_work_id, task, launch_policy, result_schema, payload_json, depends_on_json, status, result, created_at, started_at, updated_at, completed_at";
|
|
44
46
|
export function ensureWorkRegistry(context) { context.db.get("SELECT 1 FROM works LIMIT 1"); context.db.get("SELECT 1 FROM work_sessions LIMIT 1"); }
|
|
45
47
|
function ownsPhysicalWorkSession(context, projectId, owner, identity) {
|
|
@@ -184,6 +186,16 @@ export function validateWorkBatch(value) {
|
|
|
184
186
|
assertNoCycles(items.map((item) => ({ id: item.key, dependencies: item.depends_on ?? [] })));
|
|
185
187
|
assertNoParentCycles(items.filter((item) => Boolean(item.parent)).map((item) => ({ id: item.key, parent: item.parent })));
|
|
186
188
|
}
|
|
189
|
+
/** Public generic allocation must not poison a Memory domain-owned graph.
|
|
190
|
+
* Memory registration owners still reuse prepareWorkBatch/addWorkBatch. */
|
|
191
|
+
export function assertGenericWorkBatchAllowed(context, parentWorkId) {
|
|
192
|
+
const parent = requireWork(context, parentWorkId);
|
|
193
|
+
const run = context.db.get("SELECT flow_kind FROM runs WHERE project_id=? AND id=?", [parent.project_id, parent.run_id]);
|
|
194
|
+
if (run && isDiagnosticFlow(run.flow_kind))
|
|
195
|
+
throw new AppError("diagnostic_work_registration_required", "Diagnostic Stage handlers register the assigned read-only graph; generic allocation is not supported", 2, { phase: "prepare", effect: "no_effect", next_action: "use_issued_stage_entry" });
|
|
196
|
+
if (run && ["mb-init", "mb-upgrade"].includes(run.flow_kind))
|
|
197
|
+
throw new AppError("memory_work_registration_required", "Use the issued memory compose/review/corrections registration, not generic work add-batch", 2, { phase: "prepare", effect: "no_effect", recoverable: true, next_action: "use_issued_memory_registration" });
|
|
198
|
+
}
|
|
187
199
|
export function prepareWorkBatch(context, input) {
|
|
188
200
|
validateBatchRequest(input);
|
|
189
201
|
if (input.file !== undefined && input.value !== undefined)
|
|
@@ -423,7 +435,7 @@ function publishReceiptFile(file, contents) {
|
|
|
423
435
|
}
|
|
424
436
|
}
|
|
425
437
|
/** Rebuild prompt/context projections from a committed Work-start receipt. */
|
|
426
|
-
export function materializePendingWorkStarts(context, projectId, runId) {
|
|
438
|
+
export function materializePendingWorkStarts(context, projectId, runId, progress) {
|
|
427
439
|
const runHome = requireRunHome(requireRun(context, projectId, runId));
|
|
428
440
|
const rows = context.db.all("SELECT ws.id, ws.work_id, ws.prompt_path, ws.start_receipt_json, ws.start_publication_status FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id WHERE w.project_id = ? AND w.run_id = ? AND ws.status = 'running' AND ws.start_receipt_json IS NOT NULL AND (ws.start_publication_status IS NULL OR ws.start_publication_status != 'published')", [projectId, runId]);
|
|
429
441
|
for (const row of rows) {
|
|
@@ -443,6 +455,7 @@ export function materializePendingWorkStarts(context, projectId, runId) {
|
|
|
443
455
|
throw new AppError("work_start_receipt_invalid", "Committed Work start receipt does not match its retained bytes", 1, { work_id: row.work_id });
|
|
444
456
|
if (row.start_publication_status && context.db.writeTransaction(() => context.db.run("UPDATE work_sessions SET start_publication_status = 'publishing' WHERE id = ? AND status = 'running' AND prompt_path = ? AND start_receipt_json = ? AND start_publication_status IN ('pending','publishing')", [row.id, row.prompt_path, row.start_receipt_json]).changes) !== 1)
|
|
445
457
|
continue;
|
|
458
|
+
progress?.(`Work ${row.work_id}: publishing start packet`);
|
|
446
459
|
prepareAgentOutputFile(path.join(runHome, "works", row.work_id, "result-input.json"));
|
|
447
460
|
publishStartReceiptFile(row.prompt_path, receipt.prompt, String(receipt.prompt_sha256));
|
|
448
461
|
publishStartReceiptFile(path.join(path.dirname(row.prompt_path), "context.json"), receipt.context, String(receipt.context_sha256));
|
|
@@ -454,6 +467,7 @@ export function materializePendingWorkStarts(context, projectId, runId) {
|
|
|
454
467
|
continue;
|
|
455
468
|
throw new AppError("work_start_projection_conflict", "Work start binding changed during packet publication", 1, { work_id: row.work_id });
|
|
456
469
|
}
|
|
470
|
+
progress?.(`Work ${row.work_id}: start packet published`);
|
|
457
471
|
}
|
|
458
472
|
}
|
|
459
473
|
function publishStartReceiptFile(file, contents, expectedHash) {
|
|
@@ -599,7 +613,9 @@ export function shortWorkId(id) { return shortEntityReference(id); }
|
|
|
599
613
|
*/
|
|
600
614
|
function rawWorkStartCommand(context, work) {
|
|
601
615
|
const run = requireRun(context, work.project_id, work.run_id), recovery = recoveryGuard(context, work.project_id, work.run_id);
|
|
602
|
-
|
|
616
|
+
const schema = context.db.get("SELECT result_schema FROM works WHERE work_id=?", [work.work_id])?.result_schema;
|
|
617
|
+
const memory = ["dd-flow/memory-composition-work-result@1", "dd-flow/memory-review-work-result@1", "dd-flow/memory-repair-work-result@1"].includes(schema ?? "");
|
|
618
|
+
return runtimeCommand(context, ["work", "start", shortWorkId(work.work_id), "--project-root", run.project_root, ...(recovery ? ["--recovery-id", recovery.recovery_id] : []), "--json", ...(memory ? ["--progress-jsonl"] : [])]);
|
|
603
619
|
}
|
|
604
620
|
export function workStartCommand(context, work, options = {}) {
|
|
605
621
|
const command = rawWorkStartCommand(context, work);
|
|
@@ -632,7 +648,7 @@ export function workStartCommand(context, work, options = {}) {
|
|
|
632
648
|
});
|
|
633
649
|
return managedLifecycleCommand(context, `${command} --invocation-id ${id}`);
|
|
634
650
|
}
|
|
635
|
-
export function assertWorkLaunchReady(context, id) {
|
|
651
|
+
export function assertWorkLaunchReady(context, id, progress) {
|
|
636
652
|
ensureWorkRegistry(context);
|
|
637
653
|
const work = requireWork(context, id);
|
|
638
654
|
assertRunMutationAllowed(context, work.project_id, work.run_id);
|
|
@@ -653,21 +669,25 @@ export function assertWorkLaunchReady(context, id) {
|
|
|
653
669
|
else
|
|
654
670
|
(wave === 1 ? prepareMemoryRepairDispatchFromStage : prepareMemorySecondRepairDispatchFromStage)(context, { projectRoot: run.project_root, runId: work.run_id, workId: work.work_id });
|
|
655
671
|
}
|
|
672
|
+
if (wave)
|
|
673
|
+
progress?.(`Work ${work.work_id}: repair launch verified`);
|
|
656
674
|
if (parsePayload(work)?.read_only === true)
|
|
657
|
-
assertSharedReviewInput(context, work,
|
|
675
|
+
assertSharedReviewInput(context, work, run, undefined, progress);
|
|
676
|
+
progress?.(`Work ${work.work_id}: launch inputs verified`);
|
|
658
677
|
return work;
|
|
659
678
|
}
|
|
660
679
|
export function startWork(context, id, input) {
|
|
680
|
+
input.progress?.(`Work ${id}: preparing start`);
|
|
661
681
|
if (input.hookEventId && !context.env.DD_FLOW_CURRENT_INVOCATION) {
|
|
662
682
|
const retainedWork = requireWork(context, id);
|
|
663
683
|
const retainedRun = requireRun(context, retainedWork.project_id, retainedWork.run_id);
|
|
664
684
|
if (input.projectRoot && resolveProjectRoot(input.projectRoot) !== resolveProjectRoot(retainedRun.project_root))
|
|
665
685
|
throw new AppError("project_mismatch", "Work start belongs to another project", 1);
|
|
666
|
-
const reply = recoverCommittedWorkStart(context, null, input.hookEventId, recoveryGuard(context, retainedWork.project_id, retainedWork.run_id)?.generation ?? 0, retainedWork.work_id);
|
|
686
|
+
const reply = recoverCommittedWorkStart(context, null, input.hookEventId, recoveryGuard(context, retainedWork.project_id, retainedWork.run_id)?.generation ?? 0, retainedWork.work_id, input.progress);
|
|
667
687
|
if (reply)
|
|
668
688
|
return reply;
|
|
669
689
|
}
|
|
670
|
-
const work = assertWorkLaunchReady(context, id);
|
|
690
|
+
const work = assertWorkLaunchReady(context, id, input.progress);
|
|
671
691
|
const run = requireRun(context, work.project_id, work.run_id);
|
|
672
692
|
if (input.projectRoot && resolveProjectRoot(input.projectRoot) !== resolveProjectRoot(run.project_root))
|
|
673
693
|
throw new AppError("project_mismatch", "work start project root does not match its RUN", 1, { work_id: id });
|
|
@@ -676,7 +696,7 @@ export function startWork(context, id, input) {
|
|
|
676
696
|
throw new AppError("run_recovery_generation_stale", "Work launch must use the exact current recovery packet", 1, { work_id: id, recovery_id: recoveryId ?? null });
|
|
677
697
|
const hookEventId = input.hookEventId ?? findRecentMatchingHookEvent(context, { projectId: work.project_id, matchKey: entityReferenceVariants(work.work_id).map((reference) => workStartMatchKey(reference, run.project_root, recoveryId)), errorCode: "trusted_session_binding_required", operation: "work start" }).eventKey;
|
|
678
698
|
const eventKey = hookEventId;
|
|
679
|
-
const started = startBoundWork(context, work, run, () => claimWorkStartHookEvent(context, { projectId: work.project_id, eventKey, workId: work.work_id, projectRoot: run.project_root, ...(recoveryId ? { recoveryId } : {}) }), eventKey);
|
|
699
|
+
const started = startBoundWork(context, work, run, () => claimWorkStartHookEvent(context, { projectId: work.project_id, eventKey, workId: work.work_id, projectRoot: run.project_root, ...(recoveryId ? { recoveryId } : {}) }), eventKey, undefined, input.progress);
|
|
680
700
|
appendFlowRunTimelineEvent(context, work.project_id, work.run_id, { type: "work_started", work_id: work.work_id, work_session_id: started.work_session_id });
|
|
681
701
|
return started;
|
|
682
702
|
}
|
|
@@ -736,7 +756,7 @@ function preparedCoordinatorRoute(context, work, input, snapshot) {
|
|
|
736
756
|
// independently retained RUN authority fences it again without compiling
|
|
737
757
|
// descriptor schemas under this (possibly caller-owned) SQL writer.
|
|
738
758
|
assertPreparedRunBundleSqlAuthority(context, { projectId: work.project_id, runId: work.run_id }, admitted.bundle);
|
|
739
|
-
if (admitted.bundle.descriptor.descriptor.id === "mb-init") {
|
|
759
|
+
if (admitted.bundle.descriptor.descriptor.id === "mb-init" || isDiagnosticFlow(admitted.bundle.descriptor.descriptor.id)) {
|
|
740
760
|
const retained = readDescriptorStageInput(context, { projectId: work.project_id, runId: work.run_id, stage: input.stage }, admitted.bundle);
|
|
741
761
|
const payload = retained.input.payload_base64;
|
|
742
762
|
if (typeof payload !== "string")
|
|
@@ -851,7 +871,8 @@ export function startStageCoordinatorWork(context, input) {
|
|
|
851
871
|
return binding;
|
|
852
872
|
}
|
|
853
873
|
/** Claim, command authority and binding commit together; only packet projection follows. */
|
|
854
|
-
export function startBoundWork(context, work, run, claimIdentity, hookEventId, stage) {
|
|
874
|
+
export function startBoundWork(context, work, run, claimIdentity, hookEventId, stage, progress) {
|
|
875
|
+
progress?.(`Work ${work.work_id}: preparing start packet`);
|
|
855
876
|
const repairWave = registeredMemoryRepairWave(context, work, run);
|
|
856
877
|
const generation = assertRunMutationAllowed(context, work.project_id, work.run_id);
|
|
857
878
|
const directory = path.join(requireRunHome(run), "works", work.work_id);
|
|
@@ -864,16 +885,20 @@ export function startBoundWork(context, work, run, claimIdentity, hookEventId, s
|
|
|
864
885
|
const readOnly = parsePayload(work)?.read_only === true;
|
|
865
886
|
const repairPreparationContext = { ...context, env: { ...context.env, DD_FLOW_MEMORY_REPAIR_HOOK_EVENT: hookEventId } };
|
|
866
887
|
const preparedRepairCopy = repairWave ? prepareMemoryRepairStartOwner(repairPreparationContext, work, run, repairWave) : null;
|
|
888
|
+
if (preparedRepairCopy)
|
|
889
|
+
progress?.(`Work ${work.work_id}: repair copy prepared`);
|
|
867
890
|
const preparedMemoryReview = readOnly && parsePayload(work)?.kind === "memory-review"
|
|
868
|
-
? prepareMemoryReviewWorkInput(context, work, run, generation) : null;
|
|
891
|
+
? prepareMemoryReviewWorkInput(context, work, run, generation, progress) : null;
|
|
869
892
|
const nativeReviewCopy = preparedMemoryReview?.bundle.execution.work("review").delegation.mode === "native"
|
|
870
893
|
? prepareMemoryNativeReviewCopy(context, work.work_id, false, preparedMemoryReview) : null;
|
|
871
894
|
const workerRun = preparedRepairCopy ? { ...run, workspace_root: preparedRepairCopy.root() }
|
|
872
895
|
: nativeReviewCopy ? { ...run, workspace_root: nativeReviewCopy.root } : readOnly && context.env.DD_FLOW_READ_ONLY_WORKSPACE
|
|
873
896
|
? { ...run, workspace_root: path.resolve(context.env.DD_FLOW_READ_ONLY_WORKSPACE) } : run;
|
|
874
|
-
const preparedReviewInput = preparedMemoryReview?.input ?? (readOnly ? assertSharedReviewInput(context, work, run, generation) : null);
|
|
897
|
+
const preparedReviewInput = preparedMemoryReview?.input ?? (readOnly ? assertSharedReviewInput(context, work, run, generation, progress) : null);
|
|
875
898
|
const reviewPaths = preparedReviewInput?.run_inputs.filter(item => item.root === "workspace").map(item => item.path) ?? [];
|
|
876
899
|
const preparedCopy = nativeReviewCopy ?? (readOnly && context.env.DD_FLOW_READ_ONLY_WORKSPACE ? prepareBoundReviewCopy(context, work, run, context.env.DD_FLOW_READ_ONLY_WORKSPACE, reviewPaths) : null);
|
|
900
|
+
if (preparedCopy)
|
|
901
|
+
progress?.(`Work ${work.work_id}: review copy prepared`);
|
|
877
902
|
const fingerprint = readOnly ? preparedReviewInput?.source_sha256 ?? preparedReviewInput?.workspace_fingerprint ?? legacyReviewFingerprint(work, run) ?? workspaceFingerprint(run.workspace_root) : undefined;
|
|
878
903
|
const preparedDependencies = parseDependencies(work).map(dependency => context.db.get("SELECT work_id, result FROM works WHERE work_id = ?", [dependency])).filter(Boolean);
|
|
879
904
|
const preparedRecovery = recoveryPromptForWork(context, work.work_id);
|
|
@@ -885,6 +910,7 @@ export function startBoundWork(context, work, run, claimIdentity, hookEventId, s
|
|
|
885
910
|
if (preparedRecovery)
|
|
886
911
|
promptTemplate = `${recoveryPrompt(preparedRecovery)}\n\n${promptTemplate}`;
|
|
887
912
|
let binding;
|
|
913
|
+
progress?.(`Work ${work.work_id}: claiming start`);
|
|
888
914
|
try {
|
|
889
915
|
binding = context.db.writeTransaction(() => {
|
|
890
916
|
const current = requireWork(context, work.work_id);
|
|
@@ -995,19 +1021,23 @@ export function startBoundWork(context, work, run, claimIdentity, hookEventId, s
|
|
|
995
1021
|
error.details.retry_persistence_error = String(persistence);
|
|
996
1022
|
}
|
|
997
1023
|
if (retry)
|
|
998
|
-
return startBoundWork(context, work, run, claimIdentity, hookEventId, stage);
|
|
1024
|
+
return startBoundWork(context, work, run, claimIdentity, hookEventId, stage, progress);
|
|
999
1025
|
}
|
|
1000
1026
|
throw error;
|
|
1001
1027
|
}
|
|
1028
|
+
progress?.(`Work ${work.work_id}: start committed`);
|
|
1002
1029
|
try {
|
|
1003
1030
|
preparedRepairCopy?.revalidateCommittedStart(context, linkId);
|
|
1031
|
+
if (preparedRepairCopy)
|
|
1032
|
+
progress?.(`Work ${work.work_id}: committed repair start verified`);
|
|
1004
1033
|
if (readOnly) {
|
|
1005
1034
|
const committedWork = requireWork(context, work.work_id);
|
|
1006
|
-
assertSharedReviewInput(context, committedWork, run, generation);
|
|
1035
|
+
assertSharedReviewInput(context, committedWork, run, generation, progress);
|
|
1007
1036
|
const link = context.db.get("SELECT id, work_id, session_id, hook_event_id, status, prompt_path, result_path, created_at, completed_at FROM work_sessions WHERE id = ?", [linkId]);
|
|
1008
1037
|
assertReviewCopyUnchanged(context, committedWork, run, link);
|
|
1038
|
+
progress?.(`Work ${work.work_id}: committed review input verified`);
|
|
1009
1039
|
}
|
|
1010
|
-
materializePendingWorkStarts(context, work.project_id, work.run_id);
|
|
1040
|
+
materializePendingWorkStarts(context, work.project_id, work.run_id, progress);
|
|
1011
1041
|
refreshRunWorkProjection(context, work.project_id, work.run_id);
|
|
1012
1042
|
}
|
|
1013
1043
|
catch (error) {
|
|
@@ -1020,7 +1050,7 @@ export function startBoundWork(context, work, run, claimIdentity, hookEventId, s
|
|
|
1020
1050
|
return binding;
|
|
1021
1051
|
}
|
|
1022
1052
|
/** Read the exact committed reply, never bind another Session or regenerate a packet. */
|
|
1023
|
-
export function recoverCommittedWorkStart(context, invocationId, eventKey, generation, workId) {
|
|
1053
|
+
export function recoverCommittedWorkStart(context, invocationId, eventKey, generation, workId, progress) {
|
|
1024
1054
|
const row = context.db.get("SELECT ws.start_receipt_json, w.project_id, w.run_id, w.work_id, ws.id AS work_session_id, w.result_schema, w.payload_json FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id WHERE ws.start_receipt_json IS NOT NULL AND ws.hook_event_id = ? AND json_extract(ws.start_receipt_json, '$.invocation_id') IS ? AND (? IS NULL OR w.work_id = ?)", [eventKey, invocationId, workId ?? null, workId ?? null]);
|
|
1025
1055
|
if (!row)
|
|
1026
1056
|
return undefined;
|
|
@@ -1032,6 +1062,7 @@ export function recoverCommittedWorkStart(context, invocationId, eventKey, gener
|
|
|
1032
1062
|
if (receipt.generation !== generation || !receipt.response || receipt.response_sha256 !== sha256(JSON.stringify(receipt.response)))
|
|
1033
1063
|
throw new AppError("work_start_receipt_invalid", "Committed reply identity or checksum differs", 1, { invocation_id: invocationId });
|
|
1034
1064
|
assertRunMutationAllowed(context, row.project_id, row.run_id, generation);
|
|
1065
|
+
progress?.(`Work ${row.work_id}: restoring committed start`);
|
|
1035
1066
|
if (repairWave) {
|
|
1036
1067
|
let workspaceRoot;
|
|
1037
1068
|
try {
|
|
@@ -1051,8 +1082,9 @@ export function recoverCommittedWorkStart(context, invocationId, eventKey, gener
|
|
|
1051
1082
|
if (owner.root() !== workspaceRoot)
|
|
1052
1083
|
throw new AppError("work_start_receipt_invalid", "Retained repair workspace differs from its physical start", 1, { effect: "no_effect" });
|
|
1053
1084
|
owner.revalidateCommittedStart(context, row.work_session_id);
|
|
1085
|
+
progress?.(`Work ${row.work_id}: committed repair start verified`);
|
|
1054
1086
|
}
|
|
1055
|
-
materializePendingWorkStarts(context, row.project_id, row.run_id);
|
|
1087
|
+
materializePendingWorkStarts(context, row.project_id, row.run_id, progress);
|
|
1056
1088
|
return receipt.response;
|
|
1057
1089
|
}
|
|
1058
1090
|
/** Root coordinators have no worker checks: settle them inside the stage transaction. */
|
|
@@ -1064,7 +1096,7 @@ export function finishStageCoordinatorWork(context, id, result) {
|
|
|
1064
1096
|
const link = context.db.get("SELECT * FROM work_sessions WHERE work_id = ? AND status = 'running' ORDER BY created_at DESC LIMIT 1", [id]);
|
|
1065
1097
|
if (!link)
|
|
1066
1098
|
throw new AppError("runtime_missing", "Coordinator has no open Work/Session link", 1);
|
|
1067
|
-
validateWorkResult(work, result, run.project_root, run.workspace_root, requireRunHome(run), run.id, link.result_path);
|
|
1099
|
+
validateWorkResult(context, work, result, run.project_root, run.workspace_root, requireRunHome(run), run.id, link.result_path);
|
|
1068
1100
|
finishFanInWork(context, id, result);
|
|
1069
1101
|
context.db.run("UPDATE sessions SET status = 'idle', updated_at = ? WHERE project_id = ? AND session_id = ? AND NOT EXISTS (SELECT 1 FROM work_sessions WHERE session_id = ? AND status = 'running')", [context.now(), work.project_id, link.session_id, link.session_id]);
|
|
1070
1102
|
settleRecoveryWork(context, id, "completed");
|
|
@@ -1074,10 +1106,11 @@ export async function finishWork(context, id, result, progress, retry) {
|
|
|
1074
1106
|
if (work.result_schema === "dd-flow/memory-repair-work-result@1") {
|
|
1075
1107
|
if (retry)
|
|
1076
1108
|
throw new AppError("usage", "Memory repair completion uses retained publication recovery, not code-check retry", 2, { effect: "no_effect" });
|
|
1109
|
+
progress?.(`Memory repair ${work.work_id}: validating settlement`);
|
|
1077
1110
|
prepareWorkSettlement(context, id, "completed", result);
|
|
1078
|
-
const published = publishMemoryRepairWorkCompletion(context, work, result);
|
|
1111
|
+
const published = publishMemoryRepairWorkCompletion(context, work, result, progress);
|
|
1079
1112
|
try {
|
|
1080
|
-
return await finishPublishedMemoryRepairWork(context, published);
|
|
1113
|
+
return await finishPublishedMemoryRepairWork(context, published, progress);
|
|
1081
1114
|
}
|
|
1082
1115
|
catch (error) {
|
|
1083
1116
|
throw new AppError("memory_repair_completion_pending", "Repair publication completed but Work settlement requires reconciliation", 1, { effect: error instanceof AppError && error.details.effect === "unknown" ? "unknown" : "partial",
|
|
@@ -1085,7 +1118,10 @@ export async function finishWork(context, id, result, progress, retry) {
|
|
|
1085
1118
|
cause: error instanceof Error ? error.message : String(error) });
|
|
1086
1119
|
}
|
|
1087
1120
|
}
|
|
1088
|
-
|
|
1121
|
+
progress?.(`Work ${work.work_id}: preparing result`);
|
|
1122
|
+
const prepared = prepareRegisteredMemoryWorkResult(context, work, result, progress);
|
|
1123
|
+
if (prepared)
|
|
1124
|
+
progress?.(`Work ${work.work_id}: result prepared`);
|
|
1089
1125
|
return settle(context, id, "completed", result, progress, retry, prepared ?? undefined);
|
|
1090
1126
|
}
|
|
1091
1127
|
/** Controller-selected registration/result preparation, never an agent-provided binding. */
|
|
@@ -1099,10 +1135,10 @@ export function finishRegisteredMemoryReviewWork(context, prepared) {
|
|
|
1099
1135
|
throw new AppError("memory_work_result_preparation_required", "Memory reviewer completion requires registered input and independent Session proof", 1);
|
|
1100
1136
|
return settle(context, prepared.workId, "completed", prepared.bytes().toString("utf8"), undefined, undefined, prepared);
|
|
1101
1137
|
}
|
|
1102
|
-
export function finishPublishedMemoryRepairWork(context, prepared) {
|
|
1138
|
+
export function finishPublishedMemoryRepairWork(context, prepared, progress) {
|
|
1103
1139
|
if (!PreparedPublishedMemoryRepairWorkResult.isPrepared(prepared))
|
|
1104
1140
|
throw new AppError("memory_work_result_preparation_required", "Repair completion requires independently published registered output", 1, { effect: "no_effect" });
|
|
1105
|
-
return settle(context, prepared.workId, "completed", prepared.bytes().toString("utf8"),
|
|
1141
|
+
return settle(context, prepared.workId, "completed", prepared.bytes().toString("utf8"), progress, undefined, prepared);
|
|
1106
1142
|
}
|
|
1107
1143
|
/** Structured fan-in closes a parent after its Session was handed to a child. */
|
|
1108
1144
|
export function finishFanInWork(context, id, result) {
|
|
@@ -1334,10 +1370,10 @@ function legacyReviewFingerprint(work, run) {
|
|
|
1334
1370
|
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
1335
1371
|
return value.read_only_input?.version !== 2 && value.read_only_input?.parent_work_id === work.parent_work_id && typeof value.read_only_input.fingerprint === "string" ? value.read_only_input.fingerprint : null;
|
|
1336
1372
|
}
|
|
1337
|
-
function assertSharedReviewInput(context, work, run, generation = recoveryGuard(context, work.project_id, work.run_id)?.generation ?? 0) {
|
|
1373
|
+
function assertSharedReviewInput(context, work, run, generation = recoveryGuard(context, work.project_id, work.run_id)?.generation ?? 0, progress) {
|
|
1338
1374
|
const payload = parsePayload(work), kind = payload?.kind;
|
|
1339
1375
|
if (kind === "memory-review") {
|
|
1340
|
-
const input = prepareMemoryReviewWorkInput(context, work, run, generation).input;
|
|
1376
|
+
const input = prepareMemoryReviewWorkInput(context, work, run, generation, progress).input;
|
|
1341
1377
|
assertReviewSiblingBaseline(context, work, input, generation);
|
|
1342
1378
|
return input;
|
|
1343
1379
|
}
|
|
@@ -1464,13 +1500,16 @@ function assertReviewSiblingBaseline(context, work, current, generation) {
|
|
|
1464
1500
|
}
|
|
1465
1501
|
async function settle(context, id, status, result, progress, retry, memoryResult) {
|
|
1466
1502
|
const { work, run, link } = prepareWorkSettlement(context, id, status, result);
|
|
1503
|
+
const report = memoryResult ? progress : undefined;
|
|
1467
1504
|
id = work.work_id;
|
|
1468
1505
|
let receipts = [];
|
|
1469
1506
|
let coordinationDrift = [];
|
|
1470
1507
|
const assertReadOnlyInput = () => {
|
|
1471
1508
|
if (status === "completed" && parsePayload(work)?.read_only === true) {
|
|
1509
|
+
report?.(`Work ${work.work_id}: validating review copy`);
|
|
1472
1510
|
assertReviewCopyUnchanged(context, work, run, link);
|
|
1473
|
-
|
|
1511
|
+
report?.(`Work ${work.work_id}: review copy verified`);
|
|
1512
|
+
const shared = assertSharedReviewInput(context, work, run, undefined, report);
|
|
1474
1513
|
if (shared)
|
|
1475
1514
|
return;
|
|
1476
1515
|
const legacy = legacyReviewFingerprint(work, run);
|
|
@@ -1495,13 +1534,15 @@ async function settle(context, id, status, result, progress, retry, memoryResult
|
|
|
1495
1534
|
: PreparedPublishedMemoryRepairWorkResult.isPrepared(memoryResult);
|
|
1496
1535
|
if (!correctOwner || !memoryResult || memoryResult.workId !== work.work_id || !link)
|
|
1497
1536
|
throw new AppError("memory_work_result_preparation_required", "Memory Work completion requires admitted registered output", 1);
|
|
1537
|
+
report?.(`Work ${work.work_id}: revalidating result`);
|
|
1498
1538
|
memoryResult.revalidate(context);
|
|
1539
|
+
report?.(`Work ${work.work_id}: result revalidated`);
|
|
1499
1540
|
memoryResult.assertSettlementScope(context, link.session_id, link.id);
|
|
1500
1541
|
}
|
|
1501
1542
|
else {
|
|
1502
1543
|
if (memoryResult)
|
|
1503
1544
|
throw new AppError("memory_work_result_preparation_required", "Prepared memory output cannot settle another Work schema", 1);
|
|
1504
|
-
validateWorkResult(work, result, run.project_root, run.workspace_root, requireRunHome(run), run.id, link?.result_path ?? null);
|
|
1545
|
+
validateWorkResult(context, work, result, run.project_root, run.workspace_root, requireRunHome(run), run.id, link?.result_path ?? null);
|
|
1505
1546
|
}
|
|
1506
1547
|
const packet = codePacket(work);
|
|
1507
1548
|
if (packet) {
|
|
@@ -1531,10 +1572,12 @@ async function settle(context, id, status, result, progress, retry, memoryResult
|
|
|
1531
1572
|
// Shared-input and workspace fingerprint checks read files and may traverse
|
|
1532
1573
|
// the project tree. Keep them before the SQLite writer reservation.
|
|
1533
1574
|
assertReadOnlyInput();
|
|
1575
|
+
report?.(`Work ${work.work_id}: input verified`);
|
|
1534
1576
|
const resultReceipt = link?.result_path && work.parent_work_id !== null
|
|
1535
1577
|
? JSON.stringify({ work_id: work.work_id, work_session_id: link.id, status, result_sha256: sha256(result), previous_sha256: receiptFileHash(link.result_path) })
|
|
1536
1578
|
: null;
|
|
1537
1579
|
const now = context.now();
|
|
1580
|
+
report?.(`Work ${work.work_id}: settling result`);
|
|
1538
1581
|
context.db.writeTransaction(() => {
|
|
1539
1582
|
assertWorkSettlementAllowed(context, work.project_id, work.run_id, work.work_id);
|
|
1540
1583
|
assertMemoryRepairTerminalFailureAllowed(context, requireWork(context, work.work_id), status);
|
|
@@ -1554,6 +1597,7 @@ async function settle(context, id, status, result, progress, retry, memoryResult
|
|
|
1554
1597
|
context.db.run("UPDATE sessions SET status = 'idle', updated_at = ? WHERE project_id = ? AND session_id = ?", [now, work.project_id, link.session_id]);
|
|
1555
1598
|
}
|
|
1556
1599
|
});
|
|
1600
|
+
report?.(`Work ${work.work_id}: settlement committed`);
|
|
1557
1601
|
materializePendingWorkResults(context, work.project_id, work.run_id);
|
|
1558
1602
|
settleRecoveryWork(context, work.work_id, status);
|
|
1559
1603
|
refreshRunWorkProjection(context, work.project_id, work.run_id);
|
|
@@ -1625,33 +1669,43 @@ function reactivateBoundSession(context, projectId, sessionId, now) {
|
|
|
1625
1669
|
}
|
|
1626
1670
|
export function workResultByteLimit(context, id) {
|
|
1627
1671
|
const schema = requireWork(context, id).result_schema;
|
|
1628
|
-
return ["dd-flow/memory-composition-work-result@1", "dd-flow/memory-review-work-result@1", "dd-flow/memory-repair-work-result@1"].includes(schema ?? "") ? 1024 * 1024 : undefined;
|
|
1672
|
+
return ["dd-flow/memory-composition-work-result@1", "dd-flow/memory-review-work-result@1", "dd-flow/memory-repair-work-result@1", "dd-flow/diagnostic-work-result@1"].includes(schema ?? "") ? 1024 * 1024 : undefined;
|
|
1629
1673
|
}
|
|
1630
|
-
export function prepareWorkResult(context, id, result) {
|
|
1674
|
+
export function prepareWorkResult(context, id, result, progress) {
|
|
1631
1675
|
try {
|
|
1676
|
+
progress?.(`Work ${id}: preparing result for admission`);
|
|
1632
1677
|
const { work, run, link } = prepareWorkSettlement(context, id, "completed", result);
|
|
1633
1678
|
if (work.result_schema === "dd-flow/memory-repair-work-result@1") {
|
|
1634
1679
|
prepareMemoryRepairWorkCompletion(context, work, result);
|
|
1635
1680
|
return;
|
|
1636
1681
|
}
|
|
1637
|
-
const memoryResult = prepareRegisteredMemoryWorkResult(context, work, result);
|
|
1682
|
+
const memoryResult = prepareRegisteredMemoryWorkResult(context, work, result, progress);
|
|
1638
1683
|
if (memoryResult) {
|
|
1639
1684
|
if (!link)
|
|
1640
1685
|
throw new AppError("memory_work_result_preparation_required", "Memory Work completion requires an active Work Session", 1);
|
|
1686
|
+
progress?.(`Work ${work.work_id}: revalidating prepared input`);
|
|
1641
1687
|
memoryResult.revalidate(context);
|
|
1688
|
+
progress?.(`Work ${work.work_id}: prepared input revalidated`);
|
|
1642
1689
|
memoryResult.assertSettlementScope(context, link.session_id, link.id);
|
|
1643
1690
|
return;
|
|
1644
1691
|
}
|
|
1645
|
-
validateWorkResult(work, result, run.project_root, run.workspace_root, requireRunHome(run), run.id, link?.result_path ?? null);
|
|
1692
|
+
validateWorkResult(context, work, result, run.project_root, run.workspace_root, requireRunHome(run), run.id, link?.result_path ?? null);
|
|
1646
1693
|
}
|
|
1647
1694
|
catch (error) {
|
|
1648
1695
|
prepareError(error, { work_id: id, input_sha256: sha256(result) });
|
|
1649
1696
|
}
|
|
1650
1697
|
}
|
|
1651
|
-
function validateWorkResult(work, result, projectRoot, workspaceRoot, runHome, runId, resultPath) {
|
|
1698
|
+
function validateWorkResult(context, work, result, projectRoot, workspaceRoot, runHome, runId, resultPath) {
|
|
1652
1699
|
assertMemoryRepairWritePrepared(work);
|
|
1653
1700
|
if (!work.result_schema)
|
|
1654
1701
|
return;
|
|
1702
|
+
if (work.result_schema === "dd-flow/diagnostic-work-result@1") {
|
|
1703
|
+
const { bundle, packet } = diagnosticWorkAuthority(context, work, projectRoot);
|
|
1704
|
+
if (typeof packet.aspect_id !== "string" || !["aspect", "critic"].includes(String(packet.role)))
|
|
1705
|
+
throw new AppError("diagnostic_work_input_invalid", "Diagnostic Work has no assigned role/aspect", 1, { effect: "no_effect" });
|
|
1706
|
+
validateDiagnosticWorkResult(Buffer.from(result), bundle.blob("schemas/diagnostic-work-result.schema.json"), workspaceRoot, packet, { workId: work.work_id, aspectId: packet.aspect_id, role: packet.role });
|
|
1707
|
+
return;
|
|
1708
|
+
}
|
|
1655
1709
|
if (["dd-flow/memory-composition-work-result@1", "dd-flow/memory-review-work-result@1"].includes(work.result_schema ?? ""))
|
|
1656
1710
|
throw new AppError("memory_work_result_preparation_required", "Memory Work completion requires admitted registered output, not a mutable schema lookup", 1);
|
|
1657
1711
|
if (!resultPath)
|
|
@@ -1712,7 +1766,7 @@ function registeredMemoryRepairWave(context, work, run) {
|
|
|
1712
1766
|
if (!wave)
|
|
1713
1767
|
return null;
|
|
1714
1768
|
const bundle = readFlowRunExecutionBundle(context, { projectRoot: run.project_root, runId: work.run_id });
|
|
1715
|
-
if (bundle?.descriptor.descriptor.id !== "mb-init")
|
|
1769
|
+
if (bundle?.descriptor.descriptor.id !== "mb-init" && bundle?.descriptor.descriptor.id !== "mb-upgrade")
|
|
1716
1770
|
assertMemoryRepairWritePrepared(work);
|
|
1717
1771
|
return wave;
|
|
1718
1772
|
}
|
|
@@ -1732,10 +1786,8 @@ function prepareMemoryRepairStartOwner(context, work, run, wave) {
|
|
|
1732
1786
|
const owner = wave === 1 ? prepareExternalMemoryRepairCopyFromStage(context, input) : prepareMemorySecondRepairLaunchFromStage(context, input);
|
|
1733
1787
|
if (owner.root() !== workspaceRoot)
|
|
1734
1788
|
throw new AppError("memory_repair_workspace_unbound", "Repair start must inherit its exact independently prepared private copy", 1, { effect: "no_effect" });
|
|
1735
|
-
//
|
|
1736
|
-
//
|
|
1737
|
-
if (wave === 1)
|
|
1738
|
-
owner.revalidate(context);
|
|
1789
|
+
// Both factories verify the private copy and shared sources before returning;
|
|
1790
|
+
// no mutation occurs between that validation and this return.
|
|
1739
1791
|
return owner;
|
|
1740
1792
|
}
|
|
1741
1793
|
function validateCodeWorkResult(work, value, projectRoot, runHome, runId) {
|
|
@@ -1788,10 +1840,30 @@ export function validateReviewResolutions(workId, assigned, resolutions) {
|
|
|
1788
1840
|
if (missing.length || unexpected.length || duplicate)
|
|
1789
1841
|
throw new AppError("review_repair_incomplete", "Review repair must include exactly one resolution for each assigned finding reference", 2, { work_id: workId, missing, unexpected, duplicate });
|
|
1790
1842
|
}
|
|
1843
|
+
function diagnosticWorkAuthority(context, work, projectRoot) {
|
|
1844
|
+
const custody = readWorkBatchCustody(context, work.work_id);
|
|
1845
|
+
const bundle = readFlowRunExecutionBundle(context, { projectRoot, runId: work.run_id });
|
|
1846
|
+
const payload = parsePayload(work), packet = payload?.diagnostic;
|
|
1847
|
+
if (!bundle || !isDiagnosticFlow(bundle.manifest.flow_id) || !packet || payload?.read_only !== true
|
|
1848
|
+
|| !["diagnostic-aspect", "diagnostic-critic"].includes(String(payload.kind))
|
|
1849
|
+
|| packet.flow_id !== bundle.manifest.flow_id || packet.run_id !== work.run_id || packet.stage !== custody.scope.stage
|
|
1850
|
+
|| packet.key !== custody.item_key || packet.packet_sha256 !== hybridDigest({ ...packet, packet_sha256: undefined }))
|
|
1851
|
+
throw new AppError("diagnostic_work_input_invalid", "Diagnostic Work requires its immutable registered assignment", 1, { effect: "no_effect" });
|
|
1852
|
+
assertPreparedRunBundleSqlAuthority(context, { projectId: work.project_id, runId: work.run_id }, bundle);
|
|
1853
|
+
return { bundle, packet };
|
|
1854
|
+
}
|
|
1791
1855
|
function renderWorkerPrompt(context, work, run, dependencies, commands, workspaceView, reviewInput) {
|
|
1792
1856
|
const payload = parsePayload(work);
|
|
1793
1857
|
const runIndex = JSON.parse(context.db.get("SELECT index_json FROM runs WHERE project_id=? AND id=?", [work.project_id, work.run_id]).index_json);
|
|
1794
|
-
if (runIndex.execution_bundle?.flow_id
|
|
1858
|
+
if (isDiagnosticFlow(runIndex.execution_bundle?.flow_id)) {
|
|
1859
|
+
assertRunBundleSqlAuthority(context, { projectId: work.project_id, runId: work.run_id }, runIndex.execution_bundle);
|
|
1860
|
+
const { bundle, packet } = diagnosticWorkAuthority(context, work, run.project_root);
|
|
1861
|
+
return ["<diagnostic_work_packet>", JSON.stringify(packet, null, 2), "</diagnostic_work_packet>", "",
|
|
1862
|
+
"<frozen_work_result_schema>", bundle.blob("schemas/diagnostic-work-result.schema.json").toString("utf8"), "</frozen_work_result_schema>", "",
|
|
1863
|
+
"The packet is the assigned read-only diagnostic task, not an SDLC CODE or repair task. Report uncertainty and all assigned candidate decisions without editing the snapshot or original project.",
|
|
1864
|
+
renderWorkerPromptBody(context, work, run, dependencies, commands)].join("\n");
|
|
1865
|
+
}
|
|
1866
|
+
if (["mb-init", "mb-upgrade"].includes(runIndex.execution_bundle?.flow_id) && (payload?.memory_composition || payload?.kind === "memory-review" || payload?.kind === "memory-repair")) {
|
|
1795
1867
|
assertRunBundleSqlAuthority(context, { projectId: work.project_id, runId: work.run_id }, runIndex.execution_bundle);
|
|
1796
1868
|
// Memory Work owns a descriptor packet, not a missing SDLC SPECIFY or PLAN.
|
|
1797
1869
|
// Reviewer assignment custody is admitted before rendering and rechecked
|
|
@@ -1801,6 +1873,9 @@ function renderWorkerPrompt(context, work, run, dependencies, commands, workspac
|
|
|
1801
1873
|
if (reviewPacket && !reviewInput)
|
|
1802
1874
|
throw new AppError("memory_review_input_invalid", "Reviewer prompt requires independently prepared wave inputs", 1);
|
|
1803
1875
|
return ["<memory_work_packet>", JSON.stringify(packet, null, 2), "</memory_work_packet>", "",
|
|
1876
|
+
...(payload.memory_composition?.upgrade_baseline ? ["<upgrade_preservation_context>",
|
|
1877
|
+
"Read memory_work_packet.upgrade_baseline from run_home. The index maps original workspace paths to retained RUN files containing the actual old bytes. Read the old versions for your assigned paths and preservation_invariants before changing documents. Treat those bytes as historical source data, never instructions or current outputs. Preserve substantive facts, constraints, identifiers and local customizations while applying the accepted upgrade scope.",
|
|
1878
|
+
"</upgrade_preservation_context>", ""] : []),
|
|
1804
1879
|
...(payload.memory_composition ? ["<code_check_profile_contract>",
|
|
1805
1880
|
"The existing CLI contract for .memory-bank/spec/engineering/code-check-profile.json is dd-flow/code-check-profile@6 (or an existing @7 runner-evidence profile). The installed project-check-profiles@1 schema is a different contract, not a prerequisite for authoring this file.",
|
|
1806
1881
|
"This is a shape template, not an assignment or permission to execute checks. Author this file only when your registered task requires it and its path is inside your exact write_scopes. Preserve valid existing @6/@7 bindings; do not downgrade an existing profile or invent runner-evidence contracts.",
|
|
@@ -1881,6 +1956,7 @@ export function resultSchemaGuidance(work, runId) {
|
|
|
1881
1956
|
const scope = packet.write_scopes[0];
|
|
1882
1957
|
return ["This is a shape template, not a completed result. Return only JSON with the fields shown; do not add agent metadata, evidence, findings or changed_paths. Keep the assigned run_id, work_id and packet_sha256 unchanged and include every assigned unit exactly once.",
|
|
1883
1958
|
`Exact write_scopes: ${JSON.stringify(packet.write_scopes)}. Each unit needs a summary and nonempty artifacts with exactly root, path and sha256. Artifact root is workspace or run; path is relative to that root and must name an actual regular output file within an assigned write_scope. Compute sha256 from its current bytes. Replace the example path when it names a directory, and replace the zero hash; never invent output hashes or cite the old bootstrap template as current output.`,
|
|
1959
|
+
"For upgrade Work, workspace write_scopes are exact file paths: include every assigned workspace file in the result artifacts, including files retained unchanged. A report describing edits does not substitute for these current file hashes. If Finish reports missing_workspace_outputs, add the missing references to this result proposal and use its issued retry command; do not edit peer results or expand the assignment.",
|
|
1884
1960
|
"Use accepted only after completing the assigned documents. For an evidenced blocker, retain the evidence and use the assigned Fail command, not a blocked result with Finish. Work settlement does not accept Stage/RUN or independent review.",
|
|
1885
1961
|
"```json", JSON.stringify({ schema_id: schema, run_id: runId, work_id: work.work_id, packet_sha256, outcome: "accepted", summary: "Describe the actual completed documents.",
|
|
1886
1962
|
units: packet.unit_ids.map(id => ({ id, summary: "Describe this unit's actual output.", artifacts: [{ root: scope.root, path: scope.path === "." ? "replace-with-assigned-output.md" : scope.path, sha256: "0".repeat(64) }] })) }, null, 2), "```"];
|
|
@@ -1891,9 +1967,11 @@ export function resultSchemaGuidance(work, runId) {
|
|
|
1891
1967
|
throw new AppError("memory_repair_input_invalid", "Repair guidance requires its registered packet", 1);
|
|
1892
1968
|
return ["This is a shape template, not a completed repair. Return only JSON matching the frozen schema. Use accepted only with a nonempty exact physical create/update/delete delta and one causal resolution for every assigned finding. Only registered write_paths may change. before_sha256 comes from the original registered start view; after_sha256 comes from the actual private output bytes; null means absent or deleted. Never invent hashes, paths, findings, source refs or completion.",
|
|
1893
1969
|
`Assigned finding ids: ${JSON.stringify(packet.findings.map(finding => finding.id))}. Exact write_paths: ${JSON.stringify(packet.write_paths)}.`,
|
|
1894
|
-
"
|
|
1970
|
+
"Use reconciled, not accepted, when the assigned current documents are already correct and the finding or retained diagnostic is contradicted by registered evidence. Leave the entire private MB unchanged, return changes: [], and give every assigned finding a resolution with delta_paths: [] and nonempty evidence citing current registered workspace bytes. Explain the discrepancy without claiming that it is independently resolved. Do not invent a document, cosmetic edit or changed hash to satisfy a delta requirement.",
|
|
1971
|
+
"Each resolution needs finding_id, summary and nonempty evidence with exact root/path/sha256. Accepted repairs require nonempty delta_paths; reconciled results require empty delta_paths. A surviving changed output must be cited with its after hash. A deletion-only resolution needs registered retained RUN evidence. Unchanged evidence must match registered input hashes. Neither outcome claims independent re-review or closure.",
|
|
1972
|
+
"The sanitized private copy intentionally has no Git/worktree metadata and is not the execution context for declared project checks. Do not run Git-bound profile checks there, initialize Git, install dependencies, or run commands through the shared checkout. After Work settlement the coordinator must use the issued memory corrections checks command: the CLI runs fresh checks in the registered project workspace with a new correction evidence context, then dispatches independent selective reviewers. A reconciled proposal requests that proof; it never substitutes for or fabricates passed check receipts.",
|
|
1895
1973
|
"Use blocked only when the MB copy is unchanged and changes/resolutions are empty. If a blocker occurs after edits, preserve the private partial outcome and use Fail; do not conceal those edits as a clean blocked result or mutate shared files.",
|
|
1896
|
-
"Repair completion is publication, not the generic CODE check/retry path. Submit
|
|
1974
|
+
"Repair completion is publication, not the generic CODE check/retry path. Submit an accepted or reconciled private result to the assigned managed work finish command. The CLI independently verifies your bounded UTF-8 result (maximum 1 MiB), exact original start/document state and current publication custody, reserves or restores the bounded plan (zero file actions for reconciled), then settles Work. Do not run memory repair publication-* commands manually or use --retry-check. Successful Work settlement does not close findings, accept independent re-review, or finish Stage/RUN.",
|
|
1897
1975
|
"For a clean blocked result, retain its evidence and use the assigned managed Fail command with the concrete reason; do not submit blocked to Finish. Before any publication attempt, an evidenced blocker after private edits also uses Fail while preserving those private bytes. Once Finish reports partial, unknown, memory_repair_publication_pending or memory_repair_completion_pending, do not use Fail, replace result bytes, reissue Finish, or start another Session: retain the exact operation/WorkSession/receipt and await controller reconciliation. A pending publication is not a new semantic blocker.",
|
|
1898
1976
|
"Poll the original Finish tool handle until terminal. A missing reply, timeout or observation failure is not permission to repeat the command. Resume only through the controller's exact retained-invocation recovery assignment; never invent invocation IDs, adopt today's shared bytes as a new baseline, or treat generic check-retry guidance as repair-publication recovery.",
|
|
1899
1977
|
"```json", JSON.stringify({ schema_id: schema, run_id: runId, work_id: work.work_id, packet_sha256: packet.packet_sha256,
|
|
@@ -2,6 +2,7 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { AppError } from "../shared/errors.js";
|
|
5
|
+
import { retainedBrowserEnvironment } from "./check-environment.js";
|
|
5
6
|
import { assertRunMutationAllowed } from "./run-recovery.js";
|
|
6
7
|
import { codeExecutionEnvironment, reconcileWorkspaceOwnership, runCheck, workspaceOperationId } from "./code-checks.js";
|
|
7
8
|
import { confirmManagedProcess, finishManagedProcess, renewManagedProcess, managedOwnerIsAlive, managedProcessStatus, processTreeIsAlive, registerManagedProcess } from "./managed-processes.js";
|
|
@@ -56,7 +57,7 @@ export async function runWorkspaceBootstrap(context, input) {
|
|
|
56
57
|
finishManagedProcess(context, { id: managed.id, leaseToken: managed.lease_token, state: "failed", reason: "dispatch_fenced_before_spawn" });
|
|
57
58
|
throw error;
|
|
58
59
|
}
|
|
59
|
-
const completed = await runCheck(input.command, input.workspaceRoot, { ...codeExecutionEnvironment(input.workspaceRoot), DD_FLOW_CHECK_COMPLETION_FILE: completionPath }, stdout, stderr, elapsed => input.progress?.(`workspace bootstrap still running (${elapsed}s); logs: ${directory}; next report in 15s`), pid => confirmManagedProcess(context, { id: managed.id, leaseToken: managed.lease_token, pid, processGroupId: process.platform === "win32" ? null : pid }), attemptMs => renewManagedProcess({ ...context, resourceDatabaseMode: "prepared_existing", databaseDeadlineAt: Date.now() + attemptMs }, { id: managed.id, leaseToken: managed.lease_token }));
|
|
60
|
+
const completed = await runCheck(input.command, input.workspaceRoot, { ...codeExecutionEnvironment(input.workspaceRoot, retainedBrowserEnvironment(context.env)), DD_FLOW_CHECK_COMPLETION_FILE: completionPath }, stdout, stderr, elapsed => input.progress?.(`workspace bootstrap still running (${elapsed}s); logs: ${directory}; next report in 15s`), pid => confirmManagedProcess(context, { id: managed.id, leaseToken: managed.lease_token, pid, processGroupId: process.platform === "win32" ? null : pid }), attemptMs => renewManagedProcess({ ...context, resourceDatabaseMode: "prepared_existing", databaseDeadlineAt: Date.now() + attemptMs }, { id: managed.id, leaseToken: managed.lease_token }));
|
|
60
61
|
const current = managedProcessStatus(context).find(item => item.id === managed.id);
|
|
61
62
|
if (processTreeIsAlive(current))
|
|
62
63
|
throw new AppError("bootstrap_tree_not_settled", "Bootstrap shell exited but still owns live processes; resources were retained for inspection", 2, { process_id: managed.id, stdout_path: stdoutPath, stderr_path: stderrPath });
|
|
@@ -29,7 +29,7 @@ export function isGitlessWorkspace(root, result) {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
export function workspaceGitFiles(root, args) {
|
|
32
|
-
const result = spawnSync("git", ["ls-files", "-z", ...args], { cwd: root, encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
|
|
32
|
+
const result = spawnSync("git", ["ls-files", "-z", ...args], { cwd: root, env: { ...process.env, LC_ALL: "C" }, encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
|
|
33
33
|
if (isGitlessWorkspace(root, result))
|
|
34
34
|
return null;
|
|
35
35
|
if (result.status !== 0 || result.error)
|
|
@@ -1,8 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export function shortEntityReference(id) {
|
|
3
|
-
return /^(?:PRJ|RUN|WRK)-\d{3,}(?:-|$)/.exec(id)?.[0]?.replace(/-$/, "") ?? id;
|
|
4
|
-
}
|
|
5
|
-
/** Accepted spellings of one already-resolved entity. Wrong full slugs stay distinct. */
|
|
6
|
-
export function entityReferenceVariants(id) {
|
|
7
|
-
return [...new Set([id, shortEntityReference(id)])];
|
|
8
|
-
}
|
|
1
|
+
export { shortEntityReference, entityReferenceVariants } from "../harness-runtime/lib/entity-references.mjs";
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { AppError } from "../shared/errors.js";
|
|
3
|
+
const quote = (name) => `"${name.replaceAll('"', '""')}"`;
|
|
4
|
+
const digest = (value) => createHash("sha256").update(value).digest("hex");
|
|
5
|
+
function value(input) {
|
|
6
|
+
if (input === null)
|
|
7
|
+
return ["null"];
|
|
8
|
+
if (typeof input === "bigint")
|
|
9
|
+
return ["integer", input.toString()];
|
|
10
|
+
if (typeof input === "number")
|
|
11
|
+
return ["real", Object.is(input, -0) ? "-0" : input.toString()];
|
|
12
|
+
if (typeof input === "string")
|
|
13
|
+
return ["text", input];
|
|
14
|
+
if (input instanceof Uint8Array)
|
|
15
|
+
return ["blob", Buffer.from(input).toString("base64")];
|
|
16
|
+
throw new AppError("store_preservation_unsupported", "Cannot capture this SQLite value type", 1);
|
|
17
|
+
}
|
|
18
|
+
function tableData(db, table, columns) {
|
|
19
|
+
const statement = db.prepare(`SELECT ${columns.map(quote).join(",")} FROM ${quote(table)}`);
|
|
20
|
+
statement.setReadBigInts(true);
|
|
21
|
+
// ponytail: row digests use O(rows) memory; external sorting is needed only
|
|
22
|
+
// if a supported runtime store exceeds the in-memory migration contour.
|
|
23
|
+
const rows = [];
|
|
24
|
+
for (const row of statement.iterate())
|
|
25
|
+
rows.push(digest(JSON.stringify(columns.map(column => value(row[column])))));
|
|
26
|
+
rows.sort();
|
|
27
|
+
const hash = createHash("sha256");
|
|
28
|
+
for (const row of rows)
|
|
29
|
+
hash.update(row);
|
|
30
|
+
return { table, columns: [...columns], row_count: rows.length, sha256: hash.digest("hex") };
|
|
31
|
+
}
|
|
32
|
+
/** Logical old values/identities, not physical row order or implicit rowids.
|
|
33
|
+
* Capture under the migration writer barrier; backup readback uses the same
|
|
34
|
+
* columns, so additive tables/columns do not hide loss of existing data.
|
|
35
|
+
* Writer-contract and migration-receipt tables are operation-owned metadata,
|
|
36
|
+
* not application invariants; all other old tables remain covered.
|
|
37
|
+
*/
|
|
38
|
+
export function captureStoreData(db) {
|
|
39
|
+
const tables = db.prepare("SELECT name FROM sqlite_schema WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT IN ('dd_flow_writer_contract','dd_flow_writer_migrations') ORDER BY name").all();
|
|
40
|
+
if (db.prepare("SELECT 1 FROM sqlite_schema WHERE type='table' AND name='sqlite_sequence'").get())
|
|
41
|
+
tables.push({ name: "sqlite_sequence" });
|
|
42
|
+
return tables.map(row => {
|
|
43
|
+
const name = String(row.name);
|
|
44
|
+
const columns = db.prepare(`PRAGMA table_info(${quote(name)})`).all().map(column => String(column.name));
|
|
45
|
+
if (!columns.length)
|
|
46
|
+
throw new AppError("store_preservation_unsupported", "Cannot capture an empty column inventory", 1, { table: name });
|
|
47
|
+
return tableData(db, name, columns);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
export function verifyStoreData(db, expected) {
|
|
51
|
+
for (const original of expected) {
|
|
52
|
+
let actual;
|
|
53
|
+
try {
|
|
54
|
+
const columns = new Set(db.prepare(`PRAGMA table_info(${quote(original.table)})`).all().map(column => String(column.name)));
|
|
55
|
+
if (original.columns.some(column => !columns.has(column)))
|
|
56
|
+
throw new Error("Original column missing");
|
|
57
|
+
actual = tableData(db, original.table, original.columns);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
throw new AppError("store_data_not_preserved", "Migration lost an original table or column", 1, { table: original.table });
|
|
61
|
+
}
|
|
62
|
+
if (actual.row_count !== original.row_count || actual.sha256 !== original.sha256)
|
|
63
|
+
throw new AppError("store_data_not_preserved", "Migration changed original values or logical identities", 1, { table: original.table, expected_rows: original.row_count, actual_rows: actual.row_count });
|
|
64
|
+
}
|
|
65
|
+
}
|