@deksden-com/dd-flow-cli 0.9.0-beta.151 → 0.9.0-beta.155
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/dist/build-info.json +8 -8
- package/dist/cli/command-inputs.js +33 -3
- package/dist/cli/command-metadata.js +73 -2
- package/dist/cli/delivery.js +108 -0
- package/dist/cli/diagnostic-invocation.js +23 -7
- package/dist/cli/diagnostic.js +14 -3
- package/dist/cli/help.js +117 -1
- package/dist/cli/hook-ingress.js +36 -49
- package/dist/cli/interactive-invocation.js +33 -0
- package/dist/cli/interactive.js +114 -0
- package/dist/cli/memory-fix-invocation.js +52 -0
- package/dist/cli/memory-fix.js +100 -0
- package/dist/cli/memory-flow-admission.js +7 -3
- package/dist/cli/memory-init.js +5 -4
- package/dist/cli/native-hook-ingress.js +5 -1
- package/dist/cli/native-hook-preparation.js +31 -4
- package/dist/cli/run-cli.js +273 -19
- package/dist/cli.js +17 -3
- package/dist/domain/delivery-flows.js +8 -0
- package/dist/domain/distill-flow.js +20 -0
- package/dist/domain/interactive-flow.js +1 -0
- package/dist/domain/memory-fix-flow.js +2 -0
- package/dist/domain/review-fix-flow.js +2 -0
- package/dist/domain/source-snapshot-flows.js +4 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +18 -3
- package/dist/harness-runtime/bin/dd-grok.mjs +15 -5
- package/dist/harness-runtime/bin/dd-zcode.mjs +15 -7
- package/dist/harness-runtime/lib/agy-error-boundary.mjs +18 -10
- package/dist/harness-runtime/lib/agy-native-inspection.mjs +12 -3
- package/dist/harness-runtime/lib/agy-native-inventory.mjs +19 -0
- package/dist/harness-runtime/lib/agy-terminal-envelope.mjs +13 -6
- package/dist/harness-runtime/lib/agy-transcript-stream.mjs +54 -0
- package/dist/harness-runtime/lib/daemon-observation.d.mts +1 -0
- package/dist/harness-runtime/lib/daemon-observation.mjs +12 -4
- package/dist/harness-runtime/lib/daemon-operations.mjs +20 -5
- package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +371 -114
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +43 -3
- package/dist/harness-runtime/lib/dd-codex.mjs +36 -4
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +25 -11
- package/dist/harness-runtime/lib/dd-droid.mjs +124 -11
- package/dist/harness-runtime/lib/dd-grok-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +137 -25
- package/dist/harness-runtime/lib/dd-grok.mjs +4 -2
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +170 -25
- package/dist/harness-runtime/lib/dd-opencode.mjs +40 -19
- package/dist/harness-runtime/lib/dd-zcode-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +115 -22
- package/dist/harness-runtime/lib/dd-zcode.mjs +75 -36
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +1 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +10 -3
- package/dist/harness-runtime/lib/delivery-command.mjs +84 -0
- package/dist/harness-runtime/lib/driver-recovery.mjs +0 -19
- package/dist/harness-runtime/lib/droid-observation.mjs +9 -5
- package/dist/harness-runtime/lib/lifecycle-contract.d.mts +1 -0
- package/dist/harness-runtime/lib/lifecycle-contract.mjs +2 -1
- package/dist/harness-runtime/lib/native-hook-command.d.mts +3 -2
- package/dist/harness-runtime/lib/native-hook-command.mjs +63 -8
- package/dist/harness-runtime/lib/native-streams.mjs +3 -10
- package/dist/harness-runtime/lib/opencode-plugin.mjs +36 -9
- package/dist/harness-runtime/lib/operation-errors.mjs +13 -0
- package/dist/harness-runtime/lib/operation-progress.mjs +6 -2
- package/dist/harness-runtime/lib/process-json.mjs +4 -5
- package/dist/harness-runtime/lib/tool-observations.mjs +20 -10
- package/dist/harness-runtime/lib/zcode-native-hook-ownership.mjs +109 -0
- package/dist/schemas/code-work-result.schema.json +2 -1
- package/dist/schemas/delivery-stage-result.schema.json +286 -0
- package/dist/schemas/distill-stage-result.schema.json +667 -0
- package/dist/schemas/distill-work-result.schema.json +428 -0
- package/dist/schemas/execution-bundle.schema.json +6 -2
- package/dist/schemas/interactive-stage-result.schema.json +47 -0
- package/dist/schemas/interactive-work-result.schema.json +63 -0
- package/dist/schemas/mb-fix-stage-result.schema.json +93 -0
- package/dist/schemas/project-flow-pack-manifest.schema.json +2 -2
- package/dist/schemas/review-fix-stage-result.schema.json +210 -0
- package/dist/schemas/run-control-receipt.schema.json +1 -0
- package/dist/services/assigned-lifecycle.js +382 -0
- package/dist/services/check-runner-evidence.js +77 -10
- package/dist/services/check-runner-proof.js +69 -1
- package/dist/services/cli-operation-classifier.js +6 -1
- package/dist/services/code-checks.js +56 -23
- package/dist/services/codex-hook-delivery.js +11 -2
- package/dist/services/controller-execution.js +4 -1
- package/dist/services/controller-fanout.js +9 -4
- package/dist/services/controller-memory-worker.js +6 -4
- package/dist/services/controller-result-authority.js +122 -0
- package/dist/services/dashboard.js +4 -2
- package/dist/services/delivery-access.js +123 -0
- package/dist/services/delivery-approval.js +68 -0
- package/dist/services/delivery-artifact.js +86 -0
- package/dist/services/delivery-checks.js +178 -0
- package/dist/services/delivery-execution.js +181 -0
- package/dist/services/delivery-intents.js +212 -0
- package/dist/services/delivery-operation.js +81 -0
- package/dist/services/delivery-policy.js +91 -0
- package/dist/services/delivery-procedures.js +43 -0
- package/dist/services/delivery-request.js +99 -0
- package/dist/services/delivery-result.js +144 -0
- package/dist/services/delivery-stage-commands.js +26 -0
- package/dist/services/delivery-stage-entry.js +56 -0
- package/dist/services/delivery-stage-handlers.js +135 -0
- package/dist/services/delivery-stage-results.js +94 -0
- package/dist/services/descriptor-coordinator-session.js +3 -1
- package/dist/services/descriptor-run-start.js +24 -4
- package/dist/services/descriptor-stage-coordinator.js +78 -10
- package/dist/services/descriptor-stage-settlement.js +59 -14
- package/dist/services/diagnostic-repair-intake.js +123 -0
- package/dist/services/diagnostic-repair-source-intake.js +102 -0
- package/dist/services/diagnostic-report.js +30 -0
- package/dist/services/diagnostic-run-bundle.js +48 -9
- package/dist/services/diagnostic-snapshot.js +2 -2
- package/dist/services/diagnostic-stage-handlers.js +3 -2
- package/dist/services/diagnostic-stage-results.js +26 -0
- package/dist/services/diagnostic-start.js +56 -6
- package/dist/services/diagnostic-work-collection.js +3 -3
- package/dist/services/distill-document-publication.js +280 -0
- package/dist/services/distill-promotion.js +197 -0
- package/dist/services/distill-publication-custody.js +162 -0
- package/dist/services/distill-stage-handlers.js +249 -0
- package/dist/services/distill-stage-results.js +105 -0
- package/dist/services/distill-user-inputs.js +4 -0
- package/dist/services/distill-work-batch.js +42 -0
- package/dist/services/distill-work-collection.js +74 -0
- package/dist/services/distill-work-registration.js +93 -0
- package/dist/services/distill-work-results.js +98 -0
- package/dist/services/eval-snapshots.js +17 -7
- package/dist/services/external-work-launch.js +8 -2
- package/dist/services/flow-catalog.js +24 -0
- package/dist/services/frozen-engine.js +19 -2
- package/dist/services/generic-execution.js +4 -3
- package/dist/services/harness-adapter.js +2 -14
- package/dist/services/hook-memory-routing.js +1 -1
- package/dist/services/hooks.js +31 -6
- package/dist/services/hybrid-contracts.js +6 -1
- package/dist/services/installed-flow-source.js +37 -2
- package/dist/services/interactive-attached-session.js +215 -0
- package/dist/services/interactive-baseline-custody.js +54 -0
- package/dist/services/interactive-before-versions.js +148 -0
- package/dist/services/interactive-bundle-data.js +85 -0
- package/dist/services/interactive-checks.js +179 -0
- package/dist/services/interactive-execution.js +33 -0
- package/dist/services/interactive-finish-integration.js +236 -0
- package/dist/services/interactive-git-baseline.js +230 -0
- package/dist/services/interactive-git-dispatch.js +278 -0
- package/dist/services/interactive-input-delivery.js +82 -0
- package/dist/services/interactive-mutation-scope.js +231 -0
- package/dist/services/interactive-request.js +287 -0
- package/dist/services/interactive-run-bundle.js +109 -0
- package/dist/services/interactive-stage-entry.js +41 -0
- package/dist/services/interactive-stage-handlers.js +289 -0
- package/dist/services/interactive-stage-observation.js +136 -0
- package/dist/services/interactive-stage-results.js +111 -0
- package/dist/services/interactive-user-inputs.js +142 -0
- package/dist/services/interactive-work-batch.js +43 -0
- package/dist/services/interactive-work-collection.js +119 -0
- package/dist/services/interactive-work-completion.js +180 -0
- package/dist/services/interactive-work-context.js +249 -0
- package/dist/services/interactive-work-history.js +112 -0
- package/dist/services/interactive-work-registration.js +124 -0
- package/dist/services/interactive-work-results.js +115 -0
- package/dist/services/interactive-work-start.js +155 -0
- package/dist/services/lifecycle-invocations.js +214 -24
- package/dist/services/managed-daemon-binding.js +52 -1
- package/dist/services/memory-compose-handler.js +2 -22
- package/dist/services/memory-finish-integration.js +58 -17
- package/dist/services/memory-fix-finish-integration.js +129 -0
- package/dist/services/memory-fix-git-dispatch.js +139 -0
- package/dist/services/memory-fix-intake-custody.js +11 -0
- package/dist/services/memory-fix-intake-settlement.js +127 -0
- package/dist/services/memory-fix-publication.js +196 -0
- package/dist/services/memory-fix-repair-admission.js +116 -0
- package/dist/services/memory-fix-request.js +141 -0
- package/dist/services/memory-fix-run-bundle.js +83 -0
- package/dist/services/memory-fix-scope.js +78 -0
- package/dist/services/memory-fix-sources.js +85 -0
- package/dist/services/memory-fix-stage-entry.js +24 -0
- package/dist/services/memory-fix-stage-handlers.js +282 -0
- package/dist/services/memory-fix-stage-results.js +106 -0
- package/dist/services/memory-fix-workspace.js +127 -0
- package/dist/services/memory-flow-user-inputs.js +88 -0
- package/dist/services/memory-init-contexts.js +55 -5
- package/dist/services/memory-init-controller-start.js +20 -4
- package/dist/services/memory-layer.js +7 -3
- package/dist/services/memory-native-review-copy.js +27 -3
- package/dist/services/memory-repair-document-delta.js +1 -0
- package/dist/services/project-flow-source.js +195 -0
- package/dist/services/project-git-integration.js +92 -0
- package/dist/services/repair-intents.js +2 -4
- package/dist/services/retained-native-hook.js +245 -0
- package/dist/services/review-fix-handoff.js +82 -0
- package/dist/services/review-fix-intake-custody.js +17 -0
- package/dist/services/review-fix-stage-entry.js +43 -0
- package/dist/services/review-fix-stage-handlers.js +176 -0
- package/dist/services/review-fix-stage-results.js +86 -0
- package/dist/services/run-control-worker.js +42 -1
- package/dist/services/run-control.js +40 -5
- package/dist/services/run-controller-adapter.js +37 -3
- package/dist/services/run-controller-process.js +12 -4
- package/dist/services/run-controller-recovery.js +46 -6
- package/dist/services/run-controller-state.js +3 -2
- package/dist/services/run-controller.js +209 -43
- package/dist/services/run-recovery.js +31 -2
- package/dist/services/run-timeline.js +59 -0
- package/dist/services/runs.js +18 -31
- package/dist/services/sessions.js +2 -1
- package/dist/services/stage-handlers.js +55 -3
- package/dist/services/stage-lifecycle.js +4 -3
- package/dist/services/stage-pause.js +26 -9
- package/dist/services/stage-report-renderer.js +2 -6
- package/dist/services/technical-notes.js +31 -0
- package/dist/services/verification-matrix.js +1 -1
- package/dist/services/version-status.js +1 -1
- package/dist/services/vnext-code-review.js +74 -5
- package/dist/services/vnext-code.js +9 -32
- package/dist/services/vnext-fanout.js +53 -8
- package/dist/services/vnext-merge.js +10 -5
- package/dist/services/vnext-plan-review.js +1 -1
- package/dist/services/vnext-plan.js +1 -1
- package/dist/services/vnext-protocolize.js +1 -1
- package/dist/services/vnext-specify.js +10 -5
- package/dist/services/work-registry.js +252 -66
- package/dist/services/worktrees.js +7 -4
- package/dist/storage/database.js +2 -1
- package/dist/verification-components.json +82 -0
- package/package.json +4 -3
- package/tools/verification-jobs.mjs +342 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { AppError } from "../shared/errors.js";
|
|
2
|
+
import { RetainedMemoryRepairPublicationPlan } from "./memory-repair-publication-plan.js";
|
|
3
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
4
|
+
import { appendAudit } from "./audit.js";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
import { resolveStagePublicationPath } from "./stage-report-renderer.js";
|
|
8
|
+
import { validateDistillBeforeDocuments, distillBeforeDocumentReferences } from "./distill-document-publication.js";
|
|
9
|
+
const intentEvent = "distill.publication_intent", completeEvent = "distill.publication_complete";
|
|
10
|
+
const key = (binding) => `distill_publication:${hybridDigest({ project_id: binding.project_id, run_id: binding.run_id })}`;
|
|
11
|
+
function refuse(reason) {
|
|
12
|
+
throw new AppError("distill_publication_custody_invalid", "Publication requires its original approved binding and retained plan", 1, { reason, effect: "no_effect" });
|
|
13
|
+
}
|
|
14
|
+
const receipt = (binding, plan, completed = false) => ({
|
|
15
|
+
schema_id: completed ? "dd-flow/distill-publication-complete@1" : "dd-flow/distill-publication-intent@1",
|
|
16
|
+
run_id: binding.run_id, binding_sha256: hybridDigest(binding), plan_sha256: plan.sha256
|
|
17
|
+
});
|
|
18
|
+
/** SQL custody only. The caller derives binding from the active Stage,
|
|
19
|
+
* accepted review and delivered user input, never from model authority fields.
|
|
20
|
+
* An existing intent cannot adopt today's partly changed destination as baseline. */
|
|
21
|
+
export function readDistillPublication(context, binding) {
|
|
22
|
+
return context.db.readSnapshot(() => {
|
|
23
|
+
const row = context.db.get("SELECT value_json FROM runtime_config WHERE key=?", [key(binding)]);
|
|
24
|
+
const events = (type) => context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type=? AND json_extract(payload_json,'$.run_id')=? LIMIT 2", [binding.project_id, type, binding.run_id]);
|
|
25
|
+
const intents = events(intentEvent), completions = events(completeEvent);
|
|
26
|
+
if (!row && !intents.length && !completions.length)
|
|
27
|
+
return null;
|
|
28
|
+
if (!row || intents.length !== 1 || completions.length > 1 || Buffer.byteLength(row.value_json) > 180 * 1024 * 1024
|
|
29
|
+
|| [...intents, ...completions].some(value => Buffer.byteLength(value.payload_json) > 32768))
|
|
30
|
+
refuse("missing_or_unbounded_custody");
|
|
31
|
+
try {
|
|
32
|
+
const value = JSON.parse(row.value_json);
|
|
33
|
+
if (!value || Object.keys(value).sort().join() !== "before_documents,binding,plan_json,plan_sha256" || hybridDigest(value.binding) !== hybridDigest(binding)
|
|
34
|
+
|| typeof value.plan_json !== "string")
|
|
35
|
+
refuse("binding_changed");
|
|
36
|
+
const plan = RetainedMemoryRepairPublicationPlan.restore(Buffer.from(value.plan_json), value.plan_sha256);
|
|
37
|
+
const beforeDocuments = validateDistillBeforeDocuments(plan, value.before_documents);
|
|
38
|
+
if (hybridDigest(JSON.parse(intents[0].payload_json)) !== hybridDigest(receipt(binding, plan))
|
|
39
|
+
|| completions.length && hybridDigest(JSON.parse(completions[0].payload_json)) !== hybridDigest(receipt(binding, plan, true)))
|
|
40
|
+
refuse("receipt_changed");
|
|
41
|
+
return { plan, beforeDocuments, completed: completions.length === 1 };
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
if (error instanceof AppError)
|
|
45
|
+
throw error;
|
|
46
|
+
return refuse("invalid_retained_publication");
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
export function retainDistillPublication(context, binding, plan, assertAuthority, beforeDocuments = []) {
|
|
51
|
+
if (!(plan instanceof RetainedMemoryRepairPublicationPlan) || typeof assertAuthority !== "function")
|
|
52
|
+
refuse("prepared_plan_required");
|
|
53
|
+
const paths = JSON.parse(Buffer.from(JSON.parse(plan.bytes().toString("utf8")).before_base64, "base64").toString("utf8")).write_paths;
|
|
54
|
+
if (hybridDigest([...paths].sort()) !== hybridDigest([...binding.write_scope.paths].sort()))
|
|
55
|
+
refuse("plan_scope_changed");
|
|
56
|
+
const originals = validateDistillBeforeDocuments(plan, beforeDocuments);
|
|
57
|
+
return context.db.writeTransaction(() => {
|
|
58
|
+
assertAuthority();
|
|
59
|
+
const prior = readDistillPublication(context, binding);
|
|
60
|
+
if (prior) {
|
|
61
|
+
if (prior.plan.sha256 !== plan.sha256 || hybridDigest(prior.beforeDocuments) !== hybridDigest(originals))
|
|
62
|
+
refuse("original_plan_required");
|
|
63
|
+
return { changed: false, plan: prior.plan };
|
|
64
|
+
}
|
|
65
|
+
// A new intent must describe the original destination, not an already
|
|
66
|
+
// applied prefix or a different directory with coincidentally similar paths.
|
|
67
|
+
if (plan.classify(binding.write_scope.destination_root).completed !== 0)
|
|
68
|
+
refuse("original_destination_required");
|
|
69
|
+
const now = context.now();
|
|
70
|
+
const value = JSON.stringify({ binding, plan_json: plan.bytes().toString("utf8"), plan_sha256: plan.sha256, before_documents: originals });
|
|
71
|
+
if (Buffer.byteLength(value) > 180 * 1024 * 1024)
|
|
72
|
+
refuse("unbounded_publication_custody");
|
|
73
|
+
context.db.run("INSERT INTO runtime_config(key,value_json,created_at,updated_at) VALUES (?,?,?,?)", [key(binding), value, now, now]);
|
|
74
|
+
appendAudit(context, { projectId: binding.project_id, eventType: intentEvent, payload: receipt(binding, plan) });
|
|
75
|
+
if (readDistillPublication(context, binding)?.plan.sha256 !== plan.sha256)
|
|
76
|
+
refuse("intent_not_retained");
|
|
77
|
+
return { changed: true, plan };
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/** Physical readback precedes the short SQL writer; the owner fences its
|
|
81
|
+
* accepted inputs in that writer. This receipt is not independent verification
|
|
82
|
+
* or terminal RUN acceptance. */
|
|
83
|
+
export function completeDistillPublication(context, binding, assertAuthority) {
|
|
84
|
+
if (typeof assertAuthority !== "function")
|
|
85
|
+
refuse("authority_fence_required");
|
|
86
|
+
const retained = readDistillPublication(context, binding);
|
|
87
|
+
if (!retained)
|
|
88
|
+
refuse("intent_required");
|
|
89
|
+
const actual = retained.plan.classify(binding.write_scope.destination_root);
|
|
90
|
+
if (actual.completed !== actual.total)
|
|
91
|
+
refuse("publication_not_applied");
|
|
92
|
+
return context.db.writeTransaction(() => {
|
|
93
|
+
assertAuthority();
|
|
94
|
+
const current = readDistillPublication(context, binding);
|
|
95
|
+
if (!current || current.plan.sha256 !== retained.plan.sha256)
|
|
96
|
+
refuse("intent_changed");
|
|
97
|
+
// The authority fence may perform reads/callbacks. Never certify an earlier
|
|
98
|
+
// filesystem observation if the destination changed while it ran.
|
|
99
|
+
const observed = current.plan.classify(binding.write_scope.destination_root);
|
|
100
|
+
if (observed.completed !== observed.total)
|
|
101
|
+
refuse("publication_not_applied");
|
|
102
|
+
if (!current.completed)
|
|
103
|
+
appendAudit(context, { projectId: binding.project_id, eventType: completeEvent, payload: receipt(binding, current.plan, true) });
|
|
104
|
+
if (!readDistillPublication(context, binding)?.completed)
|
|
105
|
+
refuse("completion_not_retained");
|
|
106
|
+
return { changed: !current.completed, plan: current.plan, receipt: receipt(binding, current.plan, true) };
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
/** Read original completed intent for downstream Stage handoff, never infer a
|
|
110
|
+
* write scope from its result proposal or recapture today's destination. */
|
|
111
|
+
export function readCompletedDistillPublication(context, expected) {
|
|
112
|
+
return context.db.readSnapshot(() => {
|
|
113
|
+
const row = context.db.get("SELECT value_json FROM runtime_config WHERE key=?", [key(expected)]);
|
|
114
|
+
if (!row || Buffer.byteLength(row.value_json) > 180 * 1024 * 1024)
|
|
115
|
+
refuse("completed_intent_required");
|
|
116
|
+
let binding;
|
|
117
|
+
try {
|
|
118
|
+
binding = JSON.parse(row.value_json).binding;
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return refuse("invalid_retained_publication");
|
|
122
|
+
}
|
|
123
|
+
if (!binding || !binding.write_scope || !Array.isArray(binding.write_scope.paths)
|
|
124
|
+
|| typeof binding.user_inputs_sha256 !== "string" || !/^[a-f0-9]{64}$/.test(binding.user_inputs_sha256)
|
|
125
|
+
|| hybridDigest(binding) !== hybridDigest({ ...expected, write_scope: binding.write_scope, user_inputs_sha256: binding.user_inputs_sha256 }))
|
|
126
|
+
refuse("binding_changed");
|
|
127
|
+
const retained = readDistillPublication(context, binding);
|
|
128
|
+
if (!retained?.completed)
|
|
129
|
+
refuse("publication_not_complete");
|
|
130
|
+
return { binding: structuredClone(binding), plan: retained.plan, beforeDocuments: retained.beforeDocuments };
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
/** Serialization only, not proof of effects or Stage acceptance. Both publisher
|
|
134
|
+
* and handoff reader use exactly these bytes selected from committed intent. */
|
|
135
|
+
export function distillPublicationArtifact(binding, plan) {
|
|
136
|
+
const encoded = JSON.parse(plan.bytes().toString("utf8"));
|
|
137
|
+
const after = JSON.parse(Buffer.from(encoded.after_base64, "base64").toString("utf8"));
|
|
138
|
+
return Buffer.from(JSON.stringify({ schema_id: "dd-flow/distill-publication@1", binding, plan_sha256: plan.sha256,
|
|
139
|
+
documents: after.entries.filter((entry) => binding.write_scope.paths.includes(entry.path)),
|
|
140
|
+
before_documents: distillBeforeDocumentReferences(plan), receipt: receipt(binding, plan, true) }));
|
|
141
|
+
}
|
|
142
|
+
/** Retained packet custody, not a file with a persuasive name. Return null only
|
|
143
|
+
* when publication has not retained this projection; conflicting custody refuses.
|
|
144
|
+
* Identical old retries remain readable, but publishers must not append more. */
|
|
145
|
+
export function readDistillPublicationReference(context, binding, plan, logicalPath) {
|
|
146
|
+
if (!logicalPath || path.isAbsolute(logicalPath) || logicalPath.includes("\\") || logicalPath.split("/").some(part => !part || part === "." || part === ".."))
|
|
147
|
+
refuse("invalid_publication_locator");
|
|
148
|
+
const run = context.db.get("SELECT run_root FROM runs WHERE project_id=? AND id=?", [binding.project_id, binding.run_id]);
|
|
149
|
+
if (!run?.run_root)
|
|
150
|
+
refuse("run_home_required");
|
|
151
|
+
const file = path.join(run.run_root, logicalPath), bytes = distillPublicationArtifact(binding, plan), sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
152
|
+
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')='promote' AND json_extract(payload_json,'$.attempt')=? LIMIT 33", [binding.project_id, binding.run_id, binding.attempt]);
|
|
153
|
+
if (rows.length > 32 || rows.some(row => Buffer.byteLength(row.payload_json) > 8 * 1024 * 1024))
|
|
154
|
+
refuse("unbounded_publication_packets");
|
|
155
|
+
const packets = rows.map(row => JSON.parse(row.payload_json)).filter(packet => typeof packet.file === "string"
|
|
156
|
+
&& resolveStagePublicationPath(context, binding.project_id, binding.run_id, packet.file) === file);
|
|
157
|
+
if (!packets.length)
|
|
158
|
+
return null;
|
|
159
|
+
if (packets.some(packet => packet.sha256 !== sha256 || packet.base64 !== bytes.toString("base64")))
|
|
160
|
+
refuse("publication_packet_changed");
|
|
161
|
+
return { root: "run", path: logicalPath, sha256 };
|
|
162
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import { AppError } from "../shared/errors.js";
|
|
4
|
+
import { entityReferenceVariants } from "../shared/entity-references.js";
|
|
5
|
+
import { distillStages } from "../domain/distill-flow.js";
|
|
6
|
+
import { requireProjectByRoot } from "./projects.js";
|
|
7
|
+
import { resolveProjectRoot } from "../storage/paths.js";
|
|
8
|
+
import { readFlowRunExecutionBundle } from "./runs.js";
|
|
9
|
+
import { readDiagnosticSnapshot, readDiagnosticSnapshotCustody, diagnosticSnapshotPaths } from "./diagnostic-snapshot.js";
|
|
10
|
+
import { PreparedDescriptorStageStart } from "./descriptor-stage-start.js";
|
|
11
|
+
import { PreparedDescriptorStageCoordinator, readDescriptorStageInput } from "./descriptor-stage-coordinator.js";
|
|
12
|
+
import { PreparedDescriptorStageSettlement, readAcceptedDescriptorStageSettlement } from "./descriptor-stage-settlement.js";
|
|
13
|
+
import { PreparedDescriptorRunSettlement } from "./descriptor-run-settlement.js";
|
|
14
|
+
import { readAcceptedStageData, prepareStageReport, acceptStageReport, publishStageReport, publishStagePacket, resolveStagePublicationPath } from "./stage-report-renderer.js";
|
|
15
|
+
import { publishDistillBeforeDocuments } from "./distill-document-publication.js";
|
|
16
|
+
import { validateDistillStageResult, assertDistillStageScope } from "./distill-stage-results.js";
|
|
17
|
+
import { registerDistillStageGraph } from "./distill-work-registration.js";
|
|
18
|
+
import { PreparedDistillWorkCollection } from "./distill-work-collection.js";
|
|
19
|
+
import { collectDistillCandidates } from "./distill-work-results.js";
|
|
20
|
+
import { readCompletedDistillPublication, readDistillPublicationReference, distillPublicationArtifact } from "./distill-publication-custody.js";
|
|
21
|
+
import { verifyDiagnosticSources } from "./diagnostic-evidence.js";
|
|
22
|
+
import { readDistillUserInputs } from "./distill-user-inputs.js";
|
|
23
|
+
import { readPreparedMemoryFlowUserInputs } from "./memory-flow-user-inputs.js";
|
|
24
|
+
import { assertDiagnosticSessionIndependence } from "./diagnostic-work-collection.js";
|
|
25
|
+
import { stageLifecycleMatchKey } from "./hooks.js";
|
|
26
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
27
|
+
function refuse(reason) {
|
|
28
|
+
const correctable = reason.startsWith("changed_") || ["invalid_result_json", "invalid_result_details", "result_exceeds_bound",
|
|
29
|
+
"unsupported_critic_resolution", "unowned_publication_claim", "preservation_evidence_required", "destination_evidence_required"].includes(reason);
|
|
30
|
+
throw new AppError("distill_stage_invalid", "Distillation stage requires accepted research, scope and independent observed verification", 2, { reason, effect: "no_effect", ...(correctable ? { phase: "prepare", recoverable: true } : {}) });
|
|
31
|
+
}
|
|
32
|
+
function select(context, input) {
|
|
33
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot)), bundle = readFlowRunExecutionBundle(context, input);
|
|
34
|
+
if (!bundle || bundle.manifest.flow_id !== "mb-distill" || !distillStages.includes(input.stage))
|
|
35
|
+
refuse("registered_distill_stage_required");
|
|
36
|
+
const frozen = bundle.execution.snapshot().scope, snapshotSelection = { projectId: project.id, projectRoot: frozen.projectRoot,
|
|
37
|
+
sourceRoot: frozen.projectRoot, requestId: frozen.requestId, flowId: "mb-distill" };
|
|
38
|
+
const snapshot = readDiagnosticSnapshot(context, snapshotSelection, input.runId);
|
|
39
|
+
if (!snapshot || snapshot.root !== frozen.workspace)
|
|
40
|
+
refuse("snapshot_required");
|
|
41
|
+
return { bundle, snapshot, snapshotSelection, scope: { projectId: project.id, runId: input.runId, stage: input.stage } };
|
|
42
|
+
}
|
|
43
|
+
function observeSelectedPublication(publication) {
|
|
44
|
+
const root = publication.binding.write_scope.destination_root;
|
|
45
|
+
if (fs.realpathSync(root) !== root || !fs.lstatSync(root).isDirectory())
|
|
46
|
+
refuse("destination_changed");
|
|
47
|
+
const documents = JSON.parse(distillPublicationArtifact(publication.binding, publication.plan).toString("utf8")).documents;
|
|
48
|
+
verifyDiagnosticSources(root, documents);
|
|
49
|
+
for (const document of documents) {
|
|
50
|
+
let parent = root;
|
|
51
|
+
for (const part of document.path.split("/").slice(0, -1)) {
|
|
52
|
+
parent = path.join(parent, part);
|
|
53
|
+
if (!fs.lstatSync(parent).isDirectory())
|
|
54
|
+
refuse("destination_parent_changed");
|
|
55
|
+
}
|
|
56
|
+
const stat = fs.lstatSync(path.join(root, document.path));
|
|
57
|
+
if (!stat.isFile() || stat.nlink !== 1 || (stat.mode & 0o7777) !== document.mode)
|
|
58
|
+
refuse("published_output_changed");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** Internal handlers until descriptor, launch and installed family gates close.
|
|
62
|
+
* Existing generic lifecycle owns all allocation and physical settlement. */
|
|
63
|
+
export const distillStageHandlers = Object.freeze(distillStages.map(stage => Object.freeze({ flowId: "mb-distill", stage,
|
|
64
|
+
entryId: "distill-stage-entry", finishId: "distill-stage-finish", entry: startDistillStage, finish: finishDistillStage })));
|
|
65
|
+
export function startDistillStage(context, input) {
|
|
66
|
+
const selected = select(context, input);
|
|
67
|
+
const start = PreparedDescriptorStageStart.prepare(context, input).apply(context);
|
|
68
|
+
const coordinator = PreparedDescriptorStageCoordinator.prepare(context, input).apply(context, writer => {
|
|
69
|
+
if (hybridDigest(readDiagnosticSnapshotCustody(writer, selected.snapshotSelection, input.runId)) !== hybridDigest(selected.snapshot))
|
|
70
|
+
refuse("snapshot_changed");
|
|
71
|
+
});
|
|
72
|
+
if (["extract", "review"].includes(input.stage))
|
|
73
|
+
registerDistillStageGraph(context, input);
|
|
74
|
+
return { ...start, ...coordinator };
|
|
75
|
+
}
|
|
76
|
+
/** Projection fills CLI-owned registers, never new semantic findings. Explicit
|
|
77
|
+
* conflicting proposals refuse instead of being silently promoted. */
|
|
78
|
+
export function prepareDistillStageProjection(bytes, stage, prior, collection) {
|
|
79
|
+
if (!bytes.length || bytes.length > 1024 * 1024)
|
|
80
|
+
refuse("result_exceeds_bound");
|
|
81
|
+
let value;
|
|
82
|
+
try {
|
|
83
|
+
value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return refuse("invalid_result_json");
|
|
87
|
+
}
|
|
88
|
+
if (!value || typeof value !== "object" || !value.details || typeof value.details !== "object" || Array.isArray(value.details)
|
|
89
|
+
|| !Array.isArray(value.details.limitations) || value.details.limitations.some(item => typeof item !== "string"))
|
|
90
|
+
refuse("invalid_result_details");
|
|
91
|
+
const details = value.details;
|
|
92
|
+
const insert = (key, expected) => {
|
|
93
|
+
const proposed = details[key];
|
|
94
|
+
if (proposed !== undefined && (!Array.isArray(proposed) || proposed.length && hybridDigest(proposed) !== hybridDigest(expected)))
|
|
95
|
+
refuse(`changed_${key}`);
|
|
96
|
+
Object.assign(details, { [key]: structuredClone(expected) });
|
|
97
|
+
};
|
|
98
|
+
if (prior)
|
|
99
|
+
insert("coverage", prior.details.coverage);
|
|
100
|
+
if (stage === "preflight") {
|
|
101
|
+
insert("candidates", []);
|
|
102
|
+
insert("decisions", []);
|
|
103
|
+
}
|
|
104
|
+
else if (stage === "extract") {
|
|
105
|
+
if (!collection)
|
|
106
|
+
refuse("author_work_required");
|
|
107
|
+
insert("candidates", collectDistillCandidates(collection).candidates);
|
|
108
|
+
insert("decisions", []);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
if (!prior)
|
|
112
|
+
refuse("predecessor_required");
|
|
113
|
+
insert("candidates", prior.details.candidates);
|
|
114
|
+
if (stage !== "review")
|
|
115
|
+
insert("decisions", prior.details.decisions);
|
|
116
|
+
}
|
|
117
|
+
if (stage === "finish") {
|
|
118
|
+
if (!prior)
|
|
119
|
+
refuse("verified_predecessor_required");
|
|
120
|
+
insert("verification", prior.details.verification);
|
|
121
|
+
}
|
|
122
|
+
details.limitations = [...new Set([...(prior?.details.limitations ?? []), ...(collection ?? []).flatMap(work => work.result.limitations), ...details.limitations])];
|
|
123
|
+
return Buffer.from(JSON.stringify(value));
|
|
124
|
+
}
|
|
125
|
+
function verificationOwner(context, input, selected, settlement) {
|
|
126
|
+
const receipt = (writer, stage) => readAcceptedDescriptorStageSettlement(writer, { ...selected.scope, stage }, selected.bundle, entityReferenceVariants(input.runId).map(reference => stageLifecycleMatchKey("stage_finish", reference, stage, input.projectRoot)));
|
|
127
|
+
const read = (writer) => {
|
|
128
|
+
const promotion = receipt(writer, "promote"), verification = input.stage === "finish" ? receipt(writer, "verify") : null;
|
|
129
|
+
const physical = (sessionId) => {
|
|
130
|
+
const row = writer.db.get("SELECT harness,provider_session_id FROM sessions WHERE project_id=? AND run_id=? AND session_id=?", [selected.scope.projectId, input.runId, sessionId]);
|
|
131
|
+
if (!row?.harness || !row.provider_session_id)
|
|
132
|
+
refuse("verification_physical_owner_missing");
|
|
133
|
+
return row;
|
|
134
|
+
};
|
|
135
|
+
const verifier = physical(verification?.binding.sessionId ?? settlement.coordinatorSessionId()), author = physical(promotion.binding.sessionId);
|
|
136
|
+
assertDiagnosticSessionIndependence([verifier], [author]);
|
|
137
|
+
return { promotion, verification, verifier, author };
|
|
138
|
+
};
|
|
139
|
+
const original = context.db.readSnapshot(() => read(context));
|
|
140
|
+
return (writer) => { if (hybridDigest(read(writer)) !== hybridDigest(original))
|
|
141
|
+
refuse("verification_owner_changed"); };
|
|
142
|
+
}
|
|
143
|
+
export function finishDistillStage(context, input) {
|
|
144
|
+
const selected = select(context, input), { bundle, snapshot, scope } = selected, stage = input.stage;
|
|
145
|
+
const packet = readDescriptorStageInput(context, scope, bundle, true, true), previousStage = packet.input.predecessor?.stage;
|
|
146
|
+
const previous = previousStage ? readAcceptedStageData(context, { ...scope, stage: previousStage }) : null;
|
|
147
|
+
const prior = previous ? JSON.parse(previous.bytes.toString("utf8")) : null;
|
|
148
|
+
const collection = ["extract", "review"].includes(stage) ? PreparedDistillWorkCollection.prepare(context, input) : null;
|
|
149
|
+
const works = collection?.results() ?? null;
|
|
150
|
+
let bytes = prepareDistillStageProjection(input.bytes, stage, prior, works);
|
|
151
|
+
const binding = { runId: input.runId, stage, inputSha256: packet.inputSha256, snapshotSha256: snapshot.source_sha256,
|
|
152
|
+
snapshotRoot: diagnosticSnapshotPaths(context, snapshot, input.runId).root, runRoot: packet.runRoot,
|
|
153
|
+
support: bundle.manifest.inventory.filter(ref => ref.path.startsWith("support/")),
|
|
154
|
+
predecessors: packet.input.predecessor ? [{ stage: packet.input.predecessor.stage, ...packet.input.predecessor.result }] : [],
|
|
155
|
+
writeScope: prior?.details.write_scope ?? null, promotionReceipt: prior?.details.promotion_receipt ?? null };
|
|
156
|
+
let publication = null;
|
|
157
|
+
if (["promote", "verify", "finish"].includes(stage) && prior?.details.decisions.some(decision => ["adopt", "adapt"].includes(decision.disposition))) {
|
|
158
|
+
const review = readAcceptedStageData(context, { ...scope, stage: "review" });
|
|
159
|
+
const promotion = stage === "promote" ? packet : readDescriptorStageInput(context, { ...scope, stage: "promote" }, bundle, true, true);
|
|
160
|
+
publication = readCompletedDistillPublication(context, { project_id: scope.projectId, run_id: input.runId, attempt: promotion.attempt,
|
|
161
|
+
input_sha256: promotion.inputSha256, bundle_sha256: bundle.sha256, review_sha256: review.result.sha256 });
|
|
162
|
+
if (stage === "promote" && publication.binding.user_inputs_sha256 !== hybridDigest(readDistillUserInputs(context, input, bundle)))
|
|
163
|
+
refuse("user_inputs_changed");
|
|
164
|
+
binding.writeScope = publication.binding.write_scope;
|
|
165
|
+
binding.promotionReceipt = readDistillPublicationReference(context, publication.binding, publication.plan, `${bundle.descriptor.stage("promote").directory}/publication.json`);
|
|
166
|
+
if (!binding.promotionReceipt)
|
|
167
|
+
refuse("publication_reference_required");
|
|
168
|
+
publishDistillBeforeDocuments(packet.runRoot, publication.plan, publication.beforeDocuments);
|
|
169
|
+
publishStagePacket(context, scope.projectId, input.runId, "promote", path.join(packet.runRoot, binding.promotionReceipt.path));
|
|
170
|
+
const value = JSON.parse(bytes.toString("utf8"));
|
|
171
|
+
if (value.details.write_scope != null && hybridDigest(value.details.write_scope) !== hybridDigest(binding.writeScope)
|
|
172
|
+
|| value.details.promotion_receipt != null && hybridDigest(value.details.promotion_receipt) !== hybridDigest(binding.promotionReceipt))
|
|
173
|
+
refuse("unowned_publication_claim");
|
|
174
|
+
Object.assign(value.details, { write_scope: binding.writeScope, promotion_receipt: binding.promotionReceipt });
|
|
175
|
+
bytes = Buffer.from(JSON.stringify(value));
|
|
176
|
+
if (stage === "promote") {
|
|
177
|
+
const actual = publication.plan.classify(binding.writeScope.destination_root);
|
|
178
|
+
if (actual.completed !== actual.total)
|
|
179
|
+
refuse("publication_not_observed");
|
|
180
|
+
}
|
|
181
|
+
else
|
|
182
|
+
observeSelectedPublication(publication);
|
|
183
|
+
}
|
|
184
|
+
const result = validateDistillStageResult(bytes, bundle.descriptor.stageBlob(stage, "result_schema"), binding);
|
|
185
|
+
if (publication && ["verify", "finish"].includes(stage)) {
|
|
186
|
+
const artifact = JSON.parse(distillPublicationArtifact(publication.binding, publication.plan).toString("utf8"));
|
|
187
|
+
const evidence = result.details.verification.flatMap(check => check.evidence);
|
|
188
|
+
if (artifact.before_documents.some((document) => !evidence.some(ref => ref.root === "run" && ref.path === document.source.path && ref.sha256 === document.source.sha256)))
|
|
189
|
+
refuse("preservation_evidence_required");
|
|
190
|
+
if (artifact.documents.some((document) => !evidence.some(ref => ref.root === "destination" && ref.path === document.path && ref.sha256 === document.sha256))
|
|
191
|
+
|| result.details.verification.some(check => !check.evidence.some(ref => ref.root === "destination" && binding.writeScope.paths.includes(ref.path))))
|
|
192
|
+
refuse("destination_evidence_required");
|
|
193
|
+
}
|
|
194
|
+
if (prior)
|
|
195
|
+
assertDistillStageScope(result, prior);
|
|
196
|
+
if (stage === "review")
|
|
197
|
+
for (const decision of result.details.decisions) {
|
|
198
|
+
if (!works?.some(work => work.result.decisions.some(vote => vote.candidate_id === decision.candidate_id && vote.disposition === decision.disposition)))
|
|
199
|
+
refuse("unsupported_critic_resolution");
|
|
200
|
+
}
|
|
201
|
+
const settlement = PreparedDescriptorStageSettlement.prepare(context, { ...input, bytes });
|
|
202
|
+
const independent = publication && ["verify", "finish"].includes(stage) ? verificationOwner(context, input, selected, settlement) : null;
|
|
203
|
+
const authors = stage === "finish" ? PreparedDistillWorkCollection.prepare(context, { ...input, stage: "extract" }) : null;
|
|
204
|
+
const critics = stage === "finish" ? PreparedDistillWorkCollection.prepare(context, { ...input, stage: "review" }) : null;
|
|
205
|
+
const report = authors && critics ? prepareStageReport({ run_id: input.runId, stage: "finish", verdict: "completed", summary: result.summary,
|
|
206
|
+
distillation: { result, origins: collectDistillCandidates(authors.results()).origins, critics: critics.results() },
|
|
207
|
+
semantic: { acceptance: result.details.decisions.map(decision => `${decision.candidate_id}: ${decision.disposition}; publication=${binding.writeScope?.candidate_ids.includes(decision.candidate_id) ? "observed" : "not_selected"}; ${decision.reason}`),
|
|
208
|
+
checks: result.details.verification.map(check => `${check.candidate_id}: ${check.verdict}; ${check.summary}`), evidence: result.details.verification.flatMap(check => check.evidence.map(ref => `${ref.root}:${ref.path}@${ref.sha256}`)),
|
|
209
|
+
next_action: "Completed only the accepted scope; rejected, duplicate and unselected candidates remain in the register." } }) : null;
|
|
210
|
+
const reportRoot = path.join(packet.runRoot, bundle.descriptor.stage(stage).directory);
|
|
211
|
+
const fence = (writer) => {
|
|
212
|
+
if (hybridDigest(readDiagnosticSnapshotCustody(writer, selected.snapshotSelection, input.runId)) !== hybridDigest(snapshot))
|
|
213
|
+
refuse("snapshot_changed");
|
|
214
|
+
if (previous && hybridDigest(readAcceptedStageData(writer, { ...scope, stage: previousStage })) !== hybridDigest(previous))
|
|
215
|
+
refuse("predecessor_changed");
|
|
216
|
+
collection?.assertCustody(writer);
|
|
217
|
+
authors?.assertCustody(writer);
|
|
218
|
+
critics?.assertCustody(writer);
|
|
219
|
+
independent?.(writer);
|
|
220
|
+
if (publication) {
|
|
221
|
+
const { write_scope, user_inputs_sha256, ...identity } = publication.binding;
|
|
222
|
+
const current = readCompletedDistillPublication(writer, identity);
|
|
223
|
+
if (current.plan.sha256 !== publication.plan.sha256 || hybridDigest(current.binding) !== hybridDigest(publication.binding)
|
|
224
|
+
|| stage === "promote" && hybridDigest(readPreparedMemoryFlowUserInputs(writer, scope, "mb-distill", bundle)) !== user_inputs_sha256
|
|
225
|
+
|| hybridDigest(write_scope) !== hybridDigest(binding.writeScope)
|
|
226
|
+
|| hybridDigest(readDistillPublicationReference(writer, current.binding, current.plan, `${bundle.descriptor.stage("promote").directory}/publication.json`)) !== hybridDigest(binding.promotionReceipt))
|
|
227
|
+
refuse("publication_changed");
|
|
228
|
+
}
|
|
229
|
+
if (report) {
|
|
230
|
+
const rows = writer.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type='flow_run.stage_report' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.stage')='finish' AND json_extract(payload_json,'$.attempt')=? LIMIT 2", [scope.projectId, input.runId, packet.attempt]);
|
|
231
|
+
if (rows.length > 1)
|
|
232
|
+
refuse("ambiguous_terminal_report");
|
|
233
|
+
if (rows.length) {
|
|
234
|
+
const retained = JSON.parse(rows[0].payload_json);
|
|
235
|
+
if (retained.sha256 !== report.sha256 || hybridDigest(retained.bytes) !== hybridDigest(report.bytes)
|
|
236
|
+
|| resolveStagePublicationPath(writer, scope.projectId, input.runId, retained.root) !== reportRoot)
|
|
237
|
+
refuse("terminal_report_changed");
|
|
238
|
+
}
|
|
239
|
+
else
|
|
240
|
+
acceptStageReport(writer, { ...scope, root: reportRoot, prepared: report });
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
const accepted = settlement.apply(context, fence);
|
|
244
|
+
if (stage !== "finish")
|
|
245
|
+
return accepted;
|
|
246
|
+
const complete = PreparedDescriptorRunSettlement.prepare(context, input).apply(context, fence);
|
|
247
|
+
publishStageReport(context, scope.projectId, input.runId, "finish", reportRoot);
|
|
248
|
+
return { ...accepted, ...complete, reports: ["json", "md", "html"].map(extension => path.join(reportRoot, `stage-report.${extension}`)) };
|
|
249
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
3
|
+
import { AppError } from "../shared/errors.js";
|
|
4
|
+
import { distillAspects } from "../domain/distill-flow.js";
|
|
5
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
6
|
+
import { verifyDiagnosticCoverage, verifyDiagnosticSources } from "./diagnostic-evidence.js";
|
|
7
|
+
function refuse(reason) {
|
|
8
|
+
throw new AppError("distill_stage_result_invalid", "Distillation stage result lacks its exact input, reviewed scope or publication proof", 2, { reason, effect: "no_effect", ...(reason === "invalid_schema" ? { phase: "prepare", recoverable: true } : {}) });
|
|
9
|
+
}
|
|
10
|
+
const portable = (value) => Boolean(value) && !path.posix.isAbsolute(value) && !path.win32.isAbsolute(value)
|
|
11
|
+
&& !value.includes("\\") && !value.includes("\0") && value.split("/").every(part => Boolean(part) && ![".", "..", ".git", "node_modules"].includes(part));
|
|
12
|
+
/** Shape/evidence checks do not establish independent Work or publication
|
|
13
|
+
* custody. The registered handler must prepare those bindings and fence them
|
|
14
|
+
* again in its settlement transaction. */
|
|
15
|
+
export function validateDistillStageResult(bytes, schemaBytes, binding) {
|
|
16
|
+
if (!bytes.length || bytes.length > 1024 * 1024 || !schemaBytes.length || schemaBytes.length > 1024 * 1024)
|
|
17
|
+
refuse("result_exceeds_bound");
|
|
18
|
+
const validate = (() => {
|
|
19
|
+
try {
|
|
20
|
+
const schema = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(schemaBytes));
|
|
21
|
+
if (schema.$id !== "dd-flow/distill-stage-result@1")
|
|
22
|
+
refuse("frozen_schema_invalid");
|
|
23
|
+
return new Ajv2020({ strict: false, validateFormats: false }).compile(schema);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return refuse("frozen_schema_invalid");
|
|
27
|
+
}
|
|
28
|
+
})();
|
|
29
|
+
let result;
|
|
30
|
+
try {
|
|
31
|
+
result = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return refuse("invalid_schema");
|
|
35
|
+
}
|
|
36
|
+
if (!validate(result))
|
|
37
|
+
refuse("invalid_schema");
|
|
38
|
+
if (result.flow_id !== "mb-distill" || result.run_id !== binding.runId || result.stage !== binding.stage
|
|
39
|
+
|| result.input_sha256 !== binding.inputSha256 || result.snapshot_sha256 !== binding.snapshotSha256
|
|
40
|
+
|| hybridDigest(result.predecessors) !== hybridDigest(binding.predecessors))
|
|
41
|
+
refuse("foreign_stage_binding");
|
|
42
|
+
const details = result.details;
|
|
43
|
+
verifyDiagnosticCoverage(binding.snapshotRoot, distillAspects.map(aspect => aspect.id), details.coverage);
|
|
44
|
+
const ids = new Set();
|
|
45
|
+
const baseline = (refs) => {
|
|
46
|
+
if (refs.some(ref => !binding.support.some(source => source.path === ref.path && source.sha256 === ref.sha256)))
|
|
47
|
+
refuse("foreign_baseline");
|
|
48
|
+
};
|
|
49
|
+
for (const candidate of details.candidates) {
|
|
50
|
+
if (ids.has(candidate.id) || !details.coverage.some(row => row.id === candidate.aspect_id && row.applicability === "applicable"))
|
|
51
|
+
refuse("candidate_outside_selection");
|
|
52
|
+
ids.add(candidate.id);
|
|
53
|
+
baseline(candidate.baseline_refs);
|
|
54
|
+
verifyDiagnosticSources(binding.snapshotRoot, [...candidate.source_refs, ...candidate.usage_refs]);
|
|
55
|
+
}
|
|
56
|
+
const decided = new Set();
|
|
57
|
+
for (const decision of details.decisions) {
|
|
58
|
+
if (!ids.has(decision.candidate_id) || decided.has(decision.candidate_id))
|
|
59
|
+
refuse("foreign_or_duplicate_decision");
|
|
60
|
+
decided.add(decision.candidate_id);
|
|
61
|
+
baseline(decision.baseline_refs);
|
|
62
|
+
verifyDiagnosticSources(binding.snapshotRoot, decision.source_refs);
|
|
63
|
+
}
|
|
64
|
+
if (!["preflight", "extract"].includes(binding.stage) && decided.size !== ids.size)
|
|
65
|
+
refuse("unresolved_candidate");
|
|
66
|
+
if (hybridDigest(details.write_scope) !== hybridDigest(binding.writeScope)
|
|
67
|
+
|| hybridDigest(details.promotion_receipt) !== hybridDigest(binding.promotionReceipt))
|
|
68
|
+
refuse("unowned_publication_claim");
|
|
69
|
+
const scope = binding.writeScope;
|
|
70
|
+
if (!["verify", "finish"].includes(binding.stage) && details.verification.length)
|
|
71
|
+
refuse("verification_before_publication_review");
|
|
72
|
+
if (scope) {
|
|
73
|
+
if (!["promote", "verify", "finish"].includes(binding.stage) || !path.isAbsolute(scope.destination_root)
|
|
74
|
+
|| path.resolve(scope.destination_root) !== scope.destination_root || scope.paths.some(value => !portable(value))
|
|
75
|
+
|| scope.candidate_ids.some(id => !details.decisions.some(decision => decision.candidate_id === id && ["adopt", "adapt"].includes(decision.disposition))))
|
|
76
|
+
refuse("invalid_write_scope");
|
|
77
|
+
verifyDiagnosticSources(binding.runRoot, [scope.approval_source]);
|
|
78
|
+
}
|
|
79
|
+
if (binding.promotionReceipt)
|
|
80
|
+
verifyDiagnosticSources(binding.runRoot, [binding.promotionReceipt]);
|
|
81
|
+
if (binding.stage === "promote" && details.decisions.some(decision => ["adopt", "adapt"].includes(decision.disposition))
|
|
82
|
+
&& (!scope || !binding.promotionReceipt))
|
|
83
|
+
refuse("promotion_requires_accepted_scope_and_receipt");
|
|
84
|
+
const verified = new Set();
|
|
85
|
+
for (const observation of details.verification) {
|
|
86
|
+
if (!scope?.candidate_ids.includes(observation.candidate_id) || verified.has(observation.candidate_id))
|
|
87
|
+
refuse("foreign_verification");
|
|
88
|
+
verified.add(observation.candidate_id);
|
|
89
|
+
for (const evidence of observation.evidence)
|
|
90
|
+
verifyDiagnosticSources(evidence.root === "run" ? binding.runRoot : scope.destination_root, [evidence]);
|
|
91
|
+
}
|
|
92
|
+
if (["verify", "finish"].includes(binding.stage) && details.decisions.some(decision => ["adopt", "adapt"].includes(decision.disposition))
|
|
93
|
+
&& (!scope || !binding.promotionReceipt || scope.candidate_ids.some(id => !verified.has(id))
|
|
94
|
+
|| details.verification.some(observation => observation.verdict !== "verified")))
|
|
95
|
+
refuse("unverified_promotion");
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
export function assertDistillStageScope(result, prior) {
|
|
99
|
+
if (hybridDigest(result.details.coverage) !== hybridDigest(prior.details.coverage)
|
|
100
|
+
|| prior.details.limitations.some(value => !result.details.limitations.includes(value))
|
|
101
|
+
|| (prior.stage !== "preflight" && hybridDigest(result.details.candidates) !== hybridDigest(prior.details.candidates))
|
|
102
|
+
|| (!["preflight", "extract"].includes(prior.stage) && hybridDigest(result.details.decisions) !== hybridDigest(prior.details.decisions))
|
|
103
|
+
|| (prior.details.write_scope && hybridDigest(result.details.write_scope) !== hybridDigest(prior.details.write_scope)))
|
|
104
|
+
refuse("accepted_scope_changed");
|
|
105
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { distillAspects } from "../domain/distill-flow.js";
|
|
2
|
+
import { AppError } from "../shared/errors.js";
|
|
3
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
4
|
+
function refuse(reason) {
|
|
5
|
+
throw new AppError("distill_work_batch_invalid", "Distillation requires its accepted complete aspect selection", 2, { reason, effect: "no_effect" });
|
|
6
|
+
}
|
|
7
|
+
/** One selected aspect is one independent semantic unit. Capacity changes
|
|
8
|
+
* scheduling in the existing Work runtime, never selection or dependencies. */
|
|
9
|
+
export function distillWorkBatch(input) {
|
|
10
|
+
const ids = input.coverage.map(row => row.id);
|
|
11
|
+
if (input.flow_id !== "mb-distill" || !["extract", "review"].includes(input.stage)
|
|
12
|
+
|| ids.length !== distillAspects.length || new Set(ids).size !== ids.length
|
|
13
|
+
|| distillAspects.some(aspect => !ids.includes(aspect.id))
|
|
14
|
+
|| input.coverage.some(row => !row.reason.trim() || !["applicable", "not_applicable", "blocked", "out_of_scope"].includes(row.applicability)))
|
|
15
|
+
refuse("invalid_coverage");
|
|
16
|
+
const selected = distillAspects.filter(aspect => input.coverage.some(row => row.id === aspect.id && row.applicability === "applicable"));
|
|
17
|
+
if (input.stage === "extract" && input.candidates.length)
|
|
18
|
+
refuse("extract_cannot_accept_candidates");
|
|
19
|
+
if (new Set(input.candidates.map(candidate => candidate.id)).size !== input.candidates.length
|
|
20
|
+
|| input.candidates.some(candidate => !selected.some(aspect => aspect.id === candidate.aspect_id)))
|
|
21
|
+
refuse("candidate_outside_selection");
|
|
22
|
+
for (const aspect of selected)
|
|
23
|
+
if (!input.support.some(ref => ref.path === aspect.criterion))
|
|
24
|
+
refuse("missing_selected_criterion");
|
|
25
|
+
const review = input.stage === "review";
|
|
26
|
+
const assignments = review
|
|
27
|
+
? [{ id: "novelty", criterion: null, role: "critic" }, { id: "portability", criterion: null, role: "critic" }]
|
|
28
|
+
: selected.map(aspect => ({ id: aspect.id, criterion: aspect.criterion, role: "aspect" }));
|
|
29
|
+
return { works: assignments.map((assignment, index) => {
|
|
30
|
+
const key = `${assignment.role}-${String(index + 1).padStart(3, "0")}`;
|
|
31
|
+
const packet = { schema_id: "dd-flow/distill-work-input@1", ...structuredClone(input), key,
|
|
32
|
+
aspect_id: assignment.id, role: assignment.role, criterion: assignment.criterion };
|
|
33
|
+
return { key, depends_on: [], launch_policy: "fresh_agent_required",
|
|
34
|
+
result_schema: "dd-flow/distill-work-result@1",
|
|
35
|
+
task: (review
|
|
36
|
+
? `Independently review every candidate through the ${assignment.id} lens. Compare exact project usage and frozen canon baseline. Preserve observe/duplicate/reject decisions and disagreements. Adopt/adapt requires transferable applicability and sufficient evidence, not popularity or missing project compliance. Return one decision per candidate. Do not replace candidates, promote documents, change scope, allocate Work or claim another critic's review.`
|
|
37
|
+
: `Extract transferable practices only for ${assignment.id}. Read its frozen criterion and canon baseline, then actual assigned project sources. Do not audit project compliance. Explain novelty, usage, applicability, cost and complexity; distinguish established practice from plans. Keep domain and process lenses separate. Return an explicit checked/no-candidate explanation when nothing substantial is found. Decide whether external research can change the assessment and disclose use and sources. Do not mutate project/canon, promote documents, allocate Work or self-accept candidates.`)
|
|
38
|
+
+ " Read support references relative to run_home/execution-bundle; preserve their supplied support paths and hashes in baseline_refs. Project source_refs/usage_refs are relative to the assigned snapshot workspace with exact snapshot-file hashes, never canon, intake or RUN files. Old manual lifecycle/dispatch snippets in criteria are reference data, not execution authority.",
|
|
39
|
+
payload: { kind: review ? "distill-critic" : "distill-aspect", read_only: true,
|
|
40
|
+
distill: { ...packet, packet_sha256: hybridDigest(packet) } } };
|
|
41
|
+
}) };
|
|
42
|
+
}
|