@deksden-com/dd-flow-cli 0.9.0-beta.152 → 0.9.0-beta.155
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/dist/build-info.json +8 -8
- package/dist/cli/command-inputs.js +33 -3
- package/dist/cli/command-metadata.js +73 -2
- package/dist/cli/delivery.js +108 -0
- package/dist/cli/diagnostic-invocation.js +23 -7
- package/dist/cli/diagnostic.js +14 -3
- package/dist/cli/help.js +117 -1
- package/dist/cli/hook-ingress.js +36 -49
- package/dist/cli/interactive-invocation.js +33 -0
- package/dist/cli/interactive.js +114 -0
- package/dist/cli/memory-fix-invocation.js +52 -0
- package/dist/cli/memory-fix.js +100 -0
- package/dist/cli/memory-flow-admission.js +7 -3
- package/dist/cli/memory-init.js +5 -4
- package/dist/cli/native-hook-ingress.js +5 -1
- package/dist/cli/native-hook-preparation.js +31 -4
- package/dist/cli/run-cli.js +273 -19
- package/dist/cli.js +17 -3
- package/dist/domain/delivery-flows.js +8 -0
- package/dist/domain/distill-flow.js +20 -0
- package/dist/domain/interactive-flow.js +1 -0
- package/dist/domain/memory-fix-flow.js +2 -0
- package/dist/domain/review-fix-flow.js +2 -0
- package/dist/domain/source-snapshot-flows.js +4 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +18 -3
- package/dist/harness-runtime/bin/dd-grok.mjs +15 -5
- package/dist/harness-runtime/bin/dd-zcode.mjs +15 -7
- package/dist/harness-runtime/lib/agy-error-boundary.mjs +18 -10
- package/dist/harness-runtime/lib/agy-native-inspection.mjs +12 -3
- package/dist/harness-runtime/lib/agy-native-inventory.mjs +19 -0
- package/dist/harness-runtime/lib/agy-terminal-envelope.mjs +13 -6
- package/dist/harness-runtime/lib/agy-transcript-stream.mjs +54 -0
- package/dist/harness-runtime/lib/daemon-observation.d.mts +1 -0
- package/dist/harness-runtime/lib/daemon-observation.mjs +12 -4
- package/dist/harness-runtime/lib/daemon-operations.mjs +20 -5
- package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +371 -114
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +43 -3
- package/dist/harness-runtime/lib/dd-codex.mjs +36 -4
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +25 -11
- package/dist/harness-runtime/lib/dd-droid.mjs +124 -11
- package/dist/harness-runtime/lib/dd-grok-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +137 -25
- package/dist/harness-runtime/lib/dd-grok.mjs +4 -2
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +114 -16
- 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 +1 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +10 -3
- package/dist/harness-runtime/lib/delivery-command.mjs +84 -0
- package/dist/harness-runtime/lib/driver-recovery.mjs +0 -19
- package/dist/harness-runtime/lib/droid-observation.mjs +9 -5
- package/dist/harness-runtime/lib/lifecycle-contract.d.mts +1 -0
- package/dist/harness-runtime/lib/lifecycle-contract.mjs +2 -1
- package/dist/harness-runtime/lib/native-hook-command.d.mts +3 -2
- package/dist/harness-runtime/lib/native-hook-command.mjs +63 -8
- package/dist/harness-runtime/lib/native-streams.mjs +3 -10
- package/dist/harness-runtime/lib/opencode-plugin.mjs +36 -9
- package/dist/harness-runtime/lib/operation-errors.mjs +13 -0
- package/dist/harness-runtime/lib/operation-progress.mjs +6 -2
- package/dist/harness-runtime/lib/process-json.mjs +4 -5
- package/dist/harness-runtime/lib/tool-observations.mjs +20 -10
- package/dist/harness-runtime/lib/zcode-native-hook-ownership.mjs +109 -0
- package/dist/schemas/code-work-result.schema.json +2 -1
- package/dist/schemas/delivery-stage-result.schema.json +286 -0
- package/dist/schemas/distill-stage-result.schema.json +667 -0
- package/dist/schemas/distill-work-result.schema.json +428 -0
- package/dist/schemas/execution-bundle.schema.json +6 -2
- package/dist/schemas/interactive-stage-result.schema.json +47 -0
- package/dist/schemas/interactive-work-result.schema.json +63 -0
- package/dist/schemas/mb-fix-stage-result.schema.json +93 -0
- package/dist/schemas/project-flow-pack-manifest.schema.json +2 -2
- package/dist/schemas/review-fix-stage-result.schema.json +210 -0
- package/dist/schemas/run-control-receipt.schema.json +1 -0
- package/dist/services/assigned-lifecycle.js +382 -0
- package/dist/services/check-runner-evidence.js +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-execution.js +4 -1
- package/dist/services/controller-fanout.js +9 -4
- package/dist/services/controller-memory-worker.js +6 -4
- package/dist/services/controller-result-authority.js +122 -0
- package/dist/services/dashboard.js +4 -2
- package/dist/services/delivery-access.js +123 -0
- package/dist/services/delivery-approval.js +68 -0
- package/dist/services/delivery-artifact.js +86 -0
- package/dist/services/delivery-checks.js +178 -0
- package/dist/services/delivery-execution.js +181 -0
- package/dist/services/delivery-intents.js +212 -0
- package/dist/services/delivery-operation.js +81 -0
- package/dist/services/delivery-policy.js +91 -0
- package/dist/services/delivery-procedures.js +43 -0
- package/dist/services/delivery-request.js +99 -0
- package/dist/services/delivery-result.js +144 -0
- package/dist/services/delivery-stage-commands.js +26 -0
- package/dist/services/delivery-stage-entry.js +56 -0
- package/dist/services/delivery-stage-handlers.js +135 -0
- package/dist/services/delivery-stage-results.js +94 -0
- package/dist/services/descriptor-coordinator-session.js +3 -1
- package/dist/services/descriptor-run-start.js +24 -4
- package/dist/services/descriptor-stage-coordinator.js +78 -10
- package/dist/services/descriptor-stage-settlement.js +59 -14
- package/dist/services/diagnostic-repair-intake.js +123 -0
- package/dist/services/diagnostic-repair-source-intake.js +102 -0
- package/dist/services/diagnostic-report.js +30 -0
- package/dist/services/diagnostic-run-bundle.js +48 -9
- package/dist/services/diagnostic-snapshot.js +2 -2
- package/dist/services/diagnostic-stage-handlers.js +3 -2
- package/dist/services/diagnostic-stage-results.js +26 -0
- package/dist/services/diagnostic-start.js +56 -6
- package/dist/services/diagnostic-work-collection.js +3 -3
- package/dist/services/distill-document-publication.js +280 -0
- package/dist/services/distill-promotion.js +197 -0
- package/dist/services/distill-publication-custody.js +162 -0
- package/dist/services/distill-stage-handlers.js +249 -0
- package/dist/services/distill-stage-results.js +105 -0
- package/dist/services/distill-user-inputs.js +4 -0
- package/dist/services/distill-work-batch.js +42 -0
- package/dist/services/distill-work-collection.js +74 -0
- package/dist/services/distill-work-registration.js +93 -0
- package/dist/services/distill-work-results.js +98 -0
- package/dist/services/eval-snapshots.js +17 -7
- package/dist/services/external-work-launch.js +8 -2
- package/dist/services/flow-catalog.js +24 -0
- package/dist/services/frozen-engine.js +19 -2
- package/dist/services/generic-execution.js +4 -3
- package/dist/services/harness-adapter.js +2 -14
- package/dist/services/hook-memory-routing.js +1 -1
- package/dist/services/hooks.js +31 -6
- package/dist/services/hybrid-contracts.js +6 -1
- package/dist/services/installed-flow-source.js +37 -2
- package/dist/services/interactive-attached-session.js +215 -0
- package/dist/services/interactive-baseline-custody.js +54 -0
- package/dist/services/interactive-before-versions.js +148 -0
- package/dist/services/interactive-bundle-data.js +85 -0
- package/dist/services/interactive-checks.js +179 -0
- package/dist/services/interactive-execution.js +33 -0
- package/dist/services/interactive-finish-integration.js +236 -0
- package/dist/services/interactive-git-baseline.js +230 -0
- package/dist/services/interactive-git-dispatch.js +278 -0
- package/dist/services/interactive-input-delivery.js +82 -0
- package/dist/services/interactive-mutation-scope.js +231 -0
- package/dist/services/interactive-request.js +287 -0
- package/dist/services/interactive-run-bundle.js +109 -0
- package/dist/services/interactive-stage-entry.js +41 -0
- package/dist/services/interactive-stage-handlers.js +289 -0
- package/dist/services/interactive-stage-observation.js +136 -0
- package/dist/services/interactive-stage-results.js +111 -0
- package/dist/services/interactive-user-inputs.js +142 -0
- package/dist/services/interactive-work-batch.js +43 -0
- package/dist/services/interactive-work-collection.js +119 -0
- package/dist/services/interactive-work-completion.js +180 -0
- package/dist/services/interactive-work-context.js +249 -0
- package/dist/services/interactive-work-history.js +112 -0
- package/dist/services/interactive-work-registration.js +124 -0
- package/dist/services/interactive-work-results.js +115 -0
- package/dist/services/interactive-work-start.js +155 -0
- package/dist/services/lifecycle-invocations.js +214 -24
- package/dist/services/managed-daemon-binding.js +52 -1
- package/dist/services/memory-compose-handler.js +2 -22
- package/dist/services/memory-finish-integration.js +58 -17
- package/dist/services/memory-fix-finish-integration.js +129 -0
- package/dist/services/memory-fix-git-dispatch.js +139 -0
- package/dist/services/memory-fix-intake-custody.js +11 -0
- package/dist/services/memory-fix-intake-settlement.js +127 -0
- package/dist/services/memory-fix-publication.js +196 -0
- package/dist/services/memory-fix-repair-admission.js +116 -0
- package/dist/services/memory-fix-request.js +141 -0
- package/dist/services/memory-fix-run-bundle.js +83 -0
- package/dist/services/memory-fix-scope.js +78 -0
- package/dist/services/memory-fix-sources.js +85 -0
- package/dist/services/memory-fix-stage-entry.js +24 -0
- package/dist/services/memory-fix-stage-handlers.js +282 -0
- package/dist/services/memory-fix-stage-results.js +106 -0
- package/dist/services/memory-fix-workspace.js +127 -0
- package/dist/services/memory-flow-user-inputs.js +88 -0
- package/dist/services/memory-init-contexts.js +55 -5
- package/dist/services/memory-init-controller-start.js +20 -4
- package/dist/services/memory-layer.js +7 -3
- package/dist/services/memory-native-review-copy.js +27 -3
- package/dist/services/memory-repair-document-delta.js +1 -0
- package/dist/services/project-flow-source.js +195 -0
- package/dist/services/project-git-integration.js +92 -0
- package/dist/services/repair-intents.js +2 -7
- package/dist/services/retained-native-hook.js +245 -0
- package/dist/services/review-fix-handoff.js +82 -0
- package/dist/services/review-fix-intake-custody.js +17 -0
- package/dist/services/review-fix-stage-entry.js +43 -0
- package/dist/services/review-fix-stage-handlers.js +176 -0
- package/dist/services/review-fix-stage-results.js +86 -0
- package/dist/services/run-control-worker.js +42 -1
- package/dist/services/run-control.js +40 -5
- package/dist/services/run-controller-adapter.js +37 -3
- package/dist/services/run-controller-process.js +12 -4
- package/dist/services/run-controller-recovery.js +46 -6
- package/dist/services/run-controller-state.js +3 -2
- package/dist/services/run-controller.js +209 -43
- package/dist/services/run-recovery.js +31 -2
- package/dist/services/run-timeline.js +59 -0
- package/dist/services/runs.js +18 -31
- package/dist/services/sessions.js +2 -1
- package/dist/services/stage-handlers.js +55 -3
- package/dist/services/stage-lifecycle.js +4 -3
- package/dist/services/stage-pause.js +26 -9
- package/dist/services/stage-report-renderer.js +2 -6
- package/dist/services/technical-notes.js +31 -0
- package/dist/services/verification-matrix.js +1 -1
- package/dist/services/version-status.js +1 -1
- package/dist/services/vnext-code-review.js +74 -5
- package/dist/services/vnext-code.js +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 +252 -66
- package/dist/services/worktrees.js +7 -4
- package/dist/storage/database.js +2 -1
- package/dist/verification-components.json +82 -0
- package/package.json +3 -2
- package/tools/verification-jobs.mjs +35 -10
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { AppError } from "../shared/errors.js";
|
|
4
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
5
|
+
import { readInteractiveStageScope } from "./interactive-stage-entry.js";
|
|
6
|
+
import { PreparedInteractiveWorkspaceChanges } from "./interactive-mutation-scope.js";
|
|
7
|
+
import { readDescriptorStageInput } from "./descriptor-stage-coordinator.js";
|
|
8
|
+
import { prepareStagePacket, acceptStagePacket, publishStagePacket, resolveStagePublicationPath } from "./stage-report-renderer.js";
|
|
9
|
+
import { putInteractiveBundleData, readInteractiveBundleData } from "./interactive-bundle-data.js";
|
|
10
|
+
import { interactiveWorkBatch, interactiveWorkPathAllowed } from "./interactive-work-batch.js";
|
|
11
|
+
import { assertPreparedRunBundleSqlAuthority } from "./runs.js";
|
|
12
|
+
import { assertRunMutationAllowed } from "./run-recovery-guard.js";
|
|
13
|
+
import { readInteractiveWorkHistory, assertInteractiveWorkHistory, assertInteractiveCorrectionHistory } from "./interactive-work-history.js";
|
|
14
|
+
import { pendingInteractiveIssues } from "./interactive-work-results.js";
|
|
15
|
+
import { PreparedInteractiveChecks } from "./interactive-checks.js";
|
|
16
|
+
const event = "interactive.work_context", logical = "runtime/work-context.json";
|
|
17
|
+
const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
18
|
+
function refuse(reason) {
|
|
19
|
+
throw new AppError("interactive_work_context_invalid", "Interactive Work requires its exact retained user data and original versions", 2, { reason, effect: "no_effect" });
|
|
20
|
+
}
|
|
21
|
+
function json(bytes) {
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return refuse("invalid_context_json");
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function record(context, scope, bundle, reference) {
|
|
30
|
+
assertPreparedRunBundleSqlAuthority(context, scope, bundle);
|
|
31
|
+
const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type=? AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.stage')=? AND json_extract(payload_json,'$.attempt')=? AND json_extract(payload_json,'$.reference.path')=? LIMIT 2", [scope.projectId, event, scope.runId, scope.stage, scope.attempt, reference.path]);
|
|
32
|
+
if (!rows.length)
|
|
33
|
+
return null;
|
|
34
|
+
if (rows.length !== 1 || Buffer.byteLength(rows[0].payload_json) > 4 * 1024 * 1024)
|
|
35
|
+
refuse("ambiguous_or_unbounded_context");
|
|
36
|
+
const value = json(Buffer.from(rows[0].payload_json));
|
|
37
|
+
if (!value || Object.keys(value).sort().join() !== "attempt,bundle_sha256,input,logical_sha256,packet_refs,reference,run_id,schema_id,stage"
|
|
38
|
+
|| value.schema_id !== "dd-flow/interactive-work-context@1" || value.run_id !== scope.runId || value.stage !== scope.stage || value.attempt !== scope.attempt
|
|
39
|
+
|| value.bundle_sha256 !== bundle.sha256 || !/^[a-f0-9]{64}$/.test(value.logical_sha256)
|
|
40
|
+
|| hybridDigest(value.reference) !== hybridDigest(reference) || hybridDigest(value.input?.context) !== hybridDigest(reference)
|
|
41
|
+
|| value.input?.run_id !== scope.runId || value.input.owner_stage !== scope.stage
|
|
42
|
+
|| !Array.isArray(value.packet_refs) || !value.packet_refs.length || value.packet_refs.length > 33)
|
|
43
|
+
refuse("context_scope_changed");
|
|
44
|
+
interactiveWorkBatch(value.input);
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
/** Selected SQL packet bytes, never mutable projections or caller-owned JSON. */
|
|
48
|
+
export function readRetainedInteractiveWorkContext(context, scope, bundle, reference) {
|
|
49
|
+
const value = record(context, scope, bundle, reference);
|
|
50
|
+
if (!value)
|
|
51
|
+
refuse("context_custody_missing");
|
|
52
|
+
const run = assertPreparedRunBundleSqlAuthority(context, scope, bundle);
|
|
53
|
+
if (!run.run_root)
|
|
54
|
+
refuse("run_root_missing");
|
|
55
|
+
const prefix = `${bundle.descriptor.stage(scope.stage).directory}/work-contexts/${value.logical_sha256}/`;
|
|
56
|
+
const files = new Map(), inventory = [];
|
|
57
|
+
for (const ref of value.packet_refs) {
|
|
58
|
+
if (!ref || ref.root !== "run" || !ref.path.startsWith(prefix) || !/^[a-f0-9]{64}$/.test(ref.sha256))
|
|
59
|
+
refuse("foreign_context_part");
|
|
60
|
+
const name = ref.path.slice(prefix.length);
|
|
61
|
+
if (!interactiveWorkPathAllowed(name) || files.has(name))
|
|
62
|
+
refuse("invalid_context_inventory");
|
|
63
|
+
const suffix = `/${ref.path}`;
|
|
64
|
+
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')=? AND json_extract(payload_json,'$.attempt')=? AND json_extract(payload_json,'$.sha256')=? AND substr(json_extract(payload_json,'$.file'),-?)=? LIMIT 2", [scope.projectId, scope.runId, scope.stage, scope.attempt, ref.sha256, suffix.length, suffix]);
|
|
65
|
+
if (rows.length !== 1 || Buffer.byteLength(rows[0].payload_json) > 2 * 1024 * 1024)
|
|
66
|
+
refuse("context_part_custody_missing");
|
|
67
|
+
const packet = json(Buffer.from(rows[0].payload_json));
|
|
68
|
+
if (typeof packet.base64 !== "string" || resolveStagePublicationPath(context, scope.projectId, scope.runId, packet.file) !== path.join(run.run_root, ref.path))
|
|
69
|
+
refuse("context_part_scope_changed");
|
|
70
|
+
const bytes = Buffer.from(packet.base64, "base64");
|
|
71
|
+
if (!bytes.length || bytes.length > 1024 * 1024 || bytes.toString("base64") !== packet.base64 || hash(bytes) !== ref.sha256)
|
|
72
|
+
refuse("context_part_changed");
|
|
73
|
+
files.set(name, bytes);
|
|
74
|
+
inventory.push({ path: name, sha256: ref.sha256 });
|
|
75
|
+
}
|
|
76
|
+
const selected = readInteractiveBundleData({ manifest: { ...bundle.manifest, inventory }, blob: name => files.get(name) ?? Buffer.alloc(0) }, logical);
|
|
77
|
+
if (selected.sha256 !== value.logical_sha256 || selected.reference.sha256 !== reference.sha256 || reference.path !== `${prefix}${logical}`)
|
|
78
|
+
refuse("context_identity_changed");
|
|
79
|
+
const body = json(selected.bytes);
|
|
80
|
+
if (!body || body.schema_id !== "dd-flow/interactive-work-data@1" || body.run_id !== scope.runId || body.stage !== scope.stage
|
|
81
|
+
|| body.work_role !== value.input.stage
|
|
82
|
+
|| body.bundle_sha256 !== bundle.sha256 || body.input_sha256 !== value.input.input_sha256
|
|
83
|
+
|| body.baseline_sha256 !== value.input.baseline_sha256
|
|
84
|
+
|| body.user_inputs_sha256 !== value.input.user_inputs_sha256 || body.scope_revision !== value.input.scope_revision
|
|
85
|
+
|| body.file_changes_sha256 !== value.input.file_changes_sha256 || !Array.isArray(body.file_changes)
|
|
86
|
+
|| hybridDigest(body.file_changes) !== value.input.file_changes_sha256 || !Array.isArray(body.current_files)
|
|
87
|
+
|| body.current_files.some(item => !item || item.kind !== "file" || !interactiveWorkPathAllowed(item.path) || !/^[a-f0-9]{64}$/.test(item.sha256))
|
|
88
|
+
|| body.worker_instruction?.root !== "run" || body.worker_instruction.path !== `execution-bundle/interactive/hybrid/${value.input.stage}-worker.md`
|
|
89
|
+
|| hash(Buffer.from(value.input.instruction)) !== body.worker_instruction.sha256
|
|
90
|
+
|| hybridDigest(value.input.read_inputs) !== hybridDigest([...value.packet_refs, body.worker_instruction, ...(body.stage_checks?.sources ?? []),
|
|
91
|
+
...body.current_files.map(item => ({ root: "workspace", path: item.path, sha256: item.sha256 }))]))
|
|
92
|
+
refuse("context_binding_changed");
|
|
93
|
+
if (body.stage_checks) {
|
|
94
|
+
const { plan, plan_sha256, sources, receipts } = body.stage_checks;
|
|
95
|
+
if (body.work_role !== "review" || plan_sha256 !== hybridDigest(plan) || plan.run_id !== scope.runId || plan.stage !== scope.stage
|
|
96
|
+
|| plan.bundle_sha256 !== bundle.sha256 || plan.input_sha256 !== body.input_sha256 || plan.user_inputs_sha256 !== body.user_inputs_sha256
|
|
97
|
+
|| plan.scope_revision !== body.scope_revision || plan.file_changes_sha256 !== body.file_changes_sha256
|
|
98
|
+
|| !Array.isArray(receipts) || !Array.isArray(sources) || sources.some(ref => ref.root !== "run")
|
|
99
|
+
|| receipts.some(ref => !sources.some(source => source.path === ref.path && source.sha256 === ref.sha256)))
|
|
100
|
+
refuse("stage_checks_changed");
|
|
101
|
+
}
|
|
102
|
+
assertInteractiveWorkHistory(body.work_history, scope.runId, bundle.sha256);
|
|
103
|
+
if (scope.stage === "review" && value.input.stage === "hardening")
|
|
104
|
+
assertInteractiveCorrectionHistory(body.work_history, scope.attempt);
|
|
105
|
+
if (hybridDigest(body.pending_issues) !== hybridDigest(pendingInteractiveIssues(body.work_history, body)))
|
|
106
|
+
refuse("pending_issues_changed");
|
|
107
|
+
return { record: value, bytes: selected.bytes, body };
|
|
108
|
+
}
|
|
109
|
+
/** Source preparation only, not a Work registration/launch permission. The
|
|
110
|
+
* graph owner pairs retainPreparedSql with its existing atomic batch writer. */
|
|
111
|
+
export class PreparedInteractiveWorkContext {
|
|
112
|
+
#selected;
|
|
113
|
+
#changes;
|
|
114
|
+
#scope;
|
|
115
|
+
#record;
|
|
116
|
+
#packets;
|
|
117
|
+
#history;
|
|
118
|
+
#verification;
|
|
119
|
+
constructor(selected, changes, scope, value, packets, history, verification) {
|
|
120
|
+
this.#selected = selected;
|
|
121
|
+
this.#changes = changes;
|
|
122
|
+
this.#scope = scope;
|
|
123
|
+
this.#record = structuredClone(value);
|
|
124
|
+
this.#packets = packets;
|
|
125
|
+
this.#history = structuredClone(history);
|
|
126
|
+
this.#verification = verification;
|
|
127
|
+
Object.freeze(this);
|
|
128
|
+
}
|
|
129
|
+
static prepare(context, input) {
|
|
130
|
+
if (context.db.isOwnedWriteTransaction?.())
|
|
131
|
+
refuse("preparation_outside_writer_required");
|
|
132
|
+
if (!["hardening", "review"].includes(input.stage))
|
|
133
|
+
refuse("work_stage_required");
|
|
134
|
+
const role = input.role ?? input.stage;
|
|
135
|
+
if (!["hardening", "review"].includes(role) || input.stage === "hardening" && role !== "hardening")
|
|
136
|
+
refuse("invalid_work_owner_role");
|
|
137
|
+
const selected = readInteractiveStageScope(context, input), { bundle, scope, run } = selected;
|
|
138
|
+
const stage = readDescriptorStageInput(context, scope, bundle);
|
|
139
|
+
if (!run.run_root)
|
|
140
|
+
refuse("run_root_missing");
|
|
141
|
+
const history = readInteractiveWorkHistory(context, scope, bundle);
|
|
142
|
+
if (input.stage === "review" && role === "hardening") {
|
|
143
|
+
assertInteractiveCorrectionHistory(history, stage.attempt);
|
|
144
|
+
bundle.execution.work(input.stage, undefined, role);
|
|
145
|
+
}
|
|
146
|
+
const changes = PreparedInteractiveWorkspaceChanges.prepare(context, scope, bundle, { projectRoot: bundle.execution.snapshot().scope.projectRoot,
|
|
147
|
+
workspace: bundle.execution.snapshot().scope.workspace, gitExecutable: input.gitExecutable, env: context.env });
|
|
148
|
+
const workerPath = `interactive/hybrid/${role}-worker.md`, workerBytes = bundle.blob(workerPath);
|
|
149
|
+
let instruction;
|
|
150
|
+
try {
|
|
151
|
+
instruction = new TextDecoder("utf-8", { fatal: true }).decode(workerBytes);
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return refuse("invalid_worker_instruction");
|
|
155
|
+
}
|
|
156
|
+
if (!bundle.blob("schemas/interactive-work-result.schema.json").equals(bundle.engine().schema("interactive-work-result")))
|
|
157
|
+
refuse("worker_schema_not_qualified");
|
|
158
|
+
const current = changes.currentSnapshot(), patch = changes.snapshot().delta.changes, entry = selected.selected.baseline.snapshot(), state = selected.selected;
|
|
159
|
+
let stageChecks = null;
|
|
160
|
+
let verificationOwner = null;
|
|
161
|
+
if (input.checks) {
|
|
162
|
+
if (!(input.checks instanceof PreparedInteractiveChecks) || role !== "review")
|
|
163
|
+
refuse("owned_review_checks_required");
|
|
164
|
+
const plan = input.checks.value(), evidence = input.checks.evidence(context), sources = new Map();
|
|
165
|
+
if (plan.run_id !== input.runId || plan.stage !== input.stage || plan.bundle_sha256 !== bundle.sha256 || plan.input_sha256 !== stage.inputSha256
|
|
166
|
+
|| plan.user_inputs_sha256 !== state.inputs.sha256 || plan.scope_revision !== state.revision || plan.file_changes_sha256 !== hybridDigest(patch))
|
|
167
|
+
refuse("foreign_review_checks");
|
|
168
|
+
for (const source of evidence?.sourceReferences() ?? []) {
|
|
169
|
+
const prior = sources.get(source.path);
|
|
170
|
+
if (prior && prior.sha256 !== source.sha256)
|
|
171
|
+
refuse("conflicting_check_sources");
|
|
172
|
+
sources.set(source.path, source);
|
|
173
|
+
}
|
|
174
|
+
stageChecks = { plan_sha256: input.checks.sha256, plan, receipts: evidence?.retainedReferences(context) ?? [], sources: [...sources.values()] };
|
|
175
|
+
verificationOwner = { context, checks: input.checks, evidence };
|
|
176
|
+
}
|
|
177
|
+
const currentFiles = current.entries.filter(item => item.kind === "file" && interactiveWorkPathAllowed(item.path));
|
|
178
|
+
const body = { schema_id: "dd-flow/interactive-work-data@1", run_id: input.runId, stage: input.stage, work_role: role, bundle_sha256: bundle.sha256,
|
|
179
|
+
input_sha256: stage.inputSha256, baseline_sha256: state.baseline.sha256, user_inputs_sha256: state.inputs.sha256, scope_revision: state.revision, file_changes_sha256: hybridDigest(patch),
|
|
180
|
+
delivered_user_inputs: { initial_request: state.inputs.initial_request, messages: state.inputs.messages, finish_requested: state.inputs.finish_requested },
|
|
181
|
+
entry_git: { head: entry.head, branch: entry.branch, index_sha256: entry.index_sha256, status_base64: entry.status_base64, visibility: entry.visibility },
|
|
182
|
+
scope: { paths: state.active_paths, decisions: state.records.map(item => ({ basis: item.basis, reason: item.reason, paths: item.paths, user_inputs_sha256: item.user_inputs_sha256 })) },
|
|
183
|
+
before_versions: state.before_versions.filter(item => state.active_paths.includes(item.path)), current_files: currentFiles, file_changes: patch,
|
|
184
|
+
work_history: history, stage_checks: stageChecks, pending_issues: pendingInteractiveIssues(history, { file_changes_sha256: hybridDigest(patch), user_inputs_sha256: state.inputs.sha256, scope_revision: state.revision }),
|
|
185
|
+
worker_instruction: { root: "run", path: `execution-bundle/${workerPath}`, sha256: hash(workerBytes) } };
|
|
186
|
+
const bytes = Buffer.from(JSON.stringify(body)), blobs = new Map(), data = putInteractiveBundleData(blobs, logical, bytes);
|
|
187
|
+
const prefix = `${bundle.descriptor.stage(input.stage).directory}/work-contexts/${data.sha256}/`;
|
|
188
|
+
const packetRefs = [...blobs].map(([name, content]) => ({ root: "run", path: `${prefix}${name}`, sha256: hash(content) }));
|
|
189
|
+
const reference = { root: "run", path: `${prefix}${logical}`, sha256: data.reference.sha256 };
|
|
190
|
+
const workInput = { run_id: input.runId, stage: role, owner_stage: input.stage, input_sha256: stage.inputSha256,
|
|
191
|
+
baseline_sha256: state.baseline.sha256, user_inputs_sha256: state.inputs.sha256, scope_revision: state.revision,
|
|
192
|
+
file_changes_sha256: hybridDigest(patch), context: reference, read_inputs: [...packetRefs, { ...body.worker_instruction, root: "run" }, ...(stageChecks?.sources ?? []),
|
|
193
|
+
...currentFiles.map(item => ({ root: "workspace", path: item.path, sha256: item.sha256 }))],
|
|
194
|
+
write_paths: role === "hardening" ? [...state.active_paths] : [], instruction };
|
|
195
|
+
interactiveWorkBatch(workInput);
|
|
196
|
+
const ownedScope = { ...scope, stage: input.stage, attempt: stage.attempt };
|
|
197
|
+
const value = { schema_id: "dd-flow/interactive-work-context@1", run_id: input.runId, stage: input.stage,
|
|
198
|
+
attempt: stage.attempt, bundle_sha256: bundle.sha256, logical_sha256: data.sha256, reference, packet_refs: packetRefs, input: workInput };
|
|
199
|
+
if (Buffer.byteLength(JSON.stringify(value)) > 4 * 1024 * 1024)
|
|
200
|
+
refuse("unbounded_context_metadata");
|
|
201
|
+
const packets = [...blobs].map(([name, content]) => prepareStagePacket(path.join(run.run_root, prefix, name), content));
|
|
202
|
+
const prepared = new PreparedInteractiveWorkContext(selected, changes, ownedScope, value, packets, history, verificationOwner);
|
|
203
|
+
prepared.revalidate();
|
|
204
|
+
return prepared;
|
|
205
|
+
}
|
|
206
|
+
snapshot() { return structuredClone(this.#record); }
|
|
207
|
+
workInput() { return structuredClone(this.#record.input); }
|
|
208
|
+
revalidate() { this.#changes.revalidate(); if (this.#verification) {
|
|
209
|
+
this.#verification.checks.revalidate(this.#verification.context);
|
|
210
|
+
this.#verification.evidence?.revalidate(this.#verification.context);
|
|
211
|
+
} }
|
|
212
|
+
assertCustody(context) {
|
|
213
|
+
this.#selected.assertCustody(context);
|
|
214
|
+
this.#changes.assertCustody(context);
|
|
215
|
+
this.#verification?.checks.assertCustody(context);
|
|
216
|
+
this.#verification?.evidence?.assertCustody(context);
|
|
217
|
+
if (hybridDigest(readInteractiveWorkHistory(context, this.#scope, this.#selected.bundle)) !== hybridDigest(this.#history))
|
|
218
|
+
refuse("work_history_changed");
|
|
219
|
+
const current = readDescriptorStageInput(context, this.#scope, this.#selected.bundle);
|
|
220
|
+
if (current.attempt !== this.#scope.attempt || current.inputSha256 !== this.#record.input.input_sha256)
|
|
221
|
+
refuse("stage_input_changed");
|
|
222
|
+
}
|
|
223
|
+
retainPreparedSql(context) {
|
|
224
|
+
if (!context.db.isOwnedWriteTransaction?.())
|
|
225
|
+
refuse("owned_writer_required");
|
|
226
|
+
const prior = record(context, this.#scope, this.#selected.bundle, this.#record.reference);
|
|
227
|
+
if (prior) {
|
|
228
|
+
if (hybridDigest(prior) !== hybridDigest(this.#record))
|
|
229
|
+
refuse("context_changed");
|
|
230
|
+
return { changed: false };
|
|
231
|
+
}
|
|
232
|
+
this.assertCustody(context);
|
|
233
|
+
assertRunMutationAllowed(context, this.#scope.projectId, this.#scope.runId);
|
|
234
|
+
const run = assertPreparedRunBundleSqlAuthority(context, this.#scope, this.#selected.bundle), index = JSON.parse(run.index_json);
|
|
235
|
+
if (run.status !== "running" || index.current_stage !== this.#scope.stage || !index.stage_runs?.some((item) => item.stage === this.#scope.stage && item.status === "running" && item.attempt === this.#scope.attempt))
|
|
236
|
+
refuse("active_work_stage_required");
|
|
237
|
+
for (const prepared of this.#packets)
|
|
238
|
+
acceptStagePacket(context, { ...this.#scope, prepared });
|
|
239
|
+
context.db.run("INSERT INTO audit_events(project_id,event_type,payload_json,created_at) VALUES(?,?,?,?)", [this.#scope.projectId, event, JSON.stringify(this.#record), context.now()]);
|
|
240
|
+
readRetainedInteractiveWorkContext(context, this.#scope, this.#selected.bundle, this.#record.reference);
|
|
241
|
+
return { changed: true };
|
|
242
|
+
}
|
|
243
|
+
publish(context) {
|
|
244
|
+
const retained = readRetainedInteractiveWorkContext(context, this.#scope, this.#selected.bundle, this.#record.reference);
|
|
245
|
+
const run = assertPreparedRunBundleSqlAuthority(context, this.#scope, this.#selected.bundle);
|
|
246
|
+
for (const ref of retained.record.packet_refs)
|
|
247
|
+
publishStagePacket(context, this.#scope.projectId, this.#scope.runId, this.#scope.stage, path.join(run.run_root, ref.path));
|
|
248
|
+
}
|
|
249
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { AppError } from "../shared/errors.js";
|
|
3
|
+
import { readRegisteredInteractiveWork } from "./interactive-work-registration.js";
|
|
4
|
+
import { readCompletedInteractiveWork } from "./interactive-work-completion.js";
|
|
5
|
+
import { readInteractiveWorkStart } from "./interactive-work-start.js";
|
|
6
|
+
import { assertPreparedRunBundleSqlAuthority } from "./runs.js";
|
|
7
|
+
const hash = (text) => createHash("sha256").update(text).digest("hex");
|
|
8
|
+
function refuse(reason) {
|
|
9
|
+
throw new AppError("interactive_work_history_invalid", "Interactive corrections require the original registered Work history", 2, { reason, effect: "no_effect" });
|
|
10
|
+
}
|
|
11
|
+
/** A reviewer remains read-only. A separate author can be registered inside
|
|
12
|
+
* that same Stage only after its independently completed negative review. */
|
|
13
|
+
export function assertInteractiveCorrectionHistory(history, attempt) {
|
|
14
|
+
const prior = history.works.findLast(work => work.owner_stage === "review" && work.stage === "review" && work.attempt === attempt && work.status === "completed");
|
|
15
|
+
if (!prior || typeof prior.result === "string" || prior.result.verdict === "ready")
|
|
16
|
+
refuse("negative_review_required_for_correction");
|
|
17
|
+
}
|
|
18
|
+
/** Retained task data validation only; never reconstruct custody from this JSON. */
|
|
19
|
+
export function assertInteractiveWorkHistory(value, runId, bundleSha256) {
|
|
20
|
+
if (!value || Object.keys(value).sort().join() !== "bundle_sha256,run_id,schema_id,works"
|
|
21
|
+
|| value.schema_id !== "dd-flow/interactive-work-history@1" || value.run_id !== runId || value.bundle_sha256 !== bundleSha256
|
|
22
|
+
|| !Array.isArray(value.works) || value.works.length > 20_000)
|
|
23
|
+
refuse("invalid_retained_history");
|
|
24
|
+
const ids = new Set();
|
|
25
|
+
let total = 0;
|
|
26
|
+
for (const item of value.works) {
|
|
27
|
+
if (!item || Object.keys(item).sort().join() !== "actor,attempt,owner_stage,request_id,result,result_sha256,review_binding,stage,status,work_id,work_session_id"
|
|
28
|
+
|| typeof item.work_id !== "string" || !item.work_id || ids.has(item.work_id) || !["hardening", "review"].includes(item.stage)
|
|
29
|
+
|| typeof item.attempt !== "string" || !item.attempt || !/^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/.test(item.request_id)
|
|
30
|
+
|| !["hardening", "review"].includes(item.owner_stage) || item.owner_stage === "hardening" && item.stage !== "hardening"
|
|
31
|
+
|| !["completed", "failed", "cancelled"].includes(item.status)
|
|
32
|
+
|| (item.work_session_id === null ? item.actor !== null || item.status === "completed"
|
|
33
|
+
: typeof item.work_session_id !== "string" || !item.work_session_id || !item.actor
|
|
34
|
+
|| Object.keys(item.actor).sort().join() !== "harness,provider_session_id"
|
|
35
|
+
|| typeof item.actor.harness !== "string" || !item.actor.harness
|
|
36
|
+
|| typeof item.actor.provider_session_id !== "string" || !item.actor.provider_session_id))
|
|
37
|
+
refuse("invalid_history_entry");
|
|
38
|
+
if (item.status === "completed" && item.stage === "review" ? !item.review_binding
|
|
39
|
+
|| Object.keys(item.review_binding).sort().join() !== "file_changes_sha256,scope_revision,user_inputs_sha256"
|
|
40
|
+
|| Object.values(item.review_binding).some(value => typeof value !== "string" || !/^[a-f0-9]{64}$/.test(value))
|
|
41
|
+
: item.review_binding !== null)
|
|
42
|
+
refuse("invalid_history_review_binding");
|
|
43
|
+
const text = typeof item.result === "string" ? item.result : JSON.stringify(item.result);
|
|
44
|
+
if (typeof text !== "string" || !text.length || Buffer.byteLength(text) > 1024 * 1024 || hash(text) !== item.result_sha256)
|
|
45
|
+
refuse("history_result_changed");
|
|
46
|
+
if (item.status === "completed" ? typeof item.result !== "object" || !item.result || item.result.schema_id !== "dd-flow/interactive-work-result@1"
|
|
47
|
+
|| item.result.run_id !== runId || item.result.work_id !== item.work_id || item.result.stage !== item.stage : typeof item.result !== "string")
|
|
48
|
+
refuse("foreign_history_result");
|
|
49
|
+
ids.add(item.work_id);
|
|
50
|
+
total += Buffer.byteLength(JSON.stringify(item));
|
|
51
|
+
if (total > 32 * 1024 * 1024)
|
|
52
|
+
refuse("history_budget_exceeded");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** Original SQL custody, in registration order, across all attempts and stages.
|
|
56
|
+
* Missing/active Works refuse; terminal failures remain data, never fake reviews. */
|
|
57
|
+
export function readInteractiveWorkHistory(context, scope, bundle) {
|
|
58
|
+
return context.db.readSnapshot(() => {
|
|
59
|
+
if (bundle.manifest.flow_id !== "interactive" || bundle.manifest.run_id !== scope.runId)
|
|
60
|
+
refuse("interactive_bundle_required");
|
|
61
|
+
assertPreparedRunBundleSqlAuthority(context, scope, bundle);
|
|
62
|
+
const rows = context.db.all("SELECT j.value AS work_id FROM audit_events f,json_each(f.payload_json,'$.work_ids') j WHERE f.project_id=? AND f.event_type='interactive.work_registration' AND json_extract(f.payload_json,'$.run_id')=? ORDER BY f.id,j.key LIMIT 20001", [scope.projectId, scope.runId]);
|
|
63
|
+
const works = context.db.all("SELECT work_id,status FROM works WHERE project_id=? AND run_id=? AND result_schema='dd-flow/interactive-work-result@1' LIMIT 20001", [scope.projectId, scope.runId]);
|
|
64
|
+
const registered = new Set(rows.map(row => row.work_id));
|
|
65
|
+
if (rows.length > 20_000 || works.length !== rows.length || registered.size !== rows.length
|
|
66
|
+
|| works.some(work => !registered.has(work.work_id)))
|
|
67
|
+
refuse("history_registration_missing");
|
|
68
|
+
const byId = new Map(works.map(work => [work.work_id, work])), entries = [];
|
|
69
|
+
let total = 0;
|
|
70
|
+
for (const row of rows) {
|
|
71
|
+
const work = byId.get(row.work_id);
|
|
72
|
+
if (!["completed", "failed", "cancelled"].includes(work.status))
|
|
73
|
+
refuse("prior_work_not_terminal");
|
|
74
|
+
const text = context.db.get("SELECT result FROM works WHERE project_id=? AND run_id=? AND work_id=? AND length(CAST(result AS BLOB))<=1048576", [scope.projectId, scope.runId, row.work_id])?.result;
|
|
75
|
+
if (!text)
|
|
76
|
+
refuse("unbounded_or_missing_history_result");
|
|
77
|
+
const assignment = readRegisteredInteractiveWork(context, scope, bundle, row.work_id);
|
|
78
|
+
const link = context.db.get("SELECT id,status,result_receipt_json FROM work_sessions WHERE work_id=? ORDER BY rowid DESC LIMIT 1", [row.work_id]);
|
|
79
|
+
let actor = null, result = text;
|
|
80
|
+
if (work.status === "completed") {
|
|
81
|
+
const completed = readCompletedInteractiveWork(context, { ...scope, workId: row.work_id }, bundle);
|
|
82
|
+
if (link?.id !== completed.completed.session.id)
|
|
83
|
+
refuse("history_latest_session_changed");
|
|
84
|
+
actor = { harness: completed.completed.session.harness, provider_session_id: completed.completed.session.provider_session_id };
|
|
85
|
+
result = completed.result;
|
|
86
|
+
}
|
|
87
|
+
else if (link) {
|
|
88
|
+
const start = readInteractiveWorkStart(context, { ...scope, workId: row.work_id }, bundle, link.id);
|
|
89
|
+
if (link.status !== work.status || !link.result_receipt_json || Buffer.byteLength(link.result_receipt_json) > 16384)
|
|
90
|
+
refuse("terminal_session_custody_missing");
|
|
91
|
+
const receipt = JSON.parse(link.result_receipt_json);
|
|
92
|
+
if (receipt.work_id !== row.work_id || receipt.work_session_id !== link.id || receipt.status !== work.status || receipt.result_sha256 !== hash(text))
|
|
93
|
+
refuse("terminal_result_changed");
|
|
94
|
+
actor = { harness: start.receipt.actor.harness, provider_session_id: start.receipt.actor.provider_session_id };
|
|
95
|
+
}
|
|
96
|
+
const entry = { work_id: row.work_id, stage: assignment.packet.stage, owner_stage: assignment.packet.owner_stage, attempt: assignment.receipt.attempt,
|
|
97
|
+
request_id: assignment.receipt.request_id, status: work.status, work_session_id: link?.id ?? null,
|
|
98
|
+
review_binding: work.status === "completed" && assignment.packet.stage === "review" ? {
|
|
99
|
+
file_changes_sha256: assignment.packet.file_changes_sha256, user_inputs_sha256: assignment.packet.user_inputs_sha256,
|
|
100
|
+
scope_revision: assignment.packet.scope_revision
|
|
101
|
+
} : null,
|
|
102
|
+
actor, result_sha256: hash(text), result };
|
|
103
|
+
total += Buffer.byteLength(JSON.stringify(entry));
|
|
104
|
+
if (total > 32 * 1024 * 1024)
|
|
105
|
+
refuse("history_budget_exceeded");
|
|
106
|
+
entries.push(entry);
|
|
107
|
+
}
|
|
108
|
+
const history = { schema_id: "dd-flow/interactive-work-history@1", run_id: scope.runId, bundle_sha256: bundle.sha256, works: entries };
|
|
109
|
+
assertInteractiveWorkHistory(history, scope.runId, bundle.sha256);
|
|
110
|
+
return history;
|
|
111
|
+
});
|
|
112
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { AppError } from "../shared/errors.js";
|
|
2
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
3
|
+
import { readInteractiveStageScope } from "./interactive-stage-entry.js";
|
|
4
|
+
import { readDescriptorRootWork } from "./descriptor-root-work.js";
|
|
5
|
+
import { stageGraphScope } from "./stage-work-graph.js";
|
|
6
|
+
import { PreparedInteractiveWorkContext, readRetainedInteractiveWorkContext } from "./interactive-work-context.js";
|
|
7
|
+
import { interactiveWorkBatch } from "./interactive-work-batch.js";
|
|
8
|
+
import { prepareWorkBatch, addWorkBatch, readWorkBatchCustody } from "./work-registry.js";
|
|
9
|
+
import { PreparedInteractiveChecks } from "./interactive-checks.js";
|
|
10
|
+
const event = "interactive.work_registration";
|
|
11
|
+
function refuse(reason) {
|
|
12
|
+
throw new AppError("interactive_work_registration_invalid", "Interactive Work must belong to its retained Stage context and atomic graph", 2, { reason, effect: "no_effect" });
|
|
13
|
+
}
|
|
14
|
+
/** SQL-only assignment custody, usable after the Stage or workspace advanced.
|
|
15
|
+
* A launch owner must separately fence current Stage/input/physical state. */
|
|
16
|
+
export function readRegisteredInteractiveWork(context, scope, bundle, workId) {
|
|
17
|
+
const custody = readWorkBatchCustody(context, workId), bound = custody.scope;
|
|
18
|
+
if (bundle.manifest.flow_id !== "interactive" || bound.project_id !== scope.projectId || bound.run_id !== scope.runId
|
|
19
|
+
|| !["hardening", "review"].includes(bound.stage) || !bound.request_key.startsWith("interactive-"))
|
|
20
|
+
refuse("foreign_work_assignment");
|
|
21
|
+
const stage = bound.stage, requestId = bound.request_key.slice("interactive-".length);
|
|
22
|
+
const root = readDescriptorRootWork(context, scope, bundle);
|
|
23
|
+
const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type=? AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.stage')=? AND json_extract(payload_json,'$.attempt')=? AND json_extract(payload_json,'$.request_id')=? LIMIT 2", [scope.projectId, event, scope.runId, stage, bound.attempt, requestId]);
|
|
24
|
+
if (rows.length !== 1 || Buffer.byteLength(rows[0].payload_json) > 16 * 1024)
|
|
25
|
+
refuse("registration_custody_missing");
|
|
26
|
+
const receipt = JSON.parse(rows[0].payload_json);
|
|
27
|
+
if (!receipt || Object.keys(receipt).sort().join() !== "attempt,batch_sha256,bundle_sha256,context,parent_work_id,request_id,run_id,schema_id,stage,work_ids"
|
|
28
|
+
|| receipt.schema_id !== "dd-flow/interactive-work-registration@1" || receipt.run_id !== scope.runId || receipt.stage !== stage
|
|
29
|
+
|| receipt.attempt !== bound.attempt || receipt.request_id !== requestId || receipt.parent_work_id !== root.workId
|
|
30
|
+
|| bound.parent_work_id !== root.workId || receipt.bundle_sha256 !== bundle.sha256
|
|
31
|
+
|| hybridDigest(receipt.work_ids) !== hybridDigest(custody.work_ids))
|
|
32
|
+
refuse("registration_scope_changed");
|
|
33
|
+
const retained = readRetainedInteractiveWorkContext(context, { ...scope, stage, attempt: bound.attempt }, bundle, receipt.context);
|
|
34
|
+
const batch = interactiveWorkBatch(retained.record.input);
|
|
35
|
+
if (retained.record.input.owner_stage !== stage || custody.item_key !== retained.record.input.stage
|
|
36
|
+
|| receipt.work_ids[custody.item_key] !== workId || Object.keys(receipt.work_ids).join() !== custody.item_key
|
|
37
|
+
|| receipt.batch_sha256 !== hybridDigest(batch.works) || receipt.batch_sha256 !== custody.batch_sha256)
|
|
38
|
+
refuse("registration_batch_changed");
|
|
39
|
+
return { receipt, retained, packet: batch.works[0].payload.interactive, custody };
|
|
40
|
+
}
|
|
41
|
+
/** Registration replay returns original allocation metadata, not fresh launch authority. */
|
|
42
|
+
export function registerInteractiveStageGraph(context, input, assertHandlerCustody) {
|
|
43
|
+
const requestId = input.requestId ?? "initial";
|
|
44
|
+
const role = input.role ?? input.stage;
|
|
45
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/.test(requestId))
|
|
46
|
+
refuse("invalid_request_id");
|
|
47
|
+
const selected = readInteractiveStageScope(context, input), { scope, bundle } = selected;
|
|
48
|
+
const graph = stageGraphScope(context, scope.projectId, scope.runId, input.stage);
|
|
49
|
+
const root = readDescriptorRootWork(context, scope, bundle);
|
|
50
|
+
const requestKey = `interactive-${requestId}`;
|
|
51
|
+
const requestScope = { ...graph, stage: input.stage, parent_work_id: root.workId, request_key: requestKey };
|
|
52
|
+
const requestSha = hybridDigest(requestScope);
|
|
53
|
+
const read = () => {
|
|
54
|
+
const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type=? AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.stage')=? AND json_extract(payload_json,'$.attempt')=? AND json_extract(payload_json,'$.request_id')=? LIMIT 2", [scope.projectId, event, scope.runId, input.stage, graph.attempt, requestId]);
|
|
55
|
+
if (!rows.length) {
|
|
56
|
+
if (context.db.get("SELECT 1 FROM audit_events WHERE project_id=? AND event_type='work.batch_registration' AND json_extract(payload_json,'$.request_sha256')=?", [scope.projectId, requestSha])
|
|
57
|
+
|| context.db.get("SELECT 1 FROM works WHERE project_id=? AND run_id=? AND json_extract(payload_json,'$._dd_flow_batch_request.request_sha256')=?", [scope.projectId, scope.runId, requestSha]))
|
|
58
|
+
refuse("registration_receipt_missing");
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
if (rows.length !== 1 || Buffer.byteLength(rows[0].payload_json) > 16 * 1024)
|
|
62
|
+
refuse("ambiguous_registration");
|
|
63
|
+
const receipt = JSON.parse(rows[0].payload_json);
|
|
64
|
+
if (!receipt || Object.keys(receipt).sort().join() !== "attempt,batch_sha256,bundle_sha256,context,parent_work_id,request_id,run_id,schema_id,stage,work_ids"
|
|
65
|
+
|| receipt.schema_id !== "dd-flow/interactive-work-registration@1" || receipt.run_id !== scope.runId
|
|
66
|
+
|| receipt.stage !== input.stage || receipt.attempt !== graph.attempt || receipt.request_id !== requestId
|
|
67
|
+
|| receipt.parent_work_id !== root.workId || receipt.bundle_sha256 !== bundle.sha256)
|
|
68
|
+
refuse("registration_scope_changed");
|
|
69
|
+
const retained = readRetainedInteractiveWorkContext(context, { ...scope, stage: input.stage, attempt: graph.attempt }, bundle, receipt.context);
|
|
70
|
+
if (retained.record.input.stage !== role)
|
|
71
|
+
refuse("registration_role_changed");
|
|
72
|
+
const batch = interactiveWorkBatch(retained.record.input);
|
|
73
|
+
if (receipt.batch_sha256 !== hybridDigest(batch.works)
|
|
74
|
+
|| !receipt.work_ids || Object.keys(receipt.work_ids).sort().join() !== batch.works.map(item => item.key).sort().join()
|
|
75
|
+
|| new Set(Object.values(receipt.work_ids)).size !== batch.works.length)
|
|
76
|
+
refuse("registration_batch_changed");
|
|
77
|
+
for (const [key, id] of Object.entries(receipt.work_ids)) {
|
|
78
|
+
const custody = readWorkBatchCustody(context, id);
|
|
79
|
+
if (custody.item_key !== key || custody.batch_sha256 !== receipt.batch_sha256 || hybridDigest(custody.scope) !== requestSha
|
|
80
|
+
|| hybridDigest(custody.work_ids) !== hybridDigest(receipt.work_ids))
|
|
81
|
+
refuse("registration_work_changed");
|
|
82
|
+
}
|
|
83
|
+
return receipt;
|
|
84
|
+
};
|
|
85
|
+
const prior = context.db.readSnapshot(read);
|
|
86
|
+
if (prior)
|
|
87
|
+
return { ...prior, replayed: true };
|
|
88
|
+
const noOverlap = () => {
|
|
89
|
+
if (context.db.get("SELECT 1 FROM works WHERE project_id=? AND run_id=? AND parent_work_id=? AND status IN ('created','running','paused') AND json_extract(payload_json,'$.kind') IN ('interactive-hardening','interactive-review')", [scope.projectId, scope.runId, root.workId]))
|
|
90
|
+
refuse("active_previous_round");
|
|
91
|
+
};
|
|
92
|
+
noOverlap();
|
|
93
|
+
const checks = input.checks ?? (role === "review" ? PreparedInteractiveChecks.prepareReview(context, input) ?? undefined : undefined);
|
|
94
|
+
if (checks && !input.checks) {
|
|
95
|
+
checks.retain(context);
|
|
96
|
+
checks.evidence(context);
|
|
97
|
+
}
|
|
98
|
+
const frame = PreparedInteractiveWorkContext.prepare(context, { ...input, ...(checks ? { checks } : {}) }), metadata = frame.snapshot();
|
|
99
|
+
if (metadata.attempt !== graph.attempt)
|
|
100
|
+
refuse("stage_attempt_changed");
|
|
101
|
+
const batchInput = { parentWorkId: root.workId, stage: input.stage, requestKey, value: interactiveWorkBatch(frame.workInput()) };
|
|
102
|
+
const prepared = prepareWorkBatch(context, batchInput);
|
|
103
|
+
frame.revalidate();
|
|
104
|
+
return context.db.writeTransaction(() => {
|
|
105
|
+
const racing = read();
|
|
106
|
+
if (racing)
|
|
107
|
+
return { ...racing, replayed: true };
|
|
108
|
+
assertHandlerCustody?.(context);
|
|
109
|
+
frame.assertCustody(context);
|
|
110
|
+
noOverlap();
|
|
111
|
+
if (readDescriptorRootWork(context, scope, bundle).workId !== root.workId)
|
|
112
|
+
refuse("root_changed");
|
|
113
|
+
frame.retainPreparedSql(context);
|
|
114
|
+
const allocated = addWorkBatch(context, { ...batchInput, prepared });
|
|
115
|
+
const receipt = { schema_id: "dd-flow/interactive-work-registration@1", run_id: scope.runId, stage: input.stage,
|
|
116
|
+
attempt: graph.attempt, request_id: requestId, parent_work_id: root.workId, bundle_sha256: bundle.sha256,
|
|
117
|
+
context: metadata.reference, batch_sha256: hybridDigest(prepared), work_ids: allocated.work_ids };
|
|
118
|
+
context.db.run("INSERT INTO audit_events(project_id,event_type,payload_json,created_at) VALUES(?,?,?,?)", [scope.projectId, event, JSON.stringify(receipt), context.now()]);
|
|
119
|
+
const verified = read();
|
|
120
|
+
if (!verified)
|
|
121
|
+
refuse("registration_not_retained");
|
|
122
|
+
return { ...verified, replayed: false };
|
|
123
|
+
});
|
|
124
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
2
|
+
import { AppError } from "../shared/errors.js";
|
|
3
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
4
|
+
import { interactiveWorkResultSchema } from "./interactive-work-batch.js";
|
|
5
|
+
const issueKey = (workId, issueId) => JSON.stringify([workId, issueId]);
|
|
6
|
+
/** Only independently settled review decisions close earlier items. Author
|
|
7
|
+
* decisions are proposals. Null issue_id tracks an unfinished Work outcome,
|
|
8
|
+
* not an invented product finding. Failed/cancelled attempts remain explicit. */
|
|
9
|
+
export function pendingInteractiveIssues(history, binding) {
|
|
10
|
+
const pending = new Map();
|
|
11
|
+
for (const work of history.works) {
|
|
12
|
+
if (work.status !== "completed" || typeof work.result === "string") {
|
|
13
|
+
pending.set(issueKey(work.work_id, null), { work_id: work.work_id, issue_id: null, severity: "major", summary: String(work.result) });
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
const result = work.result;
|
|
17
|
+
if (work.stage === "review" && work.review_binding?.file_changes_sha256 === binding.file_changes_sha256
|
|
18
|
+
&& work.review_binding.user_inputs_sha256 === binding.user_inputs_sha256 && work.review_binding.scope_revision === binding.scope_revision)
|
|
19
|
+
for (const decision of result.issue_decisions ?? [])
|
|
20
|
+
if (decision.outcome !== "still_open")
|
|
21
|
+
pending.delete(issueKey(decision.work_id, decision.issue_id));
|
|
22
|
+
for (const issue of result.issues)
|
|
23
|
+
pending.set(issueKey(work.work_id, issue.id), {
|
|
24
|
+
work_id: work.work_id, issue_id: issue.id, severity: issue.severity, summary: issue.summary
|
|
25
|
+
});
|
|
26
|
+
if (result.verdict !== "ready" && !result.issues.length && !(result.issue_decisions ?? []).some(decision => decision.outcome === "still_open"))
|
|
27
|
+
pending.set(issueKey(work.work_id, null), { work_id: work.work_id, issue_id: null, severity: "major", summary: result.summary });
|
|
28
|
+
}
|
|
29
|
+
return [...pending.values()];
|
|
30
|
+
}
|
|
31
|
+
function refuse(reason) {
|
|
32
|
+
throw new AppError("interactive_work_result_invalid", "Interactive Work requires its exact assignment, observed changes and checks", 2, { reason, effect: "no_effect", ...(reason === "invalid_schema" ? { phase: "prepare", recoverable: true } : {}) });
|
|
33
|
+
}
|
|
34
|
+
/** Proposal checks only. Physical source/check/Session custody must be prepared
|
|
35
|
+
* independently and fenced again in Work completion and Stage settlement. */
|
|
36
|
+
export function validateInteractiveWorkResult(bytes, schemaBytes, binding) {
|
|
37
|
+
if (!bytes.length || bytes.length > 1024 * 1024 || !schemaBytes.length || schemaBytes.length > 1024 * 1024)
|
|
38
|
+
refuse("result_exceeds_bound");
|
|
39
|
+
let result;
|
|
40
|
+
try {
|
|
41
|
+
result = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return refuse("invalid_schema");
|
|
45
|
+
}
|
|
46
|
+
const validate = (() => {
|
|
47
|
+
try {
|
|
48
|
+
const schema = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(schemaBytes));
|
|
49
|
+
if (schema.$id !== interactiveWorkResultSchema)
|
|
50
|
+
refuse("frozen_schema_invalid");
|
|
51
|
+
return new Ajv2020({ strict: true, validateFormats: false }).compile(schema);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return refuse("frozen_schema_invalid");
|
|
55
|
+
}
|
|
56
|
+
})();
|
|
57
|
+
if (!validate(result) || result.schema_id !== interactiveWorkResultSchema || !result.summary.trim()
|
|
58
|
+
|| result.limitations.some(item => !item.trim()))
|
|
59
|
+
refuse("invalid_schema");
|
|
60
|
+
const packet = binding.packet;
|
|
61
|
+
if (result.run_id !== packet.run_id || result.work_id !== binding.workId || result.stage !== packet.stage
|
|
62
|
+
|| result.packet_sha256 !== packet.packet_sha256)
|
|
63
|
+
refuse("foreign_work_binding");
|
|
64
|
+
if (hybridDigest(result.changes) !== hybridDigest(binding.actualChanges))
|
|
65
|
+
refuse("unobserved_change_claim");
|
|
66
|
+
const paths = result.changes.map(item => item.path);
|
|
67
|
+
if (new Set(paths).size !== paths.length || result.changes.some(item => !packet.write_paths.includes(item.path)
|
|
68
|
+
|| item.before_sha256 === item.after_sha256 && item.before_mode === item.after_mode
|
|
69
|
+
|| Object.hasOwn(item, "before_mode") && ((item.before_sha256 === null) !== (item.before_mode === null)
|
|
70
|
+
|| (item.after_sha256 === null) !== (item.after_mode === null))))
|
|
71
|
+
refuse("change_outside_write_scope");
|
|
72
|
+
if (packet.stage === "review" && (result.changes.length || packet.write_paths.length))
|
|
73
|
+
refuse("review_changed_sources");
|
|
74
|
+
if (hybridDigest(result.checks) !== hybridDigest(binding.checks))
|
|
75
|
+
refuse("unowned_check_claim");
|
|
76
|
+
const sources = new Map(packet.read_inputs.map(ref => [`${ref.root}:${ref.path}`, ref.sha256]));
|
|
77
|
+
for (const change of result.changes)
|
|
78
|
+
sources.delete(`workspace:${change.path}`);
|
|
79
|
+
for (const ref of [...binding.observedSources, ...binding.checks]) {
|
|
80
|
+
const key = `${ref.root}:${ref.path}`;
|
|
81
|
+
// Changed workspace inputs are superseded only by independently observed
|
|
82
|
+
// post-Work sources. Agent references never extend this allowed evidence.
|
|
83
|
+
const change = ref.root === "workspace" ? result.changes.find(item => item.path === ref.path) : null;
|
|
84
|
+
if (change && change.after_sha256 !== ref.sha256)
|
|
85
|
+
refuse("stale_observed_output");
|
|
86
|
+
if (ref.root === "run" && sources.has(key) && sources.get(key) !== ref.sha256)
|
|
87
|
+
refuse("conflicting_run_evidence");
|
|
88
|
+
sources.set(key, ref.sha256);
|
|
89
|
+
}
|
|
90
|
+
const ids = new Set();
|
|
91
|
+
for (const issue of result.issues) {
|
|
92
|
+
if (!issue.id.trim() || !issue.summary.trim() || ids.has(issue.id))
|
|
93
|
+
refuse("invalid_issue");
|
|
94
|
+
ids.add(issue.id);
|
|
95
|
+
if (issue.evidence.some(ref => sources.get(`${ref.root}:${ref.path}`) !== ref.sha256))
|
|
96
|
+
refuse("foreign_issue_evidence");
|
|
97
|
+
}
|
|
98
|
+
const pending = new Map((binding.priorIssues ?? []).map(issue => [issueKey(issue.work_id, issue.issue_id), issue]));
|
|
99
|
+
const decisions = result.issue_decisions ?? [], decided = new Set();
|
|
100
|
+
if (decisions.length !== pending.size)
|
|
101
|
+
refuse("incomplete_prior_issue_decisions");
|
|
102
|
+
for (const decision of decisions) {
|
|
103
|
+
const key = issueKey(decision.work_id, decision.issue_id), prior = pending.get(key);
|
|
104
|
+
if (!prior || decided.has(key) || !decision.summary.trim())
|
|
105
|
+
refuse("foreign_or_duplicate_issue_decision");
|
|
106
|
+
if (decision.evidence.some(ref => sources.get(`${ref.root}:${ref.path}`) !== ref.sha256))
|
|
107
|
+
refuse("foreign_issue_decision_evidence");
|
|
108
|
+
if (result.verdict === "ready" && decision.outcome === "still_open" && prior.severity !== "minor")
|
|
109
|
+
refuse("unsupported_ready_claim");
|
|
110
|
+
decided.add(key);
|
|
111
|
+
}
|
|
112
|
+
if (result.verdict === "ready" && (!binding.checksSatisfied || result.issues.some(issue => issue.severity !== "minor")))
|
|
113
|
+
refuse("unsupported_ready_claim");
|
|
114
|
+
return result;
|
|
115
|
+
}
|