@deksden-com/dd-flow-cli 0.9.0-beta.136 → 0.9.0-beta.138
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 +12 -0
- package/README.md +9 -0
- package/dist/build-info.json +6 -6
- package/dist/cli/argument-values.js +5 -5
- package/dist/cli/command-inputs.js +89 -20
- package/dist/cli/command-metadata.js +56 -3
- package/dist/cli/command-option-descriptions.js +5 -0
- package/dist/cli/executor-onboarding.js +8 -3
- package/dist/cli/flows.js +29 -0
- package/dist/cli/help.js +240 -6
- package/dist/cli/input-preparation.js +54 -5
- package/dist/cli/memory-flow-admission.js +44 -0
- package/dist/cli/memory-init-invocation.js +78 -0
- package/dist/cli/memory-init.js +240 -0
- package/dist/cli/memory-storage-admission.js +53 -0
- package/dist/cli/native-hook-ingress.js +1 -1
- package/dist/cli/response-file.js +2 -0
- package/dist/cli/run-cli.js +283 -160
- package/dist/cli/setup.js +9 -7
- package/dist/cli.js +54 -2
- package/dist/domain/stage-handler-identities.js +20 -0
- package/dist/harness-runtime/DD-ZCODE.md +13 -0
- package/dist/harness-runtime/bin/dd-opencode.mjs +3 -3
- package/dist/harness-runtime/lib/daemon-operations.d.mts +1 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +4 -3
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +4 -2
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +11 -5
- package/dist/harness-runtime/lib/dd-codex.mjs +22 -10
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +2 -1
- package/dist/harness-runtime/lib/dd-droid.mjs +2 -1
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +4 -2
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +20 -5
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +45 -10
- package/dist/harness-runtime/lib/dd-zcode.mjs +7 -3
- package/dist/harness-runtime/lib/delegation-instructions.mjs +5 -3
- package/dist/harness-runtime/lib/lifecycle-command.mjs +1 -0
- package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +50 -15
- package/dist/harness-runtime/lib/managed-flow-env.mjs +15 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +1 -1
- package/dist/harness-runtime/lib/native-request-proof.d.mts +2 -0
- package/dist/harness-runtime/lib/native-request-proof.mjs +65 -0
- package/dist/harness-runtime/lib/opencode-plugin.mjs +5 -1
- package/dist/harness-runtime/lib/operation-progress.mjs +3 -1
- package/dist/runtime/context.js +1 -1
- package/dist/schemas/discovery-result.schema.json +184 -0
- package/dist/schemas/execution-bundle.schema.json +205 -0
- package/dist/schemas/execution-settings.schema.json +162 -0
- package/dist/schemas/flow-descriptor.schema.json +54 -0
- package/dist/schemas/flow-run.schema.json +22 -0
- package/dist/schemas/harness-catalog.schema.json +80 -0
- package/dist/schemas/mb-init-compose-result.schema.json +457 -0
- package/dist/schemas/mb-init-discover-result.schema.json +500 -0
- package/dist/schemas/mb-init-finish-result.schema.json +422 -0
- package/dist/schemas/mb-init-preflight-result.schema.json +367 -0
- package/dist/schemas/mb-init-review-result.schema.json +396 -0
- package/dist/schemas/mb-init-verify-result.schema.json +391 -0
- package/dist/schemas/memory-bootstrap-receipt-v2.schema.json +67 -0
- package/dist/schemas/memory-bootstrap-receipt.schema.json +354 -0
- package/dist/schemas/memory-composition-work-result.schema.json +38 -0
- package/dist/schemas/memory-repair-work-result.schema.json +49 -0
- package/dist/schemas/memory-review-correction-plan.schema.json +30 -0
- package/dist/schemas/memory-review-work-result.schema.json +43 -0
- package/dist/schemas/model-catalog.schema.json +153 -0
- package/dist/schemas/profile-settings.schema.json +122 -0
- package/dist/schemas/release-manifest.schema.json +291 -0
- package/dist/schemas/run-engine-binding.schema.json +1 -1
- package/dist/services/check-runner-evidence.js +51 -4
- package/dist/services/cli-operation-classifier.js +8 -47
- package/dist/services/code-checks.js +387 -34
- package/dist/services/controller-child-observer.js +99 -0
- package/dist/services/controller-execution.js +88 -0
- package/dist/services/controller-fanout.js +79 -13
- package/dist/services/dashboard.js +8 -2
- package/dist/services/descriptor-coordinator-session.js +35 -0
- package/dist/services/descriptor-root-work.js +53 -0
- package/dist/services/descriptor-run-settlement.js +105 -0
- package/dist/services/descriptor-run-start.js +449 -0
- package/dist/services/descriptor-stage-coordinator.js +197 -0
- package/dist/services/descriptor-stage-settlement.js +222 -0
- package/dist/services/descriptor-stage-start.js +139 -0
- package/dist/services/engines.js +39 -22
- package/dist/services/eval-snapshots.js +83 -25
- package/dist/services/execution-bundles.js +263 -0
- package/dist/services/execution-policy.js +20 -16
- package/dist/services/external-work-launch.js +91 -11
- package/dist/services/flow-catalog.js +21 -0
- package/dist/services/flow-descriptors.js +82 -0
- package/dist/services/frozen-engine.js +242 -0
- package/dist/services/generic-execution.js +164 -0
- package/dist/services/harness-adapter.js +28 -2
- package/dist/services/hooks.js +24 -6
- package/dist/services/hybrid-contracts.js +31 -3
- package/dist/services/ids.js +29 -1
- package/dist/services/installation-lock.js +7 -4
- package/dist/services/installed-flow-source.js +183 -0
- package/dist/services/lifecycle-invocations.js +257 -78
- package/dist/services/memory-accepted-composition.js +77 -0
- package/dist/services/memory-baseline-preservation.js +65 -0
- package/dist/services/memory-completed-correction-wave.js +320 -0
- package/dist/services/memory-compose-handler.js +258 -0
- package/dist/services/memory-composition-acceptance.js +199 -0
- package/dist/services/memory-composition-graph.js +134 -0
- package/dist/services/memory-composition-registration.js +146 -0
- package/dist/services/memory-composition-work-custody.js +151 -0
- package/dist/services/memory-composition-work-results.js +96 -0
- package/dist/services/memory-corrected-review-acceptance.js +129 -0
- package/dist/services/memory-corrected-review-batch.js +30 -0
- package/dist/services/memory-corrected-review-findings.js +106 -0
- package/dist/services/memory-corrected-review-inputs.js +210 -0
- package/dist/services/memory-corrected-review-projection.js +78 -0
- package/dist/services/memory-corrected-review-receipt.js +170 -0
- package/dist/services/memory-corrected-review-registration.js +191 -0
- package/dist/services/memory-correction-check-receipt.js +219 -0
- package/dist/services/memory-correction-input-owner.js +6 -0
- package/dist/services/memory-discover-handler.js +104 -0
- package/dist/services/memory-finish-evidence.js +435 -0
- package/dist/services/memory-finish-handler.js +215 -0
- package/dist/services/memory-finish-integration.js +280 -0
- package/dist/services/memory-finish-readiness.js +122 -0
- package/dist/services/memory-git-integration.js +151 -0
- package/dist/services/memory-historical-corrected-review.js +206 -0
- package/dist/services/memory-historical-original-review.js +130 -0
- package/dist/services/memory-historical-repair-registration.js +99 -0
- package/dist/services/memory-historical-second-review.js +128 -0
- package/dist/services/memory-init-bootstrap-readiness.js +210 -0
- package/dist/services/memory-init-contexts.js +89 -0
- package/dist/services/memory-init-controller-start.js +84 -0
- package/dist/services/memory-init-engine-release.js +223 -0
- package/dist/services/memory-init-git-policy.js +136 -0
- package/dist/services/memory-init-lint-capability.js +185 -0
- package/dist/services/memory-init-native-capacity.js +241 -0
- package/dist/services/memory-init-preflight.js +312 -0
- package/dist/services/memory-init-project-bootstrap.js +599 -0
- package/dist/services/memory-init-request-candidate.js +157 -0
- package/dist/services/memory-init-request-input.js +66 -0
- package/dist/services/memory-init-resource-storage.js +125 -0
- package/dist/services/memory-init-run-allocation.js +81 -0
- package/dist/services/memory-init-run-bundle.js +98 -0
- package/dist/services/memory-init-start.js +175 -0
- package/dist/services/memory-init-startup-custody.js +80 -0
- package/dist/services/memory-init-startup-input.js +240 -0
- package/dist/services/memory-init-storage.js +81 -0
- package/dist/services/memory-init-stores.js +359 -0
- package/dist/services/memory-init-worktrunk-capability.js +172 -0
- package/dist/services/memory-layer-publication.js +611 -0
- package/dist/services/memory-layer.js +156 -0
- package/dist/services/memory-lint-config.js +85 -0
- package/dist/services/memory-native-review-copy.js +165 -0
- package/dist/services/memory-normal-flow-entry.js +75 -0
- package/dist/services/memory-ordinary-defaults.js +112 -0
- package/dist/services/memory-preflight-bootstrap-custody.js +115 -0
- package/dist/services/memory-preflight-handler.js +259 -0
- package/dist/services/memory-repair-current-view.js +299 -0
- package/dist/services/memory-repair-document-delta.js +451 -0
- package/dist/services/memory-repair-history.js +491 -0
- package/dist/services/memory-repair-physical-binding.js +312 -0
- package/dist/services/memory-repair-publication-files.js +152 -0
- package/dist/services/memory-repair-publication-intent.js +27 -0
- package/dist/services/memory-repair-publication-plan.js +160 -0
- package/dist/services/memory-repair-publication-source.js +225 -0
- package/dist/services/memory-repair-publication.js +794 -0
- package/dist/services/memory-repair-registration.js +239 -0
- package/dist/services/memory-repair-work-baseline.js +413 -0
- package/dist/services/memory-repair-work-completion.js +97 -0
- package/dist/services/memory-repair-work-custody.js +73 -0
- package/dist/services/memory-repair-work-results.js +89 -0
- package/dist/services/memory-repair-workspace.js +252 -0
- package/dist/services/memory-repair-writer-copy.js +735 -0
- package/dist/services/memory-result-binding.js +173 -0
- package/dist/services/memory-retained-review-authority.js +129 -0
- package/dist/services/memory-retained-review-results.js +70 -0
- package/dist/services/memory-retained-second-baseline.js +102 -0
- package/dist/services/memory-retained-second-repairs.js +156 -0
- package/dist/services/memory-review-acceptance.js +214 -0
- package/dist/services/memory-review-correction-plan.js +171 -0
- package/dist/services/memory-review-handler.js +585 -0
- package/dist/services/memory-review-inputs.js +200 -0
- package/dist/services/memory-review-receipt.js +180 -0
- package/dist/services/memory-review-registration.js +108 -0
- package/dist/services/memory-review-work-custody.js +124 -0
- package/dist/services/memory-review-work-input.js +72 -0
- package/dist/services/memory-review-work-results.js +73 -0
- package/dist/services/memory-second-corrected-review-inputs.js +165 -0
- package/dist/services/memory-second-correction-plan.js +76 -0
- package/dist/services/memory-second-repair-baseline.js +91 -0
- package/dist/services/memory-second-repair-launch.js +614 -0
- package/dist/services/memory-second-repair-registration.js +88 -0
- package/dist/services/memory-second-repair-source.js +187 -0
- package/dist/services/memory-second-review-acceptance.js +115 -0
- package/dist/services/memory-source-observations.js +117 -0
- package/dist/services/memory-stage-dispatch.js +28 -0
- package/dist/services/memory-stage-results.js +199 -0
- package/dist/services/memory-verification-acceptance.js +135 -0
- package/dist/services/memory-verification-checks.js +252 -0
- package/dist/services/memory-verification-receipt.js +159 -0
- package/dist/services/memory-verify-handler.js +135 -0
- package/dist/services/memory-work-result-preparation.js +63 -0
- package/dist/services/projects.js +17 -2
- package/dist/services/recovery-snapshot-database.js +416 -1
- package/dist/services/repair-intents.js +1 -1
- package/dist/services/review-copy.js +10 -2
- package/dist/services/run-control.js +80 -9
- package/dist/services/run-controller-capture.js +10 -3
- package/dist/services/run-controller-process.js +21 -0
- package/dist/services/run-controller-recovery.js +3 -1
- package/dist/services/run-controller-state.js +1 -1
- package/dist/services/run-controller.js +184 -48
- package/dist/services/run-recovery-guard.js +2 -0
- package/dist/services/run-recovery.js +145 -5
- package/dist/services/run-start-custody.js +177 -0
- package/dist/services/runs.js +319 -25
- package/dist/services/stage-context.js +9 -1
- package/dist/services/stage-handlers.js +74 -0
- package/dist/services/stage-lifecycle.js +2 -1
- package/dist/services/stage-report-renderer.js +76 -0
- package/dist/services/stage-work-graph.js +17 -0
- package/dist/services/vnext-code.js +3 -3
- package/dist/services/vnext-execution-profile.js +2 -2
- package/dist/services/vnext-fanout.js +136 -10
- package/dist/services/vnext-merge.js +8 -3
- package/dist/services/vnext-plan-review.js +6 -4
- package/dist/services/vnext-protocolize.js +60 -2
- package/dist/services/work-registry.js +616 -71
- package/dist/services/workspace-files.js +1 -1
- package/dist/storage/database.js +84 -6
- package/dist/storage/writer-contract.js +11 -7
- package/package.json +13 -3
- package/tools/create-canon-release.mjs +33 -0
- package/tools/produce-hybrid-release.mjs +41 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { AppError } from "../shared/errors.js";
|
|
4
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
5
|
+
import { runtimeHarness } from "./execution-policy.js";
|
|
6
|
+
import { RetainedExecutionBundle } from "./execution-bundles.js";
|
|
7
|
+
import { PreparedMemoryInitialReviewInputs } from "./memory-review-inputs.js";
|
|
8
|
+
import { readRegisteredMemoryReview, memoryReviewBatch, memoryReviewWorkerSchema } from "./memory-review-registration.js";
|
|
9
|
+
import { readWorkBatchCustody } from "./work-registry.js";
|
|
10
|
+
import { validateMemoryReviewWorkResult } from "./memory-review-work-results.js";
|
|
11
|
+
import { ObservedMemorySources } from "./memory-source-observations.js";
|
|
12
|
+
import { PreparedMemoryStageResult, memorySourceKey } from "./memory-stage-results.js";
|
|
13
|
+
const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
14
|
+
const preparationToken = Symbol("memory-initial-review-acceptance");
|
|
15
|
+
function refuse(reason) {
|
|
16
|
+
throw new AppError("memory_review_acceptance_invalid", "Independent review lacks registered and settled result custody", 2, { reason, effect: "no_effect" });
|
|
17
|
+
}
|
|
18
|
+
function json(bytes, bound) {
|
|
19
|
+
try {
|
|
20
|
+
if (typeof bytes !== "string" || !bytes.length || Buffer.byteLength(bytes) > bound)
|
|
21
|
+
refuse("unbounded_receipt");
|
|
22
|
+
const value = JSON.parse(bytes);
|
|
23
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
24
|
+
refuse("invalid_receipt");
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
if (error instanceof AppError)
|
|
29
|
+
throw error;
|
|
30
|
+
return refuse("invalid_receipt");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** SQL-only historical settlement selection, not a status-only completion
|
|
34
|
+
* check. No current Session activity or obsolete copy is required after the
|
|
35
|
+
* Work writer committed its unchanged-copy proof and result atomically.
|
|
36
|
+
*/
|
|
37
|
+
export function readMemoryReviewWorkSettlement(context, registration, workId, harness) {
|
|
38
|
+
const scope = registration.scope, allocation = readWorkBatchCustody(context, workId);
|
|
39
|
+
const run = context.db.get("SELECT run_root,run_dir FROM runs WHERE project_id=? AND id=?", [scope.project_id, scope.run_id]);
|
|
40
|
+
const root = run?.run_root ?? run?.run_dir;
|
|
41
|
+
const work = context.db.get("SELECT status,result,result_schema FROM works WHERE project_id=? AND run_id=? AND work_id=?", [scope.project_id, scope.run_id, workId]);
|
|
42
|
+
if (!root || !path.isAbsolute(root) || !work || work.status !== "completed" || work.result_schema !== memoryReviewWorkerSchema
|
|
43
|
+
|| typeof work.result !== "string" || !work.result.length || Buffer.byteLength(work.result) > 1024 * 1024)
|
|
44
|
+
refuse("review_work_not_settled");
|
|
45
|
+
const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type='memory.review_work_settlement' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.work_id')=? LIMIT 2", [scope.project_id, scope.run_id, workId]);
|
|
46
|
+
if (rows.length !== 1)
|
|
47
|
+
refuse("review_settlement_missing_or_ambiguous");
|
|
48
|
+
const receipt = json(rows[0].payload_json, 32768);
|
|
49
|
+
const links = context.db.all("SELECT id,session_id,hook_event_id,result_path,result_receipt_json,start_receipt_json,start_publication_status FROM work_sessions WHERE work_id=? AND status='completed' LIMIT 2", [workId]);
|
|
50
|
+
if (links.length !== 1 || context.db.get("SELECT 1 FROM work_sessions WHERE work_id=? AND status='running' LIMIT 1", [workId]))
|
|
51
|
+
refuse("completed_reviewer_session_missing");
|
|
52
|
+
const link = links[0], publication = json(link.result_receipt_json, 32768), start = json(link.start_receipt_json, 8 * 1024 * 1024);
|
|
53
|
+
const session = context.db.get("SELECT provider_session_id FROM sessions WHERE project_id=? AND run_id=? AND session_id=? AND harness=?", [scope.project_id, scope.run_id, link.session_id, harness]);
|
|
54
|
+
if (!session?.provider_session_id || link.start_publication_status !== "published" || link.result_path !== path.join(root, "works", workId, "result.json"))
|
|
55
|
+
refuse("review_session_or_publication_changed");
|
|
56
|
+
if (!context.db.get("SELECT 1 FROM hook_events WHERE project_id=? AND event_key=? AND session_id=? AND harness=? AND provider_session_id=? AND status='claimed' AND event_name='PreToolUse'", [scope.project_id, link.hook_event_id, link.session_id, harness, session.provider_session_id]))
|
|
57
|
+
refuse("review_start_hook_missing");
|
|
58
|
+
if (context.db.get("SELECT 1 FROM work_sessions ws JOIN works w ON w.work_id=ws.work_id JOIN sessions s ON s.session_id=ws.session_id AND s.project_id=w.project_id WHERE w.project_id=? AND w.run_id=? AND w.work_id<>? AND s.harness=? AND s.provider_session_id=? LIMIT 1", [scope.project_id, scope.run_id, workId, harness, session.provider_session_id]))
|
|
59
|
+
refuse("reviewer_not_independent");
|
|
60
|
+
if (start.version !== 2 || typeof start.context !== "string" || start.context_sha256 !== hash(start.context)
|
|
61
|
+
|| !Number.isSafeInteger(start.generation) || start.generation < 0)
|
|
62
|
+
refuse("review_start_receipt_changed");
|
|
63
|
+
const input = json(start.context, 4 * 1024 * 1024);
|
|
64
|
+
const view = registration.view;
|
|
65
|
+
const expectedInput = { schema_id: "dd-flow/review-input@3", parent_work_id: scope.parent_work_id, kind: "memory-review",
|
|
66
|
+
cycle: `memory-review-wave-${registration.wave}`, generation: start.generation,
|
|
67
|
+
workspace_fingerprint: view.source_sha256, source_policy: view.source_policy, source_sha256: view.source_sha256, run_inputs: view.run_inputs };
|
|
68
|
+
if (input.run_id !== scope.run_id || input.work_id !== workId || input.parent_work_id !== scope.parent_work_id
|
|
69
|
+
|| input.read_only !== true || input.result_schema !== memoryReviewWorkerSchema || hybridDigest(input.review_input) !== hybridDigest(expectedInput))
|
|
70
|
+
refuse("review_start_input_changed");
|
|
71
|
+
const value = json(work.result, 1024 * 1024), bytes = Buffer.from(work.result), sha256 = hash(bytes);
|
|
72
|
+
const expected = { schema_id: "dd-flow/memory-review-work-settlement@1", run_id: scope.run_id, work_id: workId,
|
|
73
|
+
work_session_id: link.id, session_id: link.session_id, harness, provider_session_id: session.provider_session_id,
|
|
74
|
+
scope, wave: registration.wave, unit_id: value.unit_id, input_sha256: registration.input_sha256,
|
|
75
|
+
registration_sha256: hybridDigest(registration), allocation_sha256: hybridDigest(allocation), start_receipt_sha256: hash(link.start_receipt_json), result_sha256: sha256 };
|
|
76
|
+
if (hybridDigest(receipt) !== hybridDigest(expected))
|
|
77
|
+
refuse("review_settlement_changed");
|
|
78
|
+
if (publication.work_id !== workId || publication.work_session_id !== link.id || publication.status !== "completed"
|
|
79
|
+
|| publication.result_sha256 !== sha256 || publication.previous_sha256 !== null && (typeof publication.previous_sha256 !== "string" || !/^[a-f0-9]{64}$/.test(publication.previous_sha256)))
|
|
80
|
+
refuse("review_result_publication_changed");
|
|
81
|
+
return { workId, bytes, result: { path: path.relative(root, link.result_path).split(path.sep).join("/"), sha256 },
|
|
82
|
+
authority: hybridDigest({ receipt, publication, start }), root };
|
|
83
|
+
}
|
|
84
|
+
const settled = readMemoryReviewWorkSettlement;
|
|
85
|
+
/** Initial-wave fan-in only. This is not repair admission or Stage settlement.
|
|
86
|
+
* Every applicable assignment must have its own independent settled Work.
|
|
87
|
+
*/
|
|
88
|
+
export class PreparedMemoryInitialReviewAcceptance {
|
|
89
|
+
static isPrepared(value) {
|
|
90
|
+
return typeof value === "object" && value !== null && #inputs in value;
|
|
91
|
+
}
|
|
92
|
+
#inputs;
|
|
93
|
+
#registration;
|
|
94
|
+
#harness;
|
|
95
|
+
#workers;
|
|
96
|
+
#results;
|
|
97
|
+
#observed;
|
|
98
|
+
constructor(token, inputs, registration, harness, workers, results, observed) {
|
|
99
|
+
if (token !== preparationToken)
|
|
100
|
+
refuse("prepared_wave_token_required");
|
|
101
|
+
this.#inputs = inputs;
|
|
102
|
+
this.#registration = structuredClone(registration);
|
|
103
|
+
this.#harness = harness;
|
|
104
|
+
this.#workers = workers;
|
|
105
|
+
this.#results = results;
|
|
106
|
+
this.#observed = observed;
|
|
107
|
+
Object.freeze(this);
|
|
108
|
+
}
|
|
109
|
+
static prepare(context, inputs, bundle) {
|
|
110
|
+
if (!(inputs instanceof PreparedMemoryInitialReviewInputs) || !(bundle instanceof RetainedExecutionBundle))
|
|
111
|
+
refuse("prepared_wave_required");
|
|
112
|
+
const view = inputs.value();
|
|
113
|
+
if (view.scope.bundle_sha256 !== bundle.sha256 || view.scope.execution_sha256 !== bundle.execution.sha256)
|
|
114
|
+
refuse("foreign_wave_bundle");
|
|
115
|
+
inputs.revalidate(context);
|
|
116
|
+
const registration = readRegisteredMemoryReview(context, view.scope, view.wave);
|
|
117
|
+
if (registration.input_sha256 !== inputs.sha256)
|
|
118
|
+
refuse("review_registration_changed");
|
|
119
|
+
const harness = runtimeHarness(bundle.execution.work("review").worker), batch = memoryReviewBatch(view);
|
|
120
|
+
const workers = context.db.readSnapshot(() => batch.works.map(item => settled(context, registration, registration.work_ids[item.key], harness)));
|
|
121
|
+
const results = workers.map((worker, index) => validateMemoryReviewWorkResult(worker.bytes, bundle.engine(), {
|
|
122
|
+
workId: worker.workId, packet: batch.works[index].payload.memory_review, references: view.run_inputs
|
|
123
|
+
}));
|
|
124
|
+
const root = context.db.get("SELECT run_root,run_dir FROM runs WHERE project_id=? AND id=?", [view.scope.project_id, view.scope.run_id]);
|
|
125
|
+
if (!root)
|
|
126
|
+
refuse("owned_run_missing");
|
|
127
|
+
const observed = ObservedMemorySources.capture({ run: root.run_root ?? root.run_dir }, workers.map(worker => ({ root: "run", path: worker.result.path })));
|
|
128
|
+
const hashes = observed.hashes();
|
|
129
|
+
if (workers.some(worker => hashes.get(memorySourceKey("run", worker.result.path)) !== worker.result.sha256))
|
|
130
|
+
refuse("published_review_result_changed");
|
|
131
|
+
const prepared = new PreparedMemoryInitialReviewAcceptance(preparationToken, inputs, registration, harness, workers, results, observed);
|
|
132
|
+
prepared.assertCustody(context);
|
|
133
|
+
return prepared;
|
|
134
|
+
}
|
|
135
|
+
results() { return structuredClone(this.#results); }
|
|
136
|
+
resultProjection() { return memoryInitialReviewProjection(this.#registration.view, this.#results, this.references()); }
|
|
137
|
+
references() {
|
|
138
|
+
return this.#workers.map(worker => ({ reviewer_work_id: worker.workId, input_sha256: this.#registration.input_sha256,
|
|
139
|
+
result: { ...worker.result }, unit_ids: [this.#results.find(result => result.work_id === worker.workId).unit_id] }));
|
|
140
|
+
}
|
|
141
|
+
/** A coordinator cannot suppress reviewer findings or resolve them without
|
|
142
|
+
* an admitted correction wave. Initial-wave findings are still open.
|
|
143
|
+
*/
|
|
144
|
+
assertResult(result) {
|
|
145
|
+
if (!(result instanceof PreparedMemoryStageResult))
|
|
146
|
+
refuse("prepared_result_required");
|
|
147
|
+
assertMemoryInitialReviewProjection(result.value(), this.#registration.view, this.#results, this.references());
|
|
148
|
+
}
|
|
149
|
+
/** SQL-only facts for an independent Stage acceptance receipt. This does
|
|
150
|
+
* not itself accept the Stage or authorize a correction/another wave. */
|
|
151
|
+
settlementProof(context, result) {
|
|
152
|
+
this.assertCustody(context);
|
|
153
|
+
this.assertResult(result);
|
|
154
|
+
return { registration_scope: structuredClone(this.#registration.scope), registration_sha256: hybridDigest(this.#registration),
|
|
155
|
+
wave: 0, workers: this.#workers.map(worker => ({ work_id: worker.workId, authority_sha256: worker.authority, result: { ...worker.result } })) };
|
|
156
|
+
}
|
|
157
|
+
assertCustody(context) {
|
|
158
|
+
this.#inputs.assertCustody(context);
|
|
159
|
+
if (hybridDigest(readRegisteredMemoryReview(context, this.#registration.scope, this.#registration.wave)) !== hybridDigest(this.#registration))
|
|
160
|
+
refuse("review_registration_changed");
|
|
161
|
+
for (const worker of this.#workers)
|
|
162
|
+
if (settled(context, this.#registration, worker.workId, this.#harness).authority !== worker.authority)
|
|
163
|
+
refuse("review_settlement_changed");
|
|
164
|
+
}
|
|
165
|
+
revalidate(context) { this.#inputs.revalidate(context); this.#observed.revalidate(); this.assertCustody(context); }
|
|
166
|
+
}
|
|
167
|
+
/** CLI-owned fan-in: narrative proposals cannot suppress or merge independent findings. */
|
|
168
|
+
export function memoryInitialReviewProjection(view, results, references) {
|
|
169
|
+
return { ...memoryReviewWaveProjection(results, view.diagnostic_findings),
|
|
170
|
+
units: view.unit_inventory.map(unit => ({ ...structuredClone(unit), outcome: unit.applicability === "not-applicable"
|
|
171
|
+
? "not-applicable" : results.find(result => result.unit_id === unit.id)?.outcome })),
|
|
172
|
+
details: { reviews: structuredClone(references), repair_wave: 0, repair_work_ids: [] } };
|
|
173
|
+
}
|
|
174
|
+
export function projectMemoryReviewResult(proposal, projection) {
|
|
175
|
+
return { ...proposal, units: projection.units, findings: projection.findings, outcome: projection.outcome, details: projection.details };
|
|
176
|
+
}
|
|
177
|
+
/** Pure projection validation, not registration or settlement authority. */
|
|
178
|
+
export function assertMemoryInitialReviewProjection(value, view, results, references) {
|
|
179
|
+
const projection = memoryReviewWaveProjection(results, view.diagnostic_findings);
|
|
180
|
+
const details = value.details;
|
|
181
|
+
if (!details || !Array.isArray(details.repair_work_ids) || !Array.isArray(value.findings) || !Array.isArray(value.units))
|
|
182
|
+
refuse("invalid_review_projection");
|
|
183
|
+
if (value.run_id !== view.scope.run_id || value.stage !== "review" || value.input_sha256 !== view.scope.input_sha256
|
|
184
|
+
|| value.outcome !== projection.outcome || details.repair_wave !== 0 || details.repair_work_ids.length
|
|
185
|
+
|| hybridDigest(value.findings) !== hybridDigest(projection.findings)
|
|
186
|
+
|| hybridDigest(details.reviews) !== hybridDigest(references))
|
|
187
|
+
refuse("review_claim_differs_from_settled_wave");
|
|
188
|
+
const units = value.units;
|
|
189
|
+
const inventory = view.unit_inventory;
|
|
190
|
+
if (!Array.isArray(inventory) || units.length !== inventory.length || new Set(units.map(unit => unit.id)).size !== inventory.length)
|
|
191
|
+
refuse("review_unit_coverage_changed");
|
|
192
|
+
for (const original of inventory) {
|
|
193
|
+
const unit = units.find(unit => unit.id === original.id), review = results.find(item => item.unit_id === original.id);
|
|
194
|
+
const expectedOutcome = original.applicability === "not-applicable" ? "not-applicable" : review?.outcome;
|
|
195
|
+
if (!unit || !expectedOutcome || unit.outcome !== expectedOutcome
|
|
196
|
+
|| hybridDigest([unit.id, unit.aspect, unit.applicability, unit.evidence, unit.artifacts])
|
|
197
|
+
!== hybridDigest([original.id, original.aspect, original.applicability, original.evidence, original.artifacts]))
|
|
198
|
+
refuse("review_unit_claim_changed");
|
|
199
|
+
if (original.applicability === "not-applicable" && hybridDigest([unit.reason, unit.next_action]) !== hybridDigest([original.reason, original.next_action]))
|
|
200
|
+
refuse("review_unit_applicability_changed");
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/** Stable projection only, never a substitute for SQL/physical fan-in proof.
|
|
204
|
+
* Hash-scoped finding ids avoid collisions between local reviewer namespaces.
|
|
205
|
+
*/
|
|
206
|
+
export function memoryReviewWaveProjection(results, diagnostics = []) {
|
|
207
|
+
return { outcome: results.some(result => result.outcome === "blocked") ? "blocked"
|
|
208
|
+
: diagnostics.length || results.some(result => result.outcome === "needs_repair") ? "needs_repair" : "accepted",
|
|
209
|
+
findings: [...diagnostics.map(finding => ({ ...structuredClone(finding), id: `MVF-${hybridDigest({ id: finding.id, unit_id: finding.unit_id }).slice(0, 32)}`, status: "open", resolution_refs: [] })), ...results.flatMap(result => result.findings.map(finding => ({
|
|
210
|
+
id: `MRF-${hybridDigest({ work_id: result.work_id, finding_id: finding.id }).slice(0, 32)}`,
|
|
211
|
+
unit_id: result.unit_id, severity: finding.severity, statement: finding.summary,
|
|
212
|
+
evidence: structuredClone(finding.evidence), status: "open", resolution_refs: []
|
|
213
|
+
})))] };
|
|
214
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
3
|
+
import { AppError } from "../shared/errors.js";
|
|
4
|
+
import { AdmittedFrozenEngine } from "./frozen-engine.js";
|
|
5
|
+
import { PreparedRetainedMemoryReviewResults } from "./memory-retained-review-results.js";
|
|
6
|
+
import { memoryReviewWaveProjection } from "./memory-review-acceptance.js";
|
|
7
|
+
import { ObservedMemorySources } from "./memory-source-observations.js";
|
|
8
|
+
import { memorySourceKey } from "./memory-stage-results.js";
|
|
9
|
+
import { reviewSourceHash, reviewEnvironmentAllowed } from "./review-copy.js";
|
|
10
|
+
import { memoryProjectConfigurationPath } from "./memory-composition-graph.js";
|
|
11
|
+
function refuse(reason) {
|
|
12
|
+
throw new AppError("memory_review_correction_invalid", "Correction plan differs from retained material findings", 2, { reason, effect: "no_effect" });
|
|
13
|
+
}
|
|
14
|
+
const alias = (value) => value.normalize("NFC").toLowerCase();
|
|
15
|
+
const overlap = (a, b) => alias(a) === alias(b) || alias(a).startsWith(`${alias(b)}/`) || alias(b).startsWith(`${alias(a)}/`);
|
|
16
|
+
/** Exact project-owned document paths shared by graph and physical admission. */
|
|
17
|
+
export function assertMemoryRepairWritePaths(paths) {
|
|
18
|
+
if (!Array.isArray(paths) || !paths.length || paths.length > 512)
|
|
19
|
+
refuse("foreign_correction_write");
|
|
20
|
+
if (paths.some(logical => typeof logical !== "string"))
|
|
21
|
+
refuse("foreign_correction_write");
|
|
22
|
+
if (new Set(paths.map(alias)).size !== paths.length)
|
|
23
|
+
refuse("aliased_correction_paths");
|
|
24
|
+
for (const logical of paths) {
|
|
25
|
+
if (typeof logical !== "string" || !logical.startsWith(".memory-bank/") || logical.includes("\\") || logical.includes("\0")
|
|
26
|
+
|| logical.length > 4096 || logical.split("/").some(part => !part || part === "." || part === "..")
|
|
27
|
+
|| logical.split("/").some(part => !reviewEnvironmentAllowed(alias(part)) || [".git", "node_modules", ".dd-flow"].includes(alias(part)))
|
|
28
|
+
|| !memoryProjectConfigurationPath(logical)
|
|
29
|
+
&& [".memory-bank/dd-flow", ".memory-bank/mbb"].some(canonical => overlap(logical, canonical)))
|
|
30
|
+
refuse("foreign_correction_write");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** Semantic graph checks after strict frozen-schema admission. This projection
|
|
34
|
+
* is not Work allocation, source-byte admission or filesystem permission.
|
|
35
|
+
*/
|
|
36
|
+
export function validateMemoryReviewCorrectionGraph(plan, units, findings) {
|
|
37
|
+
const material = new Map(findings.filter(finding => finding.severity !== "minor" && finding.status === "open").map(finding => [finding.id, finding]));
|
|
38
|
+
if (!material.size)
|
|
39
|
+
refuse("no_material_correction_required");
|
|
40
|
+
const inventory = new Map(units.filter(unit => unit.applicability === "applicable").map(unit => [unit.id, unit]));
|
|
41
|
+
const byKey = new Map(plan.works.map(work => [work.key, work]));
|
|
42
|
+
if (byKey.size !== plan.works.length || new Set(plan.works.map(work => alias(work.key))).size !== plan.works.length)
|
|
43
|
+
refuse("duplicate_correction_work");
|
|
44
|
+
const assigned = new Set();
|
|
45
|
+
for (const work of plan.works) {
|
|
46
|
+
if (!work.task.trim() || work.task.includes("\0"))
|
|
47
|
+
refuse("missing_correction_task");
|
|
48
|
+
if (work.depends_on.some(key => !byKey.has(key) || key === work.key))
|
|
49
|
+
refuse("foreign_correction_dependency");
|
|
50
|
+
assertMemoryRepairWritePaths(work.write_paths);
|
|
51
|
+
const targetUnits = new Set();
|
|
52
|
+
for (const id of work.finding_ids) {
|
|
53
|
+
const finding = material.get(id);
|
|
54
|
+
if (!finding || assigned.has(id))
|
|
55
|
+
refuse("foreign_or_repeated_correction_finding");
|
|
56
|
+
assigned.add(id);
|
|
57
|
+
targetUnits.add(finding.unit_id);
|
|
58
|
+
}
|
|
59
|
+
if (targetUnits.size !== work.unit_ids.length || work.unit_ids.some(id => !targetUnits.has(id) || !inventory.has(id)))
|
|
60
|
+
refuse("correction_unit_scope_changed");
|
|
61
|
+
}
|
|
62
|
+
if (assigned.size !== material.size)
|
|
63
|
+
refuse("missing_material_correction");
|
|
64
|
+
const remaining = new Map(plan.works.map(work => [work.key, work.depends_on.length]));
|
|
65
|
+
const children = new Map();
|
|
66
|
+
for (const work of plan.works)
|
|
67
|
+
for (const dependency of work.depends_on)
|
|
68
|
+
children.set(dependency, [...(children.get(dependency) ?? []), work.key]);
|
|
69
|
+
const ready = plan.works.filter(work => !work.depends_on.length).map(work => work.key), ancestors = new Map();
|
|
70
|
+
for (let cursor = 0; cursor < ready.length; cursor++) {
|
|
71
|
+
const work = byKey.get(ready[cursor]), prior = new Set(work.depends_on);
|
|
72
|
+
for (const dependency of work.depends_on)
|
|
73
|
+
for (const key of ancestors.get(dependency) ?? [])
|
|
74
|
+
prior.add(key);
|
|
75
|
+
ancestors.set(work.key, prior);
|
|
76
|
+
for (const child of children.get(work.key) ?? []) {
|
|
77
|
+
const count = remaining.get(child) - 1;
|
|
78
|
+
remaining.set(child, count);
|
|
79
|
+
if (!count)
|
|
80
|
+
ready.push(child);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (ready.length !== plan.works.length)
|
|
84
|
+
refuse("cyclic_correction_graph");
|
|
85
|
+
for (let index = 0; index < plan.works.length; index++) {
|
|
86
|
+
const left = plan.works[index];
|
|
87
|
+
for (const right of plan.works.slice(index + 1)) {
|
|
88
|
+
if (ancestors.get(left.key).has(right.key) || ancestors.get(right.key).has(left.key))
|
|
89
|
+
continue;
|
|
90
|
+
const readPaths = (work) => work.unit_ids.flatMap(id => {
|
|
91
|
+
const unit = inventory.get(id);
|
|
92
|
+
return [...unit.evidence, ...unit.artifacts].filter(ref => ["project", "workspace"].includes(ref.root)).map(ref => ref.path);
|
|
93
|
+
});
|
|
94
|
+
if (left.write_paths.some(a => right.write_paths.some(b => overlap(a, b)))
|
|
95
|
+
|| left.write_paths.some(a => readPaths(right).some(b => overlap(a, b)))
|
|
96
|
+
|| right.write_paths.some(a => readPaths(left).some(b => overlap(a, b))))
|
|
97
|
+
refuse("unordered_correction_work");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
export function validateMemoryReviewCorrectionPlan(bytes, engine, binding) {
|
|
102
|
+
try {
|
|
103
|
+
if (!(bytes instanceof Uint8Array) || !bytes.byteLength || bytes.byteLength > 1024 * 1024 || !(engine instanceof AdmittedFrozenEngine)
|
|
104
|
+
|| ![1, 2].includes(binding.wave))
|
|
105
|
+
refuse("invalid_correction_binding");
|
|
106
|
+
const schema = JSON.parse(engine.schema("memory-review-correction-plan").toString("utf8"));
|
|
107
|
+
const value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
108
|
+
if (schema.$id !== "dd-flow/memory-review-correction-plan@1" || !new Ajv2020({ strict: true, validateFormats: false }).compile(schema)(value))
|
|
109
|
+
refuse("invalid_correction_schema");
|
|
110
|
+
if (value.run_id !== binding.runId || value.wave !== binding.wave || value.review_sha256 !== binding.reviewSha256)
|
|
111
|
+
refuse("foreign_correction_identity");
|
|
112
|
+
validateMemoryReviewCorrectionGraph(value, binding.units, binding.findings);
|
|
113
|
+
return structuredClone(value);
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
if (error instanceof AppError && ["memory_review_correction_invalid", "frozen_engine_invalid"].includes(error.code))
|
|
117
|
+
throw error;
|
|
118
|
+
return refuse("invalid_correction_plan_or_binding");
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/** First correction admission before document mutation/Work registration.
|
|
122
|
+
* Restart after writes must restore the independently registered repair plan;
|
|
123
|
+
* it cannot reprepare this initial-byte view or rebase unexplained changes.
|
|
124
|
+
*/
|
|
125
|
+
export class PreparedMemoryInitialCorrectionPlan {
|
|
126
|
+
sha256;
|
|
127
|
+
#review;
|
|
128
|
+
#bytes;
|
|
129
|
+
#value;
|
|
130
|
+
#observed;
|
|
131
|
+
constructor(review, bytes, value, observed) {
|
|
132
|
+
this.#review = review;
|
|
133
|
+
this.#bytes = bytes;
|
|
134
|
+
this.#value = value;
|
|
135
|
+
this.#observed = observed;
|
|
136
|
+
this.sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
137
|
+
Object.freeze(this);
|
|
138
|
+
}
|
|
139
|
+
static prepare(context, review, bytes) {
|
|
140
|
+
if (!(review instanceof PreparedRetainedMemoryReviewResults))
|
|
141
|
+
refuse("prepared_review_results_required");
|
|
142
|
+
review.revalidate(context);
|
|
143
|
+
const results = review.results(), original = review.original(), registration = original.registration(), bundle = original.bundle();
|
|
144
|
+
if (results.some(result => result.outcome === "blocked"))
|
|
145
|
+
refuse("blocked_review_requires_resolution");
|
|
146
|
+
const value = validateMemoryReviewCorrectionPlan(bytes, bundle.engine(), { runId: registration.run_id, wave: 1,
|
|
147
|
+
reviewSha256: review.sha256, units: registration.view.unit_inventory, findings: memoryReviewWaveProjection(results, registration.view.diagnostic_findings).findings });
|
|
148
|
+
for (const work of value.works)
|
|
149
|
+
bundle.execution.work("review", work.profile_id ?? undefined);
|
|
150
|
+
const refs = registration.view.run_inputs.filter(ref => ref.root === "workspace");
|
|
151
|
+
const observed = ObservedMemorySources.capture({ workspace: bundle.execution.snapshot().scope.workspace }, refs.map(ref => ({ root: "workspace", path: ref.path })));
|
|
152
|
+
const hashes = observed.hashes();
|
|
153
|
+
if (refs.some(ref => hashes.get(memorySourceKey("workspace", ref.path)) !== ref.sha256))
|
|
154
|
+
refuse("original_documents_changed_before_correction");
|
|
155
|
+
const prepared = new PreparedMemoryInitialCorrectionPlan(review, Buffer.from(bytes), value, observed);
|
|
156
|
+
prepared.revalidate(context);
|
|
157
|
+
return prepared;
|
|
158
|
+
}
|
|
159
|
+
bytes() { return Buffer.from(this.#bytes); }
|
|
160
|
+
value() { return structuredClone(this.#value); }
|
|
161
|
+
review() { return this.#review; }
|
|
162
|
+
assertCustody(context) { this.#review.assertCustody(context); }
|
|
163
|
+
revalidate(context) {
|
|
164
|
+
this.#review.revalidate(context);
|
|
165
|
+
this.#observed.revalidate();
|
|
166
|
+
const original = this.#review.original(), view = original.registration().view;
|
|
167
|
+
if (reviewSourceHash(original.bundle().execution.snapshot().scope.workspace, view.run_inputs.filter(ref => ref.root === "workspace").map(ref => ref.path), view.base) !== view.source_sha256)
|
|
168
|
+
refuse("original_copy_source_changed_before_correction");
|
|
169
|
+
this.assertCustody(context);
|
|
170
|
+
}
|
|
171
|
+
}
|