@deksden-com/dd-flow-cli 0.9.0-beta.31 → 0.9.0-beta.32
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 +6 -0
- package/dist/build-info.json +3 -3
- package/dist/schemas/code-review-result.schema.json +1 -1
- package/dist/schemas/plan-review-result.schema.json +2 -2
- package/dist/services/eval-snapshots.js +2 -0
- package/dist/services/vnext-plan-review.js +4 -1
- package/dist/services/work-registry.js +20 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @deksden-com/dd-flow-cli
|
|
2
2
|
|
|
3
|
+
## 0.9.0-beta.32
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Reject reviewer results without per-aspect evidence when the Work is finished, and distinguish missing evidence from an aspect-coverage mismatch in legacy stage validation.
|
|
8
|
+
|
|
3
9
|
## 0.9.0-beta.31
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/dist/build-info.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"cli_package": "@deksden-com/dd-flow-cli",
|
|
3
|
-
"cli_version": "0.9.0-beta.
|
|
4
|
-
"cli_commit": "
|
|
5
|
-
"built_at": "2026-09-
|
|
3
|
+
"cli_version": "0.9.0-beta.32",
|
|
4
|
+
"cli_commit": "2d5d953e65e996d9f57d1d4edec97a730c578acb",
|
|
5
|
+
"built_at": "2026-09-07T18:27:14.770Z",
|
|
6
6
|
"built_with_canon": {
|
|
7
7
|
"version": "4.0.6",
|
|
8
8
|
"commit": "c6fc50cb3b5526ea0162ee4454d1ee36f17be2da",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"schema_id": {"const": "dd-flow/code-review-result@1"},
|
|
9
9
|
"verdict": {"enum": ["pass", "findings", "blocked"]},
|
|
10
10
|
"summary": {"type": "string", "minLength": 1},
|
|
11
|
-
"aspects": {"type": "array", "items": {"type": "object", "additionalProperties": false, "required": ["aspect_id", "verdict", "summary", "evidence_refs"], "properties": {"aspect_id": {"type": "string", "minLength": 1}, "verdict": {"enum": ["pass", "findings", "blocked"]}, "summary": {"type": "string", "minLength": 1}, "evidence_refs": {"type": "array", "items": {"type": "string"}}}}},
|
|
11
|
+
"aspects": {"type": "array", "items": {"type": "object", "additionalProperties": false, "required": ["aspect_id", "verdict", "summary", "evidence_refs"], "properties": {"aspect_id": {"type": "string", "minLength": 1}, "verdict": {"enum": ["pass", "findings", "blocked"]}, "summary": {"type": "string", "minLength": 1}, "evidence_refs": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}}}}},
|
|
12
12
|
"findings": {"type": "array", "items": {"type": "object", "additionalProperties": false, "required": ["finding_id", "aspect_id", "priority", "problem", "impact", "required_outcome", "evidence_refs", "obligation_refs"], "properties": {"finding_id": {"type": "string", "pattern": "^FIND-[0-9]{3}$"}, "aspect_id": {"type": "string", "minLength": 1}, "priority": {"enum": ["p0", "p1", "p2", "p3"]}, "problem": {"type": "string", "minLength": 1}, "impact": {"type": "string", "minLength": 1}, "required_outcome": {"type": "string", "minLength": 1}, "evidence_refs": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}}, "obligation_refs": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}}}}}
|
|
13
13
|
}
|
|
14
14
|
}
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"aspect_id": {"type": "string", "minLength": 1},
|
|
21
21
|
"verdict": {"enum": ["pass", "watch", "needs_changes", "blocked"]},
|
|
22
22
|
"summary": {"type": "string", "minLength": 1},
|
|
23
|
-
"evidence_refs": {"type": "array", "items": {"type": "string", "minLength": 1}},
|
|
23
|
+
"evidence_refs": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}},
|
|
24
24
|
"findings": {
|
|
25
25
|
"type": "array",
|
|
26
26
|
"items": {
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"finding_id": {"type": "string", "pattern": "^FIND-[0-9]{3}$"},
|
|
32
32
|
"severity": {"enum": ["blocker", "high", "medium", "low", "info"]},
|
|
33
33
|
"summary": {"type": "string", "minLength": 1},
|
|
34
|
-
"evidence_refs": {"type": "array", "items": {"type": "string", "minLength": 1}}
|
|
34
|
+
"evidence_refs": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}}
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
}
|
|
@@ -8,6 +8,7 @@ import { resolveProjectRoot } from "../storage/paths.js";
|
|
|
8
8
|
import { registerProject, requireProjectByRoot } from "./projects.js";
|
|
9
9
|
import { getFlowRunStatus, startFlowRun } from "./runs.js";
|
|
10
10
|
import { readVnextFlowDefinition } from "./vnext-specify.js";
|
|
11
|
+
import { assertRunWorkResultArtifacts } from "./work-registry.js";
|
|
11
12
|
/** Capture the only flow boundary which exists before `stage start --bootstrap` creates a RUN. */
|
|
12
13
|
export function createEvalBootstrapSnapshot(context, input) {
|
|
13
14
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
@@ -80,6 +81,7 @@ function captureEvalRunSnapshot(context, input) {
|
|
|
80
81
|
const project = requireProjectByRoot(context, projectRoot);
|
|
81
82
|
const status = getFlowRunStatus(context, { projectRoot, runId: input.runId });
|
|
82
83
|
assertDedicatedHome(context, project.id, input.runId);
|
|
84
|
+
assertRunWorkResultArtifacts(context, project.id, input.runId);
|
|
83
85
|
const recovery = typeof input.recoveryId === "string";
|
|
84
86
|
if (recovery) {
|
|
85
87
|
const guard = context.db.get("SELECT status, recovery_id FROM run_recovery_guards WHERE project_id = ? AND run_id = ? AND recovery_id = ?", [project.id, input.runId, input.recoveryId]);
|
|
@@ -423,8 +423,11 @@ function reviewerEvidenceFailures(context, input) {
|
|
|
423
423
|
continue;
|
|
424
424
|
}
|
|
425
425
|
const actual = result.aspects.map((item) => item.aspect_id);
|
|
426
|
-
if (actual.length !== group.aspect_ids.length || new Set(actual).size !== actual.length || group.aspect_ids.some((id) => !actual.includes(id))
|
|
426
|
+
if (actual.length !== group.aspect_ids.length || new Set(actual).size !== actual.length || group.aspect_ids.some((id) => !actual.includes(id)))
|
|
427
427
|
failures.push({ group: group.key, work_id: child.work_id, error: "reviewer_aspect_coverage_invalid", expected_aspects: group.aspect_ids, actual_aspects: actual });
|
|
428
|
+
const missingEvidence = result.aspects.filter((item) => item.evidence_refs.length === 0).map((item) => item.aspect_id);
|
|
429
|
+
if (missingEvidence.length)
|
|
430
|
+
failures.push({ group: group.key, work_id: child.work_id, error: "reviewer_aspect_evidence_missing", missing_aspects: missingEvidence });
|
|
428
431
|
for (const ref of result.aspects.flatMap((item) => [...item.evidence_refs, ...item.findings.flatMap((finding) => finding.evidence_refs)])) {
|
|
429
432
|
try {
|
|
430
433
|
assertPortableArtifactRef(ref, { workspaceRoot: input.workspaceRoot, runHome: input.runHome, runId: input.runId });
|
|
@@ -182,6 +182,17 @@ export function listWorks(context, input) {
|
|
|
182
182
|
}) };
|
|
183
183
|
}
|
|
184
184
|
export function showWork(context, id) { const work = requireWork(context, id); const sessions = context.db.all("SELECT ws.id, ws.work_id, ws.session_id, ws.hook_event_id, ws.status, ws.prompt_path, ws.result_path, ws.created_at, ws.completed_at, s.harness AS harness_id, COALESCE(s.provider_session_id, s.session_id) AS native_session_id FROM work_sessions ws LEFT JOIN sessions s ON s.project_id = ? AND s.session_id = ws.session_id WHERE ws.work_id = ? ORDER BY ws.created_at", [work.project_id, work.work_id]); return { work: { ...work, short_id: shortWorkId(work.work_id), payload: parsePayload(work), payload_json: undefined, depends_on: parseDependencies(work), sessions: sessions.map(({ session_id, harness_id, native_session_id, ...session }) => ({ ...session, ...(harness_id && native_session_id ? { session: publicSessionIdentity({ harness: harness_id, provider_session_id: native_session_id, session_id }) } : {}) })) } }; }
|
|
185
|
+
/** DB rows are authoritative; reject snapshots that would preserve a rewritten Work receipt. */
|
|
186
|
+
export function assertRunWorkResultArtifacts(context, projectId, runId) {
|
|
187
|
+
const works = context.db.all(`SELECT w.work_id, w.result, (SELECT ws.result_path FROM work_sessions ws WHERE ws.work_id = w.work_id ORDER BY ws.created_at DESC LIMIT 1) AS result_path FROM works w WHERE w.project_id = ? AND w.run_id = ? AND w.result_schema IS NOT NULL AND w.result IS NOT NULL AND w.status = 'completed'`, [projectId, runId]);
|
|
188
|
+
for (const work of works) {
|
|
189
|
+
if (!work.result_path || !fs.existsSync(work.result_path))
|
|
190
|
+
throw new AppError("work_result_artifact_missing", "Completed structured Work has no retained result artifact", 1, { work_id: work.work_id, result_path: work.result_path });
|
|
191
|
+
const actual = fs.readFileSync(work.result_path, "utf8");
|
|
192
|
+
if (actual !== work.result)
|
|
193
|
+
throw new AppError("work_result_artifact_drift", "Completed Work result artifact no longer matches its authoritative receipt", 1, { work_id: work.work_id, result_path: work.result_path, expected_sha256: crypto.createHash("sha256").update(work.result).digest("hex"), actual_sha256: crypto.createHash("sha256").update(actual).digest("hex") });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
185
196
|
export function mutateWorkDeps(context, input) {
|
|
186
197
|
const work = requireWork(context, input.workId);
|
|
187
198
|
if (input.action === "list")
|
|
@@ -619,11 +630,11 @@ export function resultSchemaGuidance(work, runId) {
|
|
|
619
630
|
"Each evidence entry has exactly obligation_ref (one assigned ref) and refs (nonempty list of existing evidence paths). Each resolution has finding_ref, summary and nonempty evidence_refs. Do not invent file names or finding ids."];
|
|
620
631
|
}
|
|
621
632
|
if (schema === "dd-flow/code-review-result@1") {
|
|
622
|
-
return [refs, "Assess every assigned aspect exactly once. Number findings locally inside this reviewer Work as FIND-001, FIND-002, ...; dd-flow adds the Work id to form the canonical reference.", "This is a shape template: replace labels with assigned ids and observed verdicts, fill evidence_refs using existing files from the task packet, and remove findings when none exist. Report only material, direct-evidence findings; taste and cosmetics are not findings:", "```json", JSON.stringify({ schema_id: schema, verdict: "pass | findings | blocked", summary: "Concise evidence-backed conclusion.", aspects: [{ aspect_id: "assigned_aspect_id", verdict: "pass | findings | blocked", summary: "Conclusion.", evidence_refs: [] }], findings: [{ finding_id: "FIND-001", aspect_id: "assigned_aspect_id", priority: "p0 | p1 | p2 | p3", problem: "Violated obligation or rule.", impact: "Concrete risk or failure.", required_outcome: "Smallest observable result that closes the defect.", evidence_refs: ["path/to/file"], obligation_refs: ["R-001 | AC-001 | policy ref"] }] }, null, 2), "```"];
|
|
633
|
+
return [refs, "Assess every assigned aspect exactly once. Number findings locally inside this reviewer Work as FIND-001, FIND-002, ...; dd-flow adds the Work id to form the canonical reference.", "This is a shape template: replace labels with assigned ids and observed verdicts, fill evidence_refs using existing files from the task packet, and remove findings when none exist. Report only material, direct-evidence findings; taste and cosmetics are not findings:", "```json", JSON.stringify({ schema_id: schema, verdict: "pass | findings | blocked", summary: "Concise evidence-backed conclusion.", aspects: [{ aspect_id: "assigned_aspect_id", verdict: "pass | findings | blocked", summary: "Conclusion.", evidence_refs: ["path/to/evidence"] }], findings: [{ finding_id: "FIND-001", aspect_id: "assigned_aspect_id", priority: "p0 | p1 | p2 | p3", problem: "Violated obligation or rule.", impact: "Concrete risk or failure.", required_outcome: "Smallest observable result that closes the defect.", evidence_refs: ["path/to/file"], obligation_refs: ["R-001 | AC-001 | policy ref"] }] }, null, 2), "```"];
|
|
623
634
|
}
|
|
624
635
|
if (schema !== "dd-flow/plan-review-result@1")
|
|
625
636
|
return [];
|
|
626
|
-
return [refs, "This is a shape template: replace labels with assigned ids and observed verdicts, fill evidence_refs using existing files from the task packet, and remove findings when none exist. Do not add fields. Number findings locally inside this reviewer Work as FIND-001, FIND-002, ...; dd-flow adds the Work id when the coordinator classifies them:", "```json", JSON.stringify({ schema_id: schema, plan_revision: 1, overall_verdict: "pass | watch | needs_changes | blocked", summary: "Concise evidence-backed conclusion.", aspects: [{ aspect_id: "assigned_aspect_id", verdict: "pass | watch | needs_changes | blocked", summary: "Conclusion for this aspect.", evidence_refs: [], findings: [{ finding_id: "FIND-001", severity: "high | medium | low | info", summary: "Problem, if any.", evidence_refs: ["path/to/file"] }] }] }, null, 2), "```"];
|
|
637
|
+
return [refs, "This is a shape template: replace labels with assigned ids and observed verdicts, fill evidence_refs using existing files from the task packet, and remove findings when none exist. Do not add fields. Number findings locally inside this reviewer Work as FIND-001, FIND-002, ...; dd-flow adds the Work id when the coordinator classifies them:", "```json", JSON.stringify({ schema_id: schema, plan_revision: 1, overall_verdict: "pass | watch | needs_changes | blocked", summary: "Concise evidence-backed conclusion.", aspects: [{ aspect_id: "assigned_aspect_id", verdict: "pass | watch | needs_changes | blocked", summary: "Conclusion for this aspect.", evidence_refs: ["path/to/evidence"], findings: [{ finding_id: "FIND-001", severity: "high | medium | low | info", summary: "Problem, if any.", evidence_refs: ["path/to/file"] }] }] }, null, 2), "```"];
|
|
627
638
|
}
|
|
628
639
|
export function validateCodeReviewResultIdentity(work, value) {
|
|
629
640
|
const group = codeReviewGroup(work);
|
|
@@ -652,6 +663,7 @@ export function validateCodeReviewResultIdentity(work, value) {
|
|
|
652
663
|
function validateCodeReviewResult(work, value, input) {
|
|
653
664
|
validateCodeReviewResultIdentity(work, value);
|
|
654
665
|
const result = value;
|
|
666
|
+
assertReviewAspectEvidence(work, result.aspects ?? [], "CODE");
|
|
655
667
|
const findings = result.findings ?? [];
|
|
656
668
|
// A reviewer is a child Work and cannot pause its coordinator-owned Stage.
|
|
657
669
|
// Its structured blocked result is evidence for the coordinator, which then
|
|
@@ -681,6 +693,7 @@ export function validatePlanReviewResult(work, value, input) {
|
|
|
681
693
|
const unexpected = ids.filter((id) => !expected.has(id));
|
|
682
694
|
if (new Set(ids).size !== ids.length || missing.length || unexpected.length)
|
|
683
695
|
throw new AppError("review_evidence_invalid", "PLAN reviewer result must assess every assigned aspect exactly once", 2, { work_id: work.work_id, group: group.key, missing, unexpected });
|
|
696
|
+
assertReviewAspectEvidence(work, aspects, "PLAN");
|
|
684
697
|
const findingIds = aspects.flatMap((aspect) => aspect.findings ?? []).map((finding) => finding.finding_id ?? "");
|
|
685
698
|
const invalid = findingIds.filter((id) => !/^FIND-\d{3}$/.test(id));
|
|
686
699
|
if (new Set(findingIds).size !== findingIds.length || invalid.length)
|
|
@@ -695,6 +708,11 @@ export function validatePlanReviewResult(work, value, input) {
|
|
|
695
708
|
assertPortableArtifactRef(ref, input);
|
|
696
709
|
}
|
|
697
710
|
}
|
|
711
|
+
function assertReviewAspectEvidence(work, aspects, stage) {
|
|
712
|
+
const missing = aspects.filter((aspect) => !Array.isArray(aspect.evidence_refs) || aspect.evidence_refs.length === 0).map((aspect) => aspect.aspect_id ?? "");
|
|
713
|
+
if (missing.length)
|
|
714
|
+
throw new AppError("review_evidence_invalid", `${stage} reviewer result requires non-empty evidence_refs for every assigned aspect`, 2, { work_id: work.work_id, missing_aspects: missing });
|
|
715
|
+
}
|
|
698
716
|
function codeReviewGroup(work) {
|
|
699
717
|
const payload = parsePayload(work);
|
|
700
718
|
const group = payload?.group;
|