@deksden-com/dd-flow-cli 0.9.0-beta.145 → 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 +71 -0
- package/dist/build-info.json +7 -7
- package/dist/cli/command-inputs.js +6 -2
- package/dist/cli/command-metadata.js +18 -2
- package/dist/cli/diagnostic-invocation.js +46 -0
- package/dist/cli/diagnostic.js +120 -0
- package/dist/cli/help.js +35 -0
- package/dist/cli/memory-flow-admission.js +4 -2
- package/dist/cli/memory-init.js +5 -2
- package/dist/cli/run-cli.js +16 -15
- package/dist/cli.js +6 -0
- package/dist/domain/diagnostic-aspects.js +103 -0
- package/dist/domain/diagnostic-flows.js +8 -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 +306 -34
- 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 +70 -28
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +3 -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/driver-recovery.mjs +9 -8
- package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -1
- package/dist/harness-runtime/lib/managed-daemon.mjs +11 -3
- 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-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/provider-overload-policy.d.mts +1 -1
- package/dist/harness-runtime/lib/provider-overload-policy.mjs +1 -1
- 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/project-flow-pack-manifest.schema.json +1 -1
- package/dist/services/check-environment.js +21 -0
- package/dist/services/check-runner-evidence.js +267 -15
- package/dist/services/code-checks.js +23 -15
- package/dist/services/controller-execution.js +10 -4
- package/dist/services/controller-fanout.js +13 -6
- package/dist/services/controller-memory-worker.js +3 -2
- package/dist/services/dashboard.js +2 -2
- package/dist/services/descriptor-run-settlement.js +5 -3
- package/dist/services/descriptor-run-start.js +22 -3
- package/dist/services/descriptor-stage-coordinator.js +37 -4
- package/dist/services/descriptor-stage-settlement.js +2 -1
- 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/eval-snapshots.js +85 -29
- package/dist/services/execution-bundles.js +19 -7
- package/dist/services/flow-catalog.js +6 -1
- package/dist/services/frozen-engine.js +37 -2
- package/dist/services/hybrid-contracts.js +2 -0
- package/dist/services/installed-flow-source.js +25 -3
- package/dist/services/lifecycle-invocations.js +147 -34
- package/dist/services/memory-init-contexts.js +16 -0
- package/dist/services/memory-init-controller-start.js +12 -4
- package/dist/services/memory-init-lint-capability.js +5 -5
- package/dist/services/memory-init-native-capacity.js +4 -4
- package/dist/services/memory-init-preflight.js +10 -16
- package/dist/services/memory-init-worktrunk-capability.js +3 -7
- package/dist/services/memory-layer.js +4 -3
- package/dist/services/memory-tool-probe.js +25 -0
- package/dist/services/memory-verification-checks.js +15 -4
- package/dist/services/native-daemon-history.js +2 -1
- package/dist/services/provider-capacity.js +43 -38
- package/dist/services/release-cache.js +8 -0
- package/dist/services/run-control.js +2 -1
- package/dist/services/run-controller-adapter.js +2 -1
- package/dist/services/run-controller-process.js +2 -1
- package/dist/services/run-controller-recovery.js +2 -1
- package/dist/services/run-controller.js +85 -15
- package/dist/services/run-fork.js +1 -1
- 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-service.js +2 -1
- package/dist/services/stage-handlers.js +16 -3
- package/dist/services/stage-pause.js +4 -1
- package/dist/services/version-status.js +1 -1
- package/dist/services/vnext-code.js +2 -2
- package/dist/services/vnext-fanout.js +31 -1
- package/dist/services/work-registry.js +37 -6
- package/dist/services/workspace-bootstrap.js +2 -1
- package/package.json +6 -3
|
@@ -12,7 +12,8 @@ import { requireProjectByRoot } from "./projects.js";
|
|
|
12
12
|
import { stageAttempt, stageWorkIds } from "./stage-work-graph.js";
|
|
13
13
|
import { runtimeCommand } from "./runtime-command.js";
|
|
14
14
|
import { promptDeliveryInstruction } from "./prompt-delivery.js";
|
|
15
|
-
|
|
15
|
+
import { currentStageFinishCommand } from "./lifecycle-invocations.js";
|
|
16
|
+
function semanticCompletionPrompt(stage, completed, command) {
|
|
16
17
|
const task = stage === "plan-review"
|
|
17
18
|
? "Classify the reviewer findings, apply every accepted correction in the current PLAN-REVIEW Work, and write the authoritative review decision."
|
|
18
19
|
: stage === "code-review"
|
|
@@ -21,7 +22,7 @@ function semanticCompletionPrompt(stage, completed) {
|
|
|
21
22
|
return [
|
|
22
23
|
`All declared ${stage} child Work has settled (${completed} completed).`,
|
|
23
24
|
task,
|
|
24
|
-
|
|
25
|
+
`Then invoke this exact currently issued Stage finish command:\n\n${command}\n\nRefresh the authoritative verification evidence first; an earlier rejected or successful nonterminal command only reads its retained outcome.`,
|
|
25
26
|
"If that command returns either outcome = repair_required with a repair Work ID, or error.details.continuation.kind = repair_required, the repair Work is already registered. End this Turn so the controller can dispatch it. Otherwise follow the command's actual result.",
|
|
26
27
|
"Do not create speculative repair Work or start a successor Stage."
|
|
27
28
|
].join(" ");
|
|
@@ -37,10 +38,13 @@ export function controllerStageEntryPrompt(stage, command) {
|
|
|
37
38
|
stageLifecycleInstruction
|
|
38
39
|
].join("\n\n");
|
|
39
40
|
}
|
|
40
|
-
export function controllerStageResumePrompt(
|
|
41
|
+
export function controllerStageResumePrompt(packet, resumedPromptPath) {
|
|
41
42
|
return [
|
|
42
|
-
`The
|
|
43
|
-
|
|
43
|
+
`The accepted canonical answer for pause ${packet.pause_id} (${packet.stage}, ${packet.work_id}) is retained at this absolute read-only path: ${JSON.stringify(packet.answer_file)} (sha256 ${packet.answer_sha256}). Do not replace it with a placeholder or ask the user to supply it again. A null pause.answer_path before resume does not mean the answer is missing.`,
|
|
44
|
+
resumedPromptPath
|
|
45
|
+
? `This exact answer was already committed. Do not invoke stage resume again. Read the recovered authoritative continuation at ${JSON.stringify(resumedPromptPath)} and perform its current Stage instructions.`
|
|
46
|
+
: `Only while this exact pause still awaits its answer, your first tool call must be this exact standalone command. It applies the already accepted bound answer file:\n\n${packet.command}`,
|
|
47
|
+
...(packet.resume_source_path ? [`If this pause has already resumed, do not invoke stage resume again. Read the complete recovered result.worker_prompt_markdown in ${JSON.stringify(packet.resume_source_path)} instead, and follow that authoritative continuation. The controller restores this committed packet before dispatch. A different pause requires its own accepted answer.`] : []),
|
|
44
48
|
`Do not alter the answer. ${promptDeliveryInstruction} Follow the returned packet, then stop at this Stage boundary.`,
|
|
45
49
|
stageLifecycleInstruction
|
|
46
50
|
].join("\n\n");
|
|
@@ -205,7 +209,10 @@ export async function nextControllerFanout(context, input) {
|
|
|
205
209
|
].join("\n\n"));
|
|
206
210
|
}
|
|
207
211
|
}
|
|
208
|
-
|
|
212
|
+
const finish = currentStageFinishCommand(context, input.stage);
|
|
213
|
+
if (!finish)
|
|
214
|
+
throw new AppError("controller_finish_authority_missing", "Settled child Work has no current issued Stage finish; reconcile lifecycle authority", 1, { stage: input.stage });
|
|
215
|
+
return prompt("semantic_completion", semanticCompletionPrompt(input.stage, fanout.works.completed, finish));
|
|
209
216
|
}
|
|
210
217
|
/** Shared pinned runtime normalization; preserve structured errors at the TS boundary. */
|
|
211
218
|
export function controllerNativeChildren(receipt, rootSessionId) {
|
|
@@ -6,6 +6,7 @@ import { launchExternalWork } from "./external-work-launch.js";
|
|
|
6
6
|
import { requireProjectByRoot } from "./projects.js";
|
|
7
7
|
import { assertControllerLeaseAllowed } from "./run-controller-process.js";
|
|
8
8
|
import { assertManagedProcessLease } from "./managed-processes.js";
|
|
9
|
+
import { isDiagnosticFlow } from "../domain/diagnostic-flows.js";
|
|
9
10
|
if (!parentPort)
|
|
10
11
|
throw new AppError("controller_memory_task_invalid", "Memory worker requires its controller thread", 1);
|
|
11
12
|
const port = parentPort, cancellation = new AbortController();
|
|
@@ -18,8 +19,8 @@ try {
|
|
|
18
19
|
context = createContext(env, "prepared_existing");
|
|
19
20
|
const project = requireProjectByRoot(context, task.input.projectRoot);
|
|
20
21
|
const run = context.db.get("SELECT flow_kind FROM runs WHERE project_id=? AND id=?", [project.id, task.input.runId]);
|
|
21
|
-
if (!run || !["mb-init", "mb-upgrade"].includes(run.flow_kind))
|
|
22
|
-
throw new AppError("controller_memory_task_invalid", "
|
|
22
|
+
if (!run || !["mb-init", "mb-upgrade"].includes(run.flow_kind) && !isDiagnosticFlow(run.flow_kind))
|
|
23
|
+
throw new AppError("controller_memory_task_invalid", "Worker requires the selected memory or diagnostic RUN", 1, { effect: "no_effect" });
|
|
23
24
|
if (task.operation === "external") {
|
|
24
25
|
const work = context.db.get("SELECT project_id,run_id FROM works WHERE work_id=?", [task.input.workId]);
|
|
25
26
|
if (!work || work.project_id !== project.id || work.run_id !== task.input.runId)
|
|
@@ -216,10 +216,10 @@ export function autoRefreshDashboards(context, input) {
|
|
|
216
216
|
if (projectRecoveryGuarded(context, project.id))
|
|
217
217
|
return { ok: true, skipped: true, reason: "run_recovery_guarded" };
|
|
218
218
|
const configured = readProjectConfig(context, project.id);
|
|
219
|
-
// Automatic observation must not change
|
|
219
|
+
// Automatic observation must not change a memory or diagnostic source baseline.
|
|
220
220
|
// Keep projections in Home; do not hide runtime-created paths in Git or
|
|
221
221
|
// weaken source preservation by excluding user-owned .tasks directories.
|
|
222
|
-
const initializing = context.db.get("SELECT 1 FROM runs WHERE project_id=? AND flow_kind IN ('mb-init','mb-upgrade') AND (id=? OR status IN ('created','running','paused','waiting_for_user','blocked')) LIMIT 1", [project.id, input.runId ?? null]);
|
|
222
|
+
const initializing = context.db.get("SELECT 1 FROM runs WHERE project_id=? AND flow_kind IN ('mb-init','mb-upgrade','mb-audit','project-review') AND (id=? OR status IN ('created','running','paused','waiting_for_user','blocked')) LIMIT 1", [project.id, input.runId ?? null]);
|
|
223
223
|
const config = initializing ? { ...configured, dashboard: { ...configured.dashboard,
|
|
224
224
|
markdown_path: path.join(projectDashboardDir(context.ddFlowHome, project.id), "project-dashboard.md") } } : configured;
|
|
225
225
|
if (!config.dashboard.auto_refresh) {
|
|
@@ -62,17 +62,19 @@ export class PreparedDescriptorRunSettlement {
|
|
|
62
62
|
const scope = { projectId: project.id, runId: input.runId, stage: input.stage };
|
|
63
63
|
const matchKeys = entityReferenceVariants(input.runId).map(reference => stageLifecycleMatchKey("stage_finish", reference, input.stage, input.projectRoot));
|
|
64
64
|
const snapshot = context.db.readSnapshot(() => ({ owner: selected(context, scope, bundle, matchKeys), generation: assertRunMutationAllowed(context, scope.projectId, scope.runId) }));
|
|
65
|
-
const
|
|
65
|
+
const operational = assertPreparedRunBundleSqlAuthority(context, scope, bundle);
|
|
66
|
+
const git = snapshot.owner.prior?.git ?? gitFacts(operational.workspace_root ?? operational.project_root);
|
|
66
67
|
return new PreparedDescriptorRunSettlement(scope, bundle, snapshot.owner, git, snapshot.generation, matchKeys);
|
|
67
68
|
}
|
|
68
69
|
apply(context, assertHandlerCustody) {
|
|
69
70
|
if (typeof assertHandlerCustody !== "function")
|
|
70
71
|
refuse("handler_fence_required");
|
|
71
|
-
const
|
|
72
|
+
const operational = assertPreparedRunBundleSqlAuthority(context, this.#scope, this.#bundle);
|
|
73
|
+
const bundle = readFlowRunExecutionBundle(context, { projectRoot: operational.project_root, runId: this.#scope.runId });
|
|
72
74
|
if (!bundle || bundle.sha256 !== this.#bundle.sha256)
|
|
73
75
|
refuse("bundle_changed");
|
|
74
76
|
const observed = context.db.readSnapshot(() => selected(context, this.#scope, this.#bundle, this.#matchKeys));
|
|
75
|
-
if (!observed.prior && hybridDigest(gitFacts(
|
|
77
|
+
if (!observed.prior && hybridDigest(gitFacts(operational.workspace_root ?? operational.project_root)) !== hybridDigest(this.#git))
|
|
76
78
|
refuse("git_observation_changed");
|
|
77
79
|
const receipt = { run_id: this.#scope.runId, work_id: this.#owner.rootId, binding: this.#owner.binding, git: this.#git };
|
|
78
80
|
const changed = withStageSettlement(context, () => {
|
|
@@ -20,6 +20,8 @@ import { allocateDescriptorRootWork, readDescriptorRootWork } from "./descriptor
|
|
|
20
20
|
import { nextRunId } from "./ids.js";
|
|
21
21
|
import { readMemoryInitRunAllocation } from "./memory-init-run-allocation.js";
|
|
22
22
|
import { readMemoryUpgradeEvidence, readMemoryUpgradeTargetEvidence, upgradeEvidenceFilePath, upgradeEvidenceIndexPath, upgradeTargetIndexPath } from "./memory-upgrade-evidence.js";
|
|
23
|
+
import { isDiagnosticFlow } from "../domain/diagnostic-flows.js";
|
|
24
|
+
import { readDiagnosticSnapshot, readDiagnosticSnapshotCustody } from "./diagnostic-snapshot.js";
|
|
23
25
|
const encode = (value) => Buffer.from(JSON.stringify(value));
|
|
24
26
|
const owner = (directory) => {
|
|
25
27
|
const stat = fs.lstatSync(directory);
|
|
@@ -91,6 +93,13 @@ function memoryBootstrap(context, projectId, bundle) {
|
|
|
91
93
|
if (bundle.descriptor.descriptor.id === "sdlc")
|
|
92
94
|
return;
|
|
93
95
|
const scope = bundle.execution.snapshot().scope;
|
|
96
|
+
if (isDiagnosticFlow(bundle.manifest.flow_id)) {
|
|
97
|
+
const snapshot = readDiagnosticSnapshotCustody(context, { projectId, projectRoot: scope.projectRoot,
|
|
98
|
+
sourceRoot: scope.projectRoot, requestId: scope.requestId, flowId: bundle.manifest.flow_id });
|
|
99
|
+
if (!snapshot || snapshot.root !== scope.workspace || bundle.manifest.bootstrap_receipts.length !== 0)
|
|
100
|
+
refuseRunStart("diagnostic_snapshot_authority_changed");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
94
103
|
if (bundle.descriptor.descriptor.id === "mb-init") {
|
|
95
104
|
const retained = readMemoryInitBootstrapReadiness(context, { requestId: scope.requestId, projectRoot: scope.projectRoot, workspaceRoot: scope.workspace });
|
|
96
105
|
const reference = bundle.manifest.bootstrap_receipts[0];
|
|
@@ -113,6 +122,16 @@ function memoryBootstrap(context, projectId, bundle) {
|
|
|
113
122
|
|| !bundle.blob(reference.path).equals(encode(retained.authority.receipt)))
|
|
114
123
|
refuseRunStart("memory_bootstrap_authority_changed");
|
|
115
124
|
}
|
|
125
|
+
function observeRunWorkspace(context, bundle, projectId) {
|
|
126
|
+
const scope = bundle.execution.snapshot().scope;
|
|
127
|
+
if (!isDiagnosticFlow(bundle.manifest.flow_id))
|
|
128
|
+
return observeDescriptorWorkspace(context, scope.projectRoot, scope.workspace);
|
|
129
|
+
const snapshot = readDiagnosticSnapshot(context, { projectId, projectRoot: scope.projectRoot,
|
|
130
|
+
sourceRoot: scope.projectRoot, requestId: scope.requestId, flowId: bundle.manifest.flow_id });
|
|
131
|
+
if (!snapshot || snapshot.root !== scope.workspace)
|
|
132
|
+
refuseRunStart("diagnostic_snapshot_authority_changed");
|
|
133
|
+
return observeDescriptorWorkspace(context, snapshot.root, snapshot.root);
|
|
134
|
+
}
|
|
116
135
|
/** RUN allocation and SQL bundle authority are one request-owned transaction.
|
|
117
136
|
* Physical publication is separate, replayable and gates lifecycle writes.
|
|
118
137
|
* This does not register missing handlers or qualify a provider/storage route.
|
|
@@ -164,11 +183,11 @@ export class PreparedDescriptorRunStart {
|
|
|
164
183
|
const home = fs.realpathSync(context.ddFlowHome), runRoot = projectRunHome(home, selected.id, bundle.manifest.run_id);
|
|
165
184
|
if (fs.existsSync(runRoot))
|
|
166
185
|
refuseRunStart("foreign_run_root");
|
|
167
|
-
const facts =
|
|
186
|
+
const facts = observeRunWorkspace(context, bundle, selected.id), now = context.now(), artifacts = [
|
|
168
187
|
{ path: "engine-binding.json", base64: bundle.blob(bundle.manifest.engine_binding.path).toString("base64") }
|
|
169
188
|
];
|
|
170
189
|
const isSdlc = bundle.descriptor.descriptor.id === "sdlc", flowKind = isSdlc ? "vnext_specify" : bundle.descriptor.descriptor.id;
|
|
171
|
-
if (!["vnext_specify", "mb-init", "mb-upgrade"].includes(flowKind))
|
|
190
|
+
if (!["vnext_specify", "mb-init", "mb-upgrade"].includes(flowKind) && !isDiagnosticFlow(flowKind))
|
|
172
191
|
refuseRunStart("unsupported_flow_kind");
|
|
173
192
|
if (flowKind === "mb-upgrade") {
|
|
174
193
|
const upgradeScope = { requestId: scope.requestId, projectRoot: scope.projectRoot, workspaceRoot: scope.workspace };
|
|
@@ -250,7 +269,7 @@ export class PreparedDescriptorRunStart {
|
|
|
250
269
|
revalidatePhysical(context) {
|
|
251
270
|
this.revalidateDirectoryOwners(context);
|
|
252
271
|
const p = this.#payload;
|
|
253
|
-
if (hybridDigest(
|
|
272
|
+
if (hybridDigest(observeRunWorkspace(context, this.#bundle, p.project_id)) !== hybridDigest(p.git))
|
|
254
273
|
refuseRunStart("physical_scope_changed");
|
|
255
274
|
this.#bundle.engine();
|
|
256
275
|
}
|
|
@@ -8,11 +8,14 @@ import { hybridDigest } from "./hybrid-contracts.js";
|
|
|
8
8
|
import { readDescriptorRootWork } from "./descriptor-root-work.js";
|
|
9
9
|
import { bindStageWork, stageWorkIds } from "./stage-work-graph.js";
|
|
10
10
|
import { appendAudit } from "./audit.js";
|
|
11
|
-
import { acceptStagePacket, prepareStagePacket, publishStagePacket } from "./stage-report-renderer.js";
|
|
11
|
+
import { acceptStagePacket, prepareStagePacket, publishStagePacket, resolveStagePublicationPath } from "./stage-report-renderer.js";
|
|
12
12
|
import { assertRunMutationAllowed } from "./run-recovery-guard.js";
|
|
13
13
|
import { assertStageSettlementAllowed, PreparedRecoveryCoordinatorRead } from "./run-recovery.js";
|
|
14
14
|
import { loadExternalStageContextBytes, renderExternalStageContext } from "./stage-context.js";
|
|
15
15
|
import { runtimeCommand } from "./runtime-command.js";
|
|
16
|
+
import { isDiagnosticFlow } from "../domain/diagnostic-flows.js";
|
|
17
|
+
import { diagnosticAspects } from "../domain/diagnostic-aspects.js";
|
|
18
|
+
import { readDiagnosticSnapshot } from "./diagnostic-snapshot.js";
|
|
16
19
|
const event = "flow_run.descriptor_coordinator";
|
|
17
20
|
const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
18
21
|
function refuse(reason) {
|
|
@@ -45,7 +48,18 @@ function readDescriptorStageInputCustody(context, scope, bundle, run, settlement
|
|
|
45
48
|
|| typeof stage.attempt !== "string" || typeof stage.input_sha256 !== "string")
|
|
46
49
|
refuse("inactive_stage");
|
|
47
50
|
const file = path.join(run.run_root, declared.directory, "stage-input.json");
|
|
48
|
-
|
|
51
|
+
// Imported receipts retain their original locators and exact input bytes.
|
|
52
|
+
// Match their current publication through trusted RUN lineage, as accepted
|
|
53
|
+
// result readers do, rather than requiring rewritten historical SQL.
|
|
54
|
+
const candidates = 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')=? LIMIT 33", [scope.projectId, scope.runId, scope.stage, stage.attempt]);
|
|
55
|
+
if (candidates.length > 32)
|
|
56
|
+
refuse("input_custody_missing");
|
|
57
|
+
const rows = candidates.filter(row => {
|
|
58
|
+
if (Buffer.byteLength(row.payload_json) > 8 * 1024 * 1024)
|
|
59
|
+
refuse("input_custody_changed");
|
|
60
|
+
const candidate = JSON.parse(row.payload_json);
|
|
61
|
+
return typeof candidate?.file === "string" && resolveStagePublicationPath(context, scope.projectId, scope.runId, candidate.file) === file;
|
|
62
|
+
});
|
|
49
63
|
if (rows.length !== 1 || Buffer.byteLength(rows[0].payload_json) > 2 * 1024 * 1024)
|
|
50
64
|
refuse("input_custody_missing");
|
|
51
65
|
const packet = JSON.parse(rows[0].payload_json), content = Buffer.from(packet.base64, "base64");
|
|
@@ -58,7 +72,8 @@ function readDescriptorStageInputCustody(context, scope, bundle, run, settlement
|
|
|
58
72
|
|| input.descriptor_sha256 !== bundle.descriptor.sha256 || input.execution_sha256 !== bundle.execution.sha256)
|
|
59
73
|
refuse("foreign_input");
|
|
60
74
|
const starts = context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type='flow_run.descriptor_stage_start' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.stage')=? LIMIT 2", [scope.projectId, scope.runId, scope.stage]);
|
|
61
|
-
|
|
75
|
+
const start = starts.length === 1 ? JSON.parse(starts[0].payload_json) : null;
|
|
76
|
+
if (!start || typeof start.file !== "string" || hybridDigest({ ...start, file: resolveStagePublicationPath(context, scope.projectId, scope.runId, start.file) }) !== hybridDigest({ schema_id: "dd-flow/descriptor-stage-start@1", run_id: scope.runId, stage: scope.stage, attempt: stage.attempt, bundle_sha256: bundle.sha256, input_sha256: stage.input_sha256, file }))
|
|
62
77
|
refuse("start_custody_missing");
|
|
63
78
|
return { runRoot: run.run_root, attempt: stage.attempt, inputSha256: stage.input_sha256, input };
|
|
64
79
|
}
|
|
@@ -88,10 +103,16 @@ export class PreparedDescriptorStageCoordinator {
|
|
|
88
103
|
const operational = assertPreparedRunBundleSqlAuthority(context, scope, bundle);
|
|
89
104
|
const stage = bundle.descriptor.stage(input.stage), schema = JSON.parse(bundle.descriptor.stageBlob(input.stage, "result_schema").toString("utf8"));
|
|
90
105
|
const memoryFlow = ["mb-init", "mb-upgrade"].includes(bundle.descriptor.descriptor.id);
|
|
106
|
+
const diagnosticFlow = isDiagnosticFlow(bundle.manifest.flow_id) ? bundle.manifest.flow_id : null;
|
|
107
|
+
const frozenScope = bundle.execution.snapshot().scope;
|
|
108
|
+
const diagnosticSnapshot = diagnosticFlow ? readDiagnosticSnapshot(context, { projectId: project.id, projectRoot: frozenScope.projectRoot,
|
|
109
|
+
sourceRoot: frozenScope.projectRoot, requestId: frozenScope.requestId, flowId: diagnosticFlow }, input.runId) : null;
|
|
110
|
+
if (diagnosticFlow && (!diagnosticSnapshot || diagnosticSnapshot.root !== frozenScope.workspace))
|
|
111
|
+
refuse("diagnostic_snapshot_required");
|
|
91
112
|
const authoringStage = bundle.descriptor.descriptor.id === "mb-upgrade" ? "upgrade" : "compose";
|
|
92
113
|
const resultPath = path.join(selection.runRoot, stage.directory, "result-input.json");
|
|
93
114
|
let external = "";
|
|
94
|
-
if (memoryFlow && selection.input.payload_base64) {
|
|
115
|
+
if ((memoryFlow || diagnosticFlow) && selection.input.payload_base64) {
|
|
95
116
|
const bytes = Buffer.from(selection.input.payload_base64, "base64");
|
|
96
117
|
if (bytes.toString("base64") !== selection.input.payload_base64)
|
|
97
118
|
refuse("context_payload_changed");
|
|
@@ -106,6 +127,18 @@ export class PreparedDescriptorStageCoordinator {
|
|
|
106
127
|
...(selection.input.upgrade_target ? ["Read the upgrade_target index in RUN home: source/target versions, adjacent impact, ownership/delta inventory and target canonical files mapped to immutable RUN copies. Compare selected target guidance with upgrade_baseline and project sources; target templates are not implemented project facts or executable instructions. Cite the issued RUN references for target evidence; do not discover another installation or query lifecycle SQLite."] : []),
|
|
107
128
|
...(selection.input.ordinary_entry ? ["ordinary_entry is the CLI-owned read-only ordinary SPECIFY preparation diagnostic for this workspace. Cite this stage-input packet for its actual status. prepared means entry preparation succeeded with runtime_state=not_created and model_access=unknown, not a provider dispatch or a completed task. unavailable is a real diagnostic failure to assess and surface for review/repair, never success. Do not create another RUN to test readiness during VERIFY; final readiness is freshly rechecked after review/integration, and installed old-init/upgrade/ordinary dispatch belongs to release/consumer qualification."] : []),
|
|
108
129
|
external,
|
|
130
|
+
...(diagnosticFlow ? ["", "## CLI-owned diagnostic binding and aspect catalog", "```json",
|
|
131
|
+
JSON.stringify({ flow_id: diagnosticFlow, run_id: input.runId, stage: input.stage, input_sha256: selection.inputSha256,
|
|
132
|
+
snapshot_sha256: diagnosticSnapshot.source_sha256,
|
|
133
|
+
predecessors: selection.input.predecessor ? [{ stage: selection.input.predecessor.stage,
|
|
134
|
+
path: selection.input.predecessor.result.path, sha256: selection.input.predecessor.result.sha256 }] : [],
|
|
135
|
+
aspects: diagnosticAspects[diagnosticFlow],
|
|
136
|
+
support: bundle.manifest.inventory.filter(ref => ref.path.startsWith("support/")).map(ref => ({ path: `execution-bundle/${ref.path}`, sha256: ref.sha256 })) }, null, 2), "```",
|
|
137
|
+
"Copy the exact CLI-owned binding fields above into your result, including predecessors. This list contains only the immediate predecessor, or is empty for preflight; do not append transitive stage history.",
|
|
138
|
+
"Read the applicable support criteria relative to RUN home. Legacy task/subagent snippets in those reference files do not authorize manual lifecycle, repair or dispatch. Project facts must come from the assigned snapshot, not the canon criteria.",
|
|
139
|
+
"Every source/evidence path in the result is relative to the assigned snapshot workspace root, with forward slashes: src/index.ts or .memory-bank/index.md, never an absolute filesystem path. Hash the exact snapshot file bytes. Intake transport files, this RUN's packets and canonical support references are not snapshot sources: use the frozen input to interpret the request, but do not insert them into sources or evidence. Do not fabricate a snapshot-relative spelling for a file outside the snapshot.",
|
|
140
|
+
"Files under .dd-flow/review-evidence are CLI-generated observations, not original project sources: do not put them in sources/evidence or infer the original repository's history or unborn state from the working copy's Git metadata. For missing linked targets cite the existing referring project document and describe the absence as a limitation, not as a source file.",
|
|
141
|
+
"Every baseline aspect needs an explicit applicability row. The runtime registers applicable analytical Work and independent critics from the accepted predecessor; do not allocate or dispatch your own Work. Do not edit either the original project or snapshot. Read completed assigned Work results before proposing analysis/review. Keep all candidates, rejected findings, critic disagreement and limitations; a completed diagnostic is not a claim that the project is clean."] : []),
|
|
109
142
|
...(selection.input.ordinary_defaults ? ["", "## Ordinary SDLC technical defaults", "Use ordinary_defaults.proposals from the frozen input verbatim as project-owned JSON. Assign each missing configuration to an applicable unit with its exact workspace write scope and reviewed output artifact. Preserve valid existing configs. These defaults do not authorize future work, push, deletion or standing project policy. Do not invent a bootstrap command. If ordinary_defaults.missing is nonempty, resolve that actual choice through the existing stage pause/answer path before composing; do not guess."] : []),
|
|
110
143
|
...(memoryFlow ? ["", "## Request support reference", "```json",
|
|
111
144
|
JSON.stringify({ root: "run", path: `${stage.directory}/stage-input.json`, sha256: selection.inputSha256 }), "```",
|
|
@@ -174,7 +174,8 @@ export class PreparedDescriptorStageSettlement {
|
|
|
174
174
|
* It keeps bootstrap/check/reviewer custody in the same acceptance writer.
|
|
175
175
|
*/
|
|
176
176
|
apply(context, assertHandlerCustody) {
|
|
177
|
-
const
|
|
177
|
+
const operational = assertPreparedRunBundleSqlAuthority(context, this.#scope, this.#bundle);
|
|
178
|
+
const current = readFlowRunExecutionBundle(context, { projectRoot: operational.project_root, runId: this.#scope.runId });
|
|
178
179
|
if (!current || current.sha256 !== this.#bundle.sha256)
|
|
179
180
|
refuse("bundle_changed");
|
|
180
181
|
const receipt = { run_id: this.#scope.runId, stage: this.#scope.stage, work_id: this.#owner.workId, work_session_id: this.#owner.workSessionId, binding: this.binding() };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { AppError } from "../shared/errors.js";
|
|
5
|
+
const refuse = (reason, details = {}) => {
|
|
6
|
+
const correctable = ["invalid_source_reference", "source_hash_changed", "source_missing"].includes(reason);
|
|
7
|
+
const message = reason === "source_hash_changed"
|
|
8
|
+
? "Citation sha256 does not match its assigned snapshot file; correct the result using source_path and actual_source_sha256, then retry. No result was accepted."
|
|
9
|
+
: reason === "source_missing" ? "Citation is absent from the original project snapshot. Cite an existing project source instead; generated .dd-flow/review-evidence observations are not project sources. For a missing link target cite the existing referring document and record the absence as a limitation. Correct the result, then retry; no result was accepted."
|
|
10
|
+
: correctable ? "Use snapshot-relative evidence paths with forward slashes (for example src/index.ts), without absolute paths, dot segments or .git; sha256 must be 64 lowercase hex characters"
|
|
11
|
+
: "Diagnostic evidence does not match the assigned snapshot and coverage";
|
|
12
|
+
throw new AppError("diagnostic_evidence_invalid", message, 2, { ...details, reason, effect: "no_effect", ...(correctable ? { phase: "prepare", recoverable: true } : {}) });
|
|
13
|
+
};
|
|
14
|
+
/** Run after schema validation; schema-shaped hashes are not evidence by themselves. */
|
|
15
|
+
export function verifyDiagnosticSources(root, sources) {
|
|
16
|
+
const canonicalRoot = fs.realpathSync(root), seen = new Map();
|
|
17
|
+
for (const source of sources) {
|
|
18
|
+
if (!source.path || source.path.length > 4096 || source.path.includes("\\") || path.posix.isAbsolute(source.path) || path.win32.isAbsolute(source.path)
|
|
19
|
+
|| source.path.split("/").some(part => !part || part === "." || part === ".." || part === ".git")
|
|
20
|
+
|| !/^[a-f0-9]{64}$/.test(source.sha256))
|
|
21
|
+
refuse("invalid_source_reference");
|
|
22
|
+
if (seen.has(source.path) && seen.get(source.path) !== source.sha256)
|
|
23
|
+
refuse("conflicting_source_reference");
|
|
24
|
+
seen.set(source.path, source.sha256);
|
|
25
|
+
let bytes;
|
|
26
|
+
try {
|
|
27
|
+
const file = fs.realpathSync(path.join(canonicalRoot, source.path)), relative = path.relative(canonicalRoot, file);
|
|
28
|
+
if (relative === ".." || relative.startsWith(".." + path.sep) || path.isAbsolute(relative))
|
|
29
|
+
refuse("escaping_source_reference");
|
|
30
|
+
const stat = fs.statSync(file);
|
|
31
|
+
if (!stat.isFile() || stat.size > 16 * 1024 * 1024)
|
|
32
|
+
refuse("unbounded_source_reference");
|
|
33
|
+
bytes = fs.readFileSync(file);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
if (error instanceof AppError)
|
|
37
|
+
throw error;
|
|
38
|
+
if (["ENOENT", "ENOTDIR"].includes(error.code ?? "")) {
|
|
39
|
+
return refuse("source_missing", { source_path: source.path });
|
|
40
|
+
}
|
|
41
|
+
return refuse("source_unavailable");
|
|
42
|
+
}
|
|
43
|
+
const actual = createHash("sha256").update(bytes).digest("hex");
|
|
44
|
+
if (actual !== source.sha256)
|
|
45
|
+
refuse("source_hash_changed", {
|
|
46
|
+
source_path: source.path, provided_source_sha256: source.sha256, actual_source_sha256: actual
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export function verifyDiagnosticCoverage(root, baseline, coverage) {
|
|
51
|
+
const ids = coverage.map(value => value.id);
|
|
52
|
+
if (new Set(ids).size !== ids.length || ids.length !== baseline.length || baseline.some(id => !ids.includes(id)))
|
|
53
|
+
refuse("incomplete_aspect_coverage");
|
|
54
|
+
for (const value of coverage) {
|
|
55
|
+
if (!value.reason.trim())
|
|
56
|
+
refuse("missing_applicability_reason");
|
|
57
|
+
verifyDiagnosticSources(root, value.evidence);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** Each independent critic must account for every candidate, including rejected findings. */
|
|
61
|
+
export function verifyDiagnosticCritic(root, candidates, decisions) {
|
|
62
|
+
const ids = candidates.map(value => value.id), reviewed = decisions.map(value => value.finding_id);
|
|
63
|
+
if (new Set(ids).size !== ids.length || new Set(reviewed).size !== reviewed.length
|
|
64
|
+
|| ids.length !== reviewed.length || ids.some(id => !reviewed.includes(id)))
|
|
65
|
+
refuse("incomplete_critic_coverage");
|
|
66
|
+
for (const candidate of candidates) {
|
|
67
|
+
if (!candidate.evidence.length)
|
|
68
|
+
refuse("finding_without_evidence");
|
|
69
|
+
verifyDiagnosticSources(root, candidate.evidence);
|
|
70
|
+
}
|
|
71
|
+
for (const decision of decisions)
|
|
72
|
+
verifyDiagnosticSources(root, decision.evidence);
|
|
73
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { AppError } from "../shared/errors.js";
|
|
4
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
5
|
+
import { appendAudit } from "./audit.js";
|
|
6
|
+
import { prepareStagePacket, acceptStagePacket } from "./stage-report-renderer.js";
|
|
7
|
+
import { publishImmutableInput } from "../storage/immutable-input.js";
|
|
8
|
+
import { renderDiagnosticReport } from "./diagnostic-report.js";
|
|
9
|
+
function refuse(reason) { throw new AppError("diagnostic_report_publication_invalid", "Diagnostic report publication requires its owned stage and exact bytes", 2, { reason, effect: "no_effect" }); }
|
|
10
|
+
/** Files are immutable candidates until retain is called in the Stage acceptance
|
|
11
|
+
* writer. Interrupted publication can replay before any terminal acceptance. */
|
|
12
|
+
export function prepareDiagnosticReportPublication(context, scope, report) {
|
|
13
|
+
const run = context.db.get("SELECT run_root,index_json FROM runs WHERE project_id=? AND id=?", [scope.projectId, scope.runId]);
|
|
14
|
+
const stage = run ? JSON.parse(run.index_json).stage_runs.find((value) => value.stage === scope.stage) : null;
|
|
15
|
+
if (!run?.run_root || scope.stage !== "report" || stage?.attempt !== scope.attempt || stage.input_sha256 !== scope.inputSha256
|
|
16
|
+
|| report.run.id !== scope.runId || !["running", "done"].includes(stage.status))
|
|
17
|
+
refuse("foreign_stage");
|
|
18
|
+
const directory = path.join(run.run_root, stage.dir);
|
|
19
|
+
if (fs.realpathSync(run.run_root) !== run.run_root || fs.realpathSync(directory) !== directory
|
|
20
|
+
|| !directory.startsWith(run.run_root + path.sep))
|
|
21
|
+
refuse("unsafe_report_directory");
|
|
22
|
+
const rendered = renderDiagnosticReport(report);
|
|
23
|
+
const packets = Object.entries(rendered).map(([kind, bytes]) => {
|
|
24
|
+
if (bytes.length > 4 * 1024 * 1024)
|
|
25
|
+
refuse("report_exceeds_bound");
|
|
26
|
+
const suffix = kind === "markdown" ? "md" : kind;
|
|
27
|
+
const file = path.join(directory, `diagnostic-report.${suffix}`);
|
|
28
|
+
try {
|
|
29
|
+
if (!fs.lstatSync(file).isFile() || fs.lstatSync(file).isSymbolicLink())
|
|
30
|
+
refuse("unsafe_report_file");
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (error.code !== "ENOENT")
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
return prepareStagePacket(file, bytes);
|
|
37
|
+
});
|
|
38
|
+
const manifest = { schema_id: "dd-flow/diagnostic-report-publication@1", run_id: scope.runId, stage: scope.stage, attempt: scope.attempt,
|
|
39
|
+
input_sha256: scope.inputSha256, artifacts: packets.map(packet => ({ path: path.relative(run.run_root, packet.file).split(path.sep).join("/"), sha256: packet.sha256 })) };
|
|
40
|
+
const retained = (writer) => writer.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type='diagnostic.report' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.attempt')=? LIMIT 2", [scope.projectId, scope.runId, scope.attempt]);
|
|
41
|
+
return {
|
|
42
|
+
manifest,
|
|
43
|
+
publish() {
|
|
44
|
+
if (fs.realpathSync(directory) !== directory)
|
|
45
|
+
refuse("report_directory_changed");
|
|
46
|
+
for (const packet of packets)
|
|
47
|
+
publishImmutableInput(packet.file, packet.bytes, "diagnostic_report_conflict");
|
|
48
|
+
},
|
|
49
|
+
/** SQL-only. The existing packet ledger retains recoverable authoritative
|
|
50
|
+
* bytes in the same transaction as Stage completion. */
|
|
51
|
+
retain(writer) {
|
|
52
|
+
const rows = retained(writer);
|
|
53
|
+
if (rows.length) {
|
|
54
|
+
if (rows.length !== 1 || hybridDigest(JSON.parse(rows[0].payload_json)) !== hybridDigest(manifest))
|
|
55
|
+
refuse("report_custody_changed");
|
|
56
|
+
for (const packet of packets) {
|
|
57
|
+
const records = writer.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,'$.file')=? LIMIT 2", [scope.projectId, scope.runId, scope.stage, scope.attempt, packet.file]);
|
|
58
|
+
if (records.length !== 1 || Buffer.byteLength(records[0].payload_json) > 6 * 1024 * 1024
|
|
59
|
+
|| hybridDigest(JSON.parse(records[0].payload_json)) !== hybridDigest({ run_id: scope.runId, stage: scope.stage,
|
|
60
|
+
attempt: scope.attempt, file: packet.file, sha256: packet.sha256, base64: packet.bytes.toString("base64") }))
|
|
61
|
+
refuse("report_packet_custody_changed");
|
|
62
|
+
}
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (stage.status !== "running")
|
|
66
|
+
refuse("report_receipt_missing");
|
|
67
|
+
for (const packet of packets)
|
|
68
|
+
acceptStagePacket(writer, { ...scope, prepared: packet });
|
|
69
|
+
appendAudit(writer, { projectId: scope.projectId, eventType: "diagnostic.report", payload: manifest });
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { AppError } from "../shared/errors.js";
|
|
2
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
3
|
+
import { collectDiagnosticCandidates } from "./diagnostic-work-results.js";
|
|
4
|
+
function refuse(reason) { throw new AppError("diagnostic_report_invalid", "Diagnostic report requires the reviewed candidate and critic registers", 2, { reason, effect: "no_effect" }); }
|
|
5
|
+
/** Projection only. The handler separately owns source checks, physical critic
|
|
6
|
+
* independence, SQL custody, publication and terminal RUN settlement. */
|
|
7
|
+
export function prepareDiagnosticReport(result, analysis, critics) {
|
|
8
|
+
if (result.stage !== "report" || !critics.length
|
|
9
|
+
|| [...analysis, ...critics].some(work => work.workId !== work.result.work_id || work.result.run_id !== result.run_id || work.result.flow_id !== result.flow_id))
|
|
10
|
+
refuse("foreign_work");
|
|
11
|
+
const candidates = collectDiagnosticCandidates(analysis);
|
|
12
|
+
if (hybridDigest(candidates.findings) !== hybridDigest(result.details.findings))
|
|
13
|
+
refuse("candidate_register_changed");
|
|
14
|
+
for (const critic of critics) {
|
|
15
|
+
const ids = critic.result.decisions.map(value => value.finding_id);
|
|
16
|
+
if (critic.result.stage !== "review" || critic.result.findings.length || new Set(ids).size !== ids.length
|
|
17
|
+
|| ids.length !== candidates.findings.length || candidates.findings.some(value => !ids.includes(value.id)))
|
|
18
|
+
refuse("incomplete_critic");
|
|
19
|
+
}
|
|
20
|
+
const decisions = new Map(result.details.decisions.map(value => [value.finding_id, value]));
|
|
21
|
+
if (decisions.size !== candidates.findings.length || result.details.decisions.length !== decisions.size
|
|
22
|
+
|| candidates.findings.some(value => !decisions.has(value.id)))
|
|
23
|
+
refuse("incomplete_resolution");
|
|
24
|
+
const findings = candidates.findings.map((finding, index) => ({ ...finding, origin: candidates.origins[index],
|
|
25
|
+
resolution: decisions.get(finding.id),
|
|
26
|
+
critic_decisions: critics.map(work => ({ work_id: work.workId, decision: work.result.decisions.find(value => value.finding_id === finding.id) })) }));
|
|
27
|
+
const incomplete = result.details.coverage.some(value => value.applicability === "blocked" || value.applicability === "out_of_scope")
|
|
28
|
+
|| result.details.missing_expected_sources.length > 0 || result.details.limitations.length > 0;
|
|
29
|
+
const accepted = findings.filter(value => !["rejected", "duplicate"].includes(value.resolution.disposition));
|
|
30
|
+
const sourceMap = new Map();
|
|
31
|
+
for (const source of [...result.details.sources, ...result.details.coverage.flatMap(value => value.evidence),
|
|
32
|
+
...analysis.flatMap(value => value.result.sources), ...critics.flatMap(value => value.result.sources),
|
|
33
|
+
...findings.flatMap(value => [...value.evidence, ...value.resolution.evidence, ...value.critic_decisions.flatMap(critic => critic.decision.evidence)])]) {
|
|
34
|
+
if (sourceMap.has(source.path) && sourceMap.get(source.path).sha256 !== source.sha256)
|
|
35
|
+
refuse("conflicting_source_versions");
|
|
36
|
+
sourceMap.set(source.path, structuredClone(source));
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
schema_id: "dd-flow/diagnostic-report@1",
|
|
40
|
+
run: { id: result.run_id, flow_id: result.flow_id, snapshot_sha256: result.snapshot_sha256 },
|
|
41
|
+
summary: result.summary, review_focus: structuredClone(result.details.focus), source_map: [...sourceMap.values()],
|
|
42
|
+
aspect_coverage: structuredClone(result.details.coverage),
|
|
43
|
+
critic_pass: critics.map(work => ({ work_id: work.workId, role: work.result.aspect_id, summary: work.result.summary,
|
|
44
|
+
semantic_sha256: hybridDigest(work.result), decisions: structuredClone(work.result.decisions), limitations: [...work.result.limitations] })),
|
|
45
|
+
findings_register: findings,
|
|
46
|
+
conformance_summary: { assessment: incomplete ? "incomplete" : accepted.length ? "findings" : "no_accepted_findings_in_selected_scope",
|
|
47
|
+
accepted_findings: accepted.length, rejected_or_duplicate_findings: findings.length - accepted.length,
|
|
48
|
+
applicable_aspects: result.details.coverage.filter(value => value.applicability === "applicable").length,
|
|
49
|
+
blocked_aspects: result.details.coverage.filter(value => value.applicability === "blocked").length,
|
|
50
|
+
missing_expected_sources: [...result.details.missing_expected_sources] },
|
|
51
|
+
review_fix_recommendation: { authorization: "not_granted", candidate_ids: findings.filter(value => value.resolution.disposition === "review_fix_candidate").map(value => value.id) },
|
|
52
|
+
deferrals: { authorization: "not_granted", candidate_ids: findings.filter(value => value.resolution.disposition === "def_candidate").map(value => value.id),
|
|
53
|
+
observed_sources: [...sourceMap.values()].filter(value => /(?:^|\/)DEF(?:-MBU)?-[^/]+/.test(value.path)) },
|
|
54
|
+
limitations: [...result.details.limitations],
|
|
55
|
+
dashboard_links: critics.map(work => ({ work_id: work.workId, result: `run://${result.run_id}/works/${work.workId}/result.json` }))
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/** HTML embeds exactly the authoritative JSON value; untrusted prose is text,
|
|
59
|
+
* never HTML or executable script. Markdown remains a compact companion. */
|
|
60
|
+
export function renderDiagnosticReport(report) {
|
|
61
|
+
const json = Buffer.from(JSON.stringify(report, null, 2) + "\n");
|
|
62
|
+
const escape = (text) => text.replace(/[&<>"']/g, value => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[value]);
|
|
63
|
+
const embedded = json.toString("utf8").replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
|
|
64
|
+
const markdown = Buffer.from(`# ${report.run.flow_id} · ${report.run.id}\n\n${report.summary}\n\nAssessment: ${report.conformance_summary.assessment}\n\n${report.findings_register.length} findings retained; ${report.critic_pass.length} independent critic passes.\n\nAuthoritative data: diagnostic-report.json\n`);
|
|
65
|
+
const html = Buffer.from(`<!doctype html><html lang="en"><meta charset="utf-8"><title>${escape(report.run.flow_id)} · ${escape(report.run.id)}</title><body><h1>${escape(report.review_focus.title)}</h1><p>${escape(report.summary)}</p><p>${escape(report.conformance_summary.assessment)}</p><h2>Coverage</h2><ul>${report.aspect_coverage.map(value => `<li>${escape(value.id)}: ${escape(value.applicability)} — ${escape(value.reason)}</li>`).join("")}</ul><h2>Findings</h2><pre>${escape(JSON.stringify(report.findings_register, null, 2))}</pre><script id="diagnostic-report-data" type="application/json">${embedded}</script></body></html>\n`);
|
|
66
|
+
return { json, markdown, html };
|
|
67
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { AppError } from "../shared/errors.js";
|
|
3
|
+
import { isDiagnosticFlow } from "../domain/diagnostic-flows.js";
|
|
4
|
+
import { PreparedInstalledFlowSource } from "./installed-flow-source.js";
|
|
5
|
+
import { FrozenGenericExecution } from "./generic-execution.js";
|
|
6
|
+
import { AdmittedFrozenEngine } from "./frozen-engine.js";
|
|
7
|
+
import { RetainedExecutionBundle } from "./execution-bundles.js";
|
|
8
|
+
import { readDiagnosticSnapshot } from "./diagnostic-snapshot.js";
|
|
9
|
+
import { requireProjectByRoot } from "./projects.js";
|
|
10
|
+
import { nextDescriptorRunId } from "./descriptor-run-start.js";
|
|
11
|
+
import { bundledHandlerContext } from "./stage-handlers.js";
|
|
12
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
13
|
+
const bytes = (value) => Buffer.from(JSON.stringify(value));
|
|
14
|
+
const hash = (value) => createHash("sha256").update(value).digest("hex");
|
|
15
|
+
function refuse(reason) { throw new AppError("diagnostic_run_bundle_invalid", "Diagnostic bundle requires its frozen source, execution and snapshot", 2, { reason, effect: "no_effect" }); }
|
|
16
|
+
/** Initial construction only. Retry after allocation restores the retained RUN
|
|
17
|
+
* bundle, never current operator settings/canon. boundAt belongs to the request. */
|
|
18
|
+
export function prepareDiagnosticRunBundle(context, input) {
|
|
19
|
+
if (!(input.source instanceof PreparedInstalledFlowSource) || !(input.execution instanceof FrozenGenericExecution)
|
|
20
|
+
|| !(input.engine instanceof AdmittedFrozenEngine) || !Number.isFinite(Date.parse(input.boundAt)))
|
|
21
|
+
refuse("unprepared_selection");
|
|
22
|
+
const frozen = input.execution.snapshot(), flowId = frozen.settings.flow_id;
|
|
23
|
+
if (!isDiagnosticFlow(flowId))
|
|
24
|
+
refuse("diagnostic_flow_required");
|
|
25
|
+
const project = requireProjectByRoot(context, frozen.scope.projectRoot);
|
|
26
|
+
const snapshot = readDiagnosticSnapshot(context, { projectId: project.id, projectRoot: frozen.scope.projectRoot,
|
|
27
|
+
sourceRoot: frozen.scope.projectRoot, requestId: frozen.scope.requestId, flowId });
|
|
28
|
+
if (!snapshot || snapshot.root !== frozen.scope.workspace)
|
|
29
|
+
refuse("snapshot_selection_changed");
|
|
30
|
+
input.source.revalidate();
|
|
31
|
+
const flow = input.source.resolveRegisteredFlow();
|
|
32
|
+
if (flow.descriptor.id !== flowId || flow.sha256 !== frozen.settings.descriptor_sha256)
|
|
33
|
+
refuse("flow_selection_changed");
|
|
34
|
+
input.engine.admitDiagnosticFlow(input.source, flowId);
|
|
35
|
+
const manifest = input.engine.manifest(), checksum = manifest.integrity.checksum;
|
|
36
|
+
if (typeof checksum !== "string")
|
|
37
|
+
refuse("engine_identity_missing");
|
|
38
|
+
const runId = nextDescriptorRunId(context, { projectRoot: frozen.scope.projectRoot, requestId: frozen.scope.requestId, slug: flowId });
|
|
39
|
+
const binding = { schema_id: "dd-flow/run-engine-binding@1", run_id: runId,
|
|
40
|
+
project_root: frozen.scope.projectRoot, bound_at: input.boundAt, source: "run_creation", reason: "Request-bound read-only diagnostic",
|
|
41
|
+
engine: { package_name: manifest.package_name, package_version: manifest.package_version, engine_version: manifest.engine_version,
|
|
42
|
+
integrity_checksum: checksum, snapshot_root: manifest.snapshot_root }, probe: { status: "not_required", command: null } };
|
|
43
|
+
const blobs = input.source.blobs();
|
|
44
|
+
const put = (logical, content) => {
|
|
45
|
+
if (blobs.has(logical))
|
|
46
|
+
refuse("bundle_path_collision");
|
|
47
|
+
blobs.set(logical, content);
|
|
48
|
+
return { path: logical, sha256: hash(content) };
|
|
49
|
+
};
|
|
50
|
+
for (const ref of [frozen.settings.attached_session_receipt, frozen.settings.delegation.native_capacity_receipt]) {
|
|
51
|
+
if (!ref)
|
|
52
|
+
continue;
|
|
53
|
+
const content = input.receiptBytes?.get(ref.sha256);
|
|
54
|
+
if (!content || hash(content) !== ref.sha256 || !input.receipts?.has(ref.sha256))
|
|
55
|
+
refuse("execution_receipt_missing");
|
|
56
|
+
put(ref.path, Buffer.from(content));
|
|
57
|
+
}
|
|
58
|
+
const profiles = { schema_id: "dd-flow/profile-settings@1", profiles: frozen.profiles, aliases: {}, default_profile: null };
|
|
59
|
+
const profile = put("runtime/profiles.json", bytes({ ...profiles, revision: hybridDigest(profiles) }));
|
|
60
|
+
const execution = put("runtime/execution.json", bytes(frozen.settings));
|
|
61
|
+
const engineBinding = put("runtime/engine-binding.json", bytes(binding));
|
|
62
|
+
const engineManifest = put("runtime/engine-manifest.json", input.engine.manifestBytes());
|
|
63
|
+
put("runtime/diagnostic-snapshot.json", bytes(snapshot));
|
|
64
|
+
const provenance = { canon: input.source.canonProvenance(), project_pack: null };
|
|
65
|
+
const bundle = { schema_id: "dd-flow/execution-bundle@1", run_id: runId, flow_id: flowId,
|
|
66
|
+
descriptor: input.source.descriptorReference(), execution_settings: execution, profile_settings: profile,
|
|
67
|
+
engine_binding: engineBinding, engine_manifest: engineManifest, bootstrap_receipts: [], ...provenance,
|
|
68
|
+
inventory: [...blobs].map(([logical, content]) => ({ path: logical, sha256: hash(content) })) };
|
|
69
|
+
const encoded = bytes(bundle);
|
|
70
|
+
return RetainedExecutionBundle.admit(encoded, hash(encoded), blobs, { runId, executionScope: frozen.scope,
|
|
71
|
+
bundleProjectRoot: frozen.scope.projectRoot, descriptorSha256: flow.sha256, engineChecksum: checksum,
|
|
72
|
+
engineBindingSha256: engineBinding.sha256, engineManifestSha256: engineManifest.sha256,
|
|
73
|
+
descriptorContext: bundledHandlerContext(new Map()), bundleProvenance: provenance, receipts: input.receipts ?? new Map() });
|
|
74
|
+
}
|