@xenonbyte/da-vinci-workflow 0.2.2 → 0.2.4
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 +28 -0
- package/README.md +49 -14
- package/README.zh-CN.md +169 -14
- package/commands/claude/dv/breakdown.md +8 -0
- package/commands/claude/dv/build.md +16 -0
- package/commands/claude/dv/continue.md +4 -0
- package/commands/claude/dv/design.md +5 -2
- package/commands/claude/dv/tasks.md +14 -0
- package/commands/claude/dv/verify.md +11 -0
- package/commands/codex/prompts/dv-breakdown.md +8 -0
- package/commands/codex/prompts/dv-build.md +16 -0
- package/commands/codex/prompts/dv-continue.md +4 -0
- package/commands/codex/prompts/dv-design.md +5 -2
- package/commands/codex/prompts/dv-tasks.md +14 -0
- package/commands/codex/prompts/dv-verify.md +10 -0
- package/commands/gemini/dv/breakdown.toml +8 -0
- package/commands/gemini/dv/build.toml +16 -0
- package/commands/gemini/dv/continue.toml +4 -0
- package/commands/gemini/dv/design.toml +5 -2
- package/commands/gemini/dv/tasks.toml +14 -0
- package/commands/gemini/dv/verify.toml +10 -0
- package/commands/templates/dv-continue.shared.md +4 -0
- package/docs/discipline-and-orchestration-upgrade.md +83 -0
- package/docs/dv-command-reference.md +61 -2
- package/docs/execution-chain-migration.md +23 -0
- package/docs/execution-chain-plan.md +10 -3
- package/docs/mode-use-cases.md +2 -1
- package/docs/pencil-rendering-workflow.md +15 -12
- package/docs/prompt-entrypoints.md +5 -0
- package/docs/prompt-presets/README.md +1 -1
- package/docs/prompt-presets/desktop-app.md +3 -3
- package/docs/prompt-presets/mobile-app.md +3 -3
- package/docs/prompt-presets/tablet-app.md +3 -3
- package/docs/prompt-presets/web-app.md +3 -3
- package/docs/skill-usage.md +61 -38
- package/docs/workflow-examples.md +16 -13
- package/docs/workflow-overview.md +19 -0
- package/docs/zh-CN/dv-command-reference.md +59 -2
- package/docs/zh-CN/execution-chain-migration.md +23 -0
- package/docs/zh-CN/mode-use-cases.md +2 -1
- package/docs/zh-CN/pencil-rendering-workflow.md +15 -12
- package/docs/zh-CN/prompt-entrypoints.md +5 -0
- package/docs/zh-CN/prompt-presets/README.md +1 -1
- package/docs/zh-CN/prompt-presets/desktop-app.md +3 -3
- package/docs/zh-CN/prompt-presets/mobile-app.md +3 -3
- package/docs/zh-CN/prompt-presets/tablet-app.md +3 -3
- package/docs/zh-CN/prompt-presets/web-app.md +3 -3
- package/docs/zh-CN/skill-usage.md +61 -38
- package/docs/zh-CN/workflow-examples.md +15 -13
- package/docs/zh-CN/workflow-overview.md +19 -0
- package/examples/greenfield-spec-markupflow/.da-vinci/state/execution-signals/demo__lint-tasks.json +16 -0
- package/lib/audit-parsers.js +166 -10
- package/lib/audit.js +3 -26
- package/lib/cli.js +156 -2
- package/lib/design-source-registry.js +146 -0
- package/lib/execution-profile.js +143 -0
- package/lib/execution-signals.js +19 -1
- package/lib/lint-tasks.js +86 -2
- package/lib/planning-parsers.js +255 -18
- package/lib/save-current-design.js +790 -0
- package/lib/supervisor-review.js +3 -2
- package/lib/task-execution.js +160 -0
- package/lib/task-review.js +197 -0
- package/lib/verify.js +152 -1
- package/lib/workflow-bootstrap.js +2 -13
- package/lib/workflow-persisted-state.js +3 -1
- package/lib/workflow-state.js +503 -33
- package/lib/worktree-preflight.js +214 -0
- package/package.json +1 -1
- package/references/artifact-templates.md +56 -6
- package/tui/catalog.js +103 -0
- package/tui/index.js +2274 -418
package/lib/supervisor-review.js
CHANGED
|
@@ -67,7 +67,8 @@ function resolveCodexBinaryPath(explicitPath) {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
function normalizePositiveInt(value, fallback, minimum = 1, maximum = Number.POSITIVE_INFINITY) {
|
|
70
|
-
const
|
|
70
|
+
const raw = value === undefined || value === null ? "" : String(value).trim();
|
|
71
|
+
const parsed = Number.parseInt(raw, 10);
|
|
71
72
|
if (!Number.isFinite(parsed)) {
|
|
72
73
|
return fallback;
|
|
73
74
|
}
|
|
@@ -416,7 +417,7 @@ function extractMcpRuntimeGateStatus(markdownText) {
|
|
|
416
417
|
}
|
|
417
418
|
|
|
418
419
|
const statusMatch = String(section).match(
|
|
419
|
-
/(?:^|\n)\s*-\s*(?:Status|状态)\s*:\s*`?(PASS|WARN|BLOCK)`?\b/i
|
|
420
|
+
/(?:^|\n)\s*-\s*(?:Final runtime gate status|Status|状态)\s*:\s*`?(PASS|WARN|BLOCK)`?\b/i
|
|
420
421
|
);
|
|
421
422
|
return statusMatch ? String(statusMatch[1]).toUpperCase() : "";
|
|
422
423
|
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const { resolveChangeDir, unique } = require("./planning-parsers");
|
|
3
|
+
const { writeExecutionSignal } = require("./execution-signals");
|
|
4
|
+
|
|
5
|
+
const VALID_IMPLEMENTER_STATUSES = new Set([
|
|
6
|
+
"DONE",
|
|
7
|
+
"DONE_WITH_CONCERNS",
|
|
8
|
+
"NEEDS_CONTEXT",
|
|
9
|
+
"BLOCKED"
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
function normalizeStatus(value) {
|
|
13
|
+
const normalized = String(value || "").trim().toUpperCase();
|
|
14
|
+
if (!VALID_IMPLEMENTER_STATUSES.has(normalized)) {
|
|
15
|
+
throw new Error(
|
|
16
|
+
"`task-execution --status` must be one of DONE, DONE_WITH_CONCERNS, NEEDS_CONTEXT, BLOCKED."
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
return normalized;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function normalizeList(value) {
|
|
23
|
+
if (Array.isArray(value)) {
|
|
24
|
+
return unique(value.map((item) => String(item || "").trim()).filter(Boolean));
|
|
25
|
+
}
|
|
26
|
+
return unique(
|
|
27
|
+
String(value || "")
|
|
28
|
+
.split(/[,\n;]/)
|
|
29
|
+
.map((item) => item.trim())
|
|
30
|
+
.filter(Boolean)
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function normalizeTaskGroupId(value) {
|
|
35
|
+
const normalized = String(value || "").trim();
|
|
36
|
+
if (!normalized) {
|
|
37
|
+
throw new Error("`task-execution` requires `--task-group <id>`.");
|
|
38
|
+
}
|
|
39
|
+
return normalized;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function mapImplementerStatusToSignal(status) {
|
|
43
|
+
if (status === "DONE") {
|
|
44
|
+
return "PASS";
|
|
45
|
+
}
|
|
46
|
+
if (status === "DONE_WITH_CONCERNS" || status === "NEEDS_CONTEXT") {
|
|
47
|
+
return "WARN";
|
|
48
|
+
}
|
|
49
|
+
return "BLOCK";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function buildSurface(taskGroupId) {
|
|
53
|
+
const safeId = String(taskGroupId || "")
|
|
54
|
+
.trim()
|
|
55
|
+
.replace(/[^A-Za-z0-9._-]+/g, "_");
|
|
56
|
+
return `task-execution.${safeId}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function normalizeTaskExecutionEnvelope(input = {}) {
|
|
60
|
+
const taskGroupId = normalizeTaskGroupId(input.taskGroupId);
|
|
61
|
+
const status = normalizeStatus(input.status);
|
|
62
|
+
const summary = String(input.summary || "").trim();
|
|
63
|
+
if (!summary) {
|
|
64
|
+
throw new Error("`task-execution` requires `--summary <text>`.");
|
|
65
|
+
}
|
|
66
|
+
const changedFiles = normalizeList(input.changedFiles);
|
|
67
|
+
const testEvidence = normalizeList(input.testEvidence);
|
|
68
|
+
const concerns = normalizeList(input.concerns);
|
|
69
|
+
const blockers = normalizeList(input.blockers);
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
taskGroupId,
|
|
73
|
+
status,
|
|
74
|
+
summary,
|
|
75
|
+
changedFiles,
|
|
76
|
+
testEvidence,
|
|
77
|
+
concerns,
|
|
78
|
+
blockers,
|
|
79
|
+
recordedAt: new Date().toISOString()
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function writeTaskExecutionEnvelope(projectPathInput, options = {}) {
|
|
84
|
+
const projectRoot = path.resolve(projectPathInput || process.cwd());
|
|
85
|
+
const requestedChangeId = options.changeId ? String(options.changeId).trim() : "";
|
|
86
|
+
const resolved = resolveChangeDir(projectRoot, requestedChangeId);
|
|
87
|
+
if (!resolved.changeDir || !resolved.changeId) {
|
|
88
|
+
const resolveError =
|
|
89
|
+
Array.isArray(resolved.failures) && resolved.failures.length > 0
|
|
90
|
+
? String(resolved.failures[0] || "").trim()
|
|
91
|
+
: "";
|
|
92
|
+
throw new Error(resolveError || "Unable to resolve active change for task-execution.");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const envelope = normalizeTaskExecutionEnvelope(options);
|
|
96
|
+
const signalPath = writeExecutionSignal(projectRoot, {
|
|
97
|
+
changeId: resolved.changeId,
|
|
98
|
+
surface: buildSurface(envelope.taskGroupId),
|
|
99
|
+
status: mapImplementerStatusToSignal(envelope.status),
|
|
100
|
+
advisory: false,
|
|
101
|
+
strict: true,
|
|
102
|
+
failures: envelope.status === "BLOCKED" ? envelope.blockers : [],
|
|
103
|
+
warnings:
|
|
104
|
+
envelope.status === "DONE_WITH_CONCERNS" || envelope.status === "NEEDS_CONTEXT"
|
|
105
|
+
? envelope.concerns
|
|
106
|
+
: [],
|
|
107
|
+
notes: [envelope.summary, ...envelope.testEvidence.map((item) => `test: ${item}`)],
|
|
108
|
+
details: {
|
|
109
|
+
type: "task_execution",
|
|
110
|
+
envelope
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
status: mapImplementerStatusToSignal(envelope.status),
|
|
116
|
+
projectRoot,
|
|
117
|
+
changeId: resolved.changeId,
|
|
118
|
+
taskGroupId: envelope.taskGroupId,
|
|
119
|
+
implementerStatus: envelope.status,
|
|
120
|
+
summary: envelope.summary,
|
|
121
|
+
changedFiles: envelope.changedFiles,
|
|
122
|
+
testEvidence: envelope.testEvidence,
|
|
123
|
+
concerns: envelope.concerns,
|
|
124
|
+
blockers: envelope.blockers,
|
|
125
|
+
signalPath
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function formatTaskExecutionReport(result) {
|
|
130
|
+
const lines = [
|
|
131
|
+
"Task execution envelope",
|
|
132
|
+
`Project: ${result.projectRoot}`,
|
|
133
|
+
`Change: ${result.changeId}`,
|
|
134
|
+
`Task group: ${result.taskGroupId}`,
|
|
135
|
+
`Implementer status: ${result.implementerStatus}`,
|
|
136
|
+
`Signal status: ${result.status}`,
|
|
137
|
+
`Summary: ${result.summary}`,
|
|
138
|
+
`Signal path: ${result.signalPath}`
|
|
139
|
+
];
|
|
140
|
+
if (result.changedFiles.length > 0) {
|
|
141
|
+
lines.push(`Changed files: ${result.changedFiles.join(", ")}`);
|
|
142
|
+
}
|
|
143
|
+
if (result.testEvidence.length > 0) {
|
|
144
|
+
lines.push(`Test evidence: ${result.testEvidence.join(", ")}`);
|
|
145
|
+
}
|
|
146
|
+
if (result.concerns.length > 0) {
|
|
147
|
+
lines.push(`Concerns: ${result.concerns.join(", ")}`);
|
|
148
|
+
}
|
|
149
|
+
if (result.blockers.length > 0) {
|
|
150
|
+
lines.push(`Blockers: ${result.blockers.join(", ")}`);
|
|
151
|
+
}
|
|
152
|
+
return lines.join("\n");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
module.exports = {
|
|
156
|
+
VALID_IMPLEMENTER_STATUSES,
|
|
157
|
+
normalizeTaskExecutionEnvelope,
|
|
158
|
+
writeTaskExecutionEnvelope,
|
|
159
|
+
formatTaskExecutionReport
|
|
160
|
+
};
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { resolveChangeDir, unique } = require("./planning-parsers");
|
|
4
|
+
const { readExecutionSignals, writeExecutionSignal } = require("./execution-signals");
|
|
5
|
+
const { writeFileAtomic, readTextIfExists } = require("./utils");
|
|
6
|
+
|
|
7
|
+
const VALID_REVIEW_STAGES = new Set(["spec", "quality"]);
|
|
8
|
+
const VALID_REVIEW_STATUSES = new Set(["PASS", "WARN", "BLOCK"]);
|
|
9
|
+
|
|
10
|
+
function normalizeReviewStage(value) {
|
|
11
|
+
const normalized = String(value || "").trim().toLowerCase();
|
|
12
|
+
if (!VALID_REVIEW_STAGES.has(normalized)) {
|
|
13
|
+
throw new Error("`task-review --stage` must be `spec` or `quality`.");
|
|
14
|
+
}
|
|
15
|
+
return normalized;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function normalizeReviewStatus(value) {
|
|
19
|
+
const normalized = String(value || "").trim().toUpperCase();
|
|
20
|
+
if (!VALID_REVIEW_STATUSES.has(normalized)) {
|
|
21
|
+
throw new Error("`task-review --status` must be PASS, WARN, or BLOCK.");
|
|
22
|
+
}
|
|
23
|
+
return normalized;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function normalizeTaskGroupId(value) {
|
|
27
|
+
const normalized = String(value || "").trim();
|
|
28
|
+
if (!normalized) {
|
|
29
|
+
throw new Error("`task-review` requires `--task-group <id>`.");
|
|
30
|
+
}
|
|
31
|
+
return normalized;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function normalizeList(value) {
|
|
35
|
+
if (Array.isArray(value)) {
|
|
36
|
+
return unique(value.map((item) => String(item || "").trim()).filter(Boolean));
|
|
37
|
+
}
|
|
38
|
+
return unique(
|
|
39
|
+
String(value || "")
|
|
40
|
+
.split(/[,\n;]/)
|
|
41
|
+
.map((item) => item.trim())
|
|
42
|
+
.filter(Boolean)
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function buildSafeTaskGroupId(taskGroupId) {
|
|
47
|
+
return String(taskGroupId || "")
|
|
48
|
+
.trim()
|
|
49
|
+
.replace(/[^A-Za-z0-9._-]+/g, "_");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function buildTaskReviewSurface(taskGroupId, stage) {
|
|
53
|
+
return `task-review.${buildSafeTaskGroupId(taskGroupId)}.${stage}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function mapReviewStatusToSignalStatus(status) {
|
|
57
|
+
return status;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function normalizeTaskReviewEnvelope(input = {}) {
|
|
61
|
+
const taskGroupId = normalizeTaskGroupId(input.taskGroupId);
|
|
62
|
+
const stage = normalizeReviewStage(input.stage);
|
|
63
|
+
const status = normalizeReviewStatus(input.status);
|
|
64
|
+
const summary = String(input.summary || "").trim();
|
|
65
|
+
if (!summary) {
|
|
66
|
+
throw new Error("`task-review` requires `--summary <text>`.");
|
|
67
|
+
}
|
|
68
|
+
const issues = normalizeList(input.issues);
|
|
69
|
+
const reviewer = String(input.reviewer || "").trim() || "unspecified";
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
taskGroupId,
|
|
73
|
+
stage,
|
|
74
|
+
status,
|
|
75
|
+
summary,
|
|
76
|
+
issues,
|
|
77
|
+
reviewer,
|
|
78
|
+
recordedAt: new Date().toISOString()
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function findLatestTaskReviewSignal(signals, taskGroupId, stage) {
|
|
83
|
+
const surface = buildTaskReviewSurface(taskGroupId, stage);
|
|
84
|
+
return (signals || [])
|
|
85
|
+
.filter((signal) => String(signal.surface || "") === surface)
|
|
86
|
+
.sort((left, right) => String(right.timestamp || "").localeCompare(String(left.timestamp || "")))[0] || null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function appendTaskReviewEvidence(verificationText, envelope) {
|
|
90
|
+
const existing = String(verificationText || "").trim();
|
|
91
|
+
const section = [
|
|
92
|
+
`### Task Review ${envelope.taskGroupId} (${envelope.stage})`,
|
|
93
|
+
`- Status: ${envelope.status}`,
|
|
94
|
+
`- Reviewer: ${envelope.reviewer}`,
|
|
95
|
+
`- Summary: ${envelope.summary}`,
|
|
96
|
+
`- Issues: ${envelope.issues.length > 0 ? envelope.issues.join("; ") : "none"}`,
|
|
97
|
+
`- Recorded at: ${envelope.recordedAt}`
|
|
98
|
+
].join("\n");
|
|
99
|
+
|
|
100
|
+
if (!existing) {
|
|
101
|
+
return `# Verification\n\n## Task Review Evidence\n\n${section}\n`;
|
|
102
|
+
}
|
|
103
|
+
if (!/^\s*##\s+Task Review Evidence\s*$/im.test(existing)) {
|
|
104
|
+
return `${existing}\n\n## Task Review Evidence\n\n${section}\n`;
|
|
105
|
+
}
|
|
106
|
+
return `${existing}\n\n${section}\n`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function writeTaskReviewEnvelope(projectPathInput, options = {}) {
|
|
110
|
+
const projectRoot = path.resolve(projectPathInput || process.cwd());
|
|
111
|
+
const requestedChangeId = options.changeId ? String(options.changeId).trim() : "";
|
|
112
|
+
const resolved = resolveChangeDir(projectRoot, requestedChangeId);
|
|
113
|
+
if (!resolved.changeDir || !resolved.changeId) {
|
|
114
|
+
const resolveError =
|
|
115
|
+
Array.isArray(resolved.failures) && resolved.failures.length > 0
|
|
116
|
+
? String(resolved.failures[0] || "").trim()
|
|
117
|
+
: "";
|
|
118
|
+
throw new Error(resolveError || "Unable to resolve active change for task-review.");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const envelope = normalizeTaskReviewEnvelope(options);
|
|
122
|
+
const existingSignals = readExecutionSignals(projectRoot, { changeId: resolved.changeId });
|
|
123
|
+
|
|
124
|
+
if (envelope.stage === "quality") {
|
|
125
|
+
const specSignal = findLatestTaskReviewSignal(existingSignals, envelope.taskGroupId, "spec");
|
|
126
|
+
if (!specSignal || String(specSignal.status || "").toUpperCase() !== "PASS") {
|
|
127
|
+
throw new Error(
|
|
128
|
+
`Quality review for task group ${envelope.taskGroupId} requires a prior spec review PASS.`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const signalPath = writeExecutionSignal(projectRoot, {
|
|
134
|
+
changeId: resolved.changeId,
|
|
135
|
+
surface: buildTaskReviewSurface(envelope.taskGroupId, envelope.stage),
|
|
136
|
+
status: mapReviewStatusToSignalStatus(envelope.status),
|
|
137
|
+
advisory: false,
|
|
138
|
+
strict: true,
|
|
139
|
+
failures: envelope.status === "BLOCK" ? envelope.issues : [],
|
|
140
|
+
warnings: envelope.status === "WARN" ? envelope.issues : [],
|
|
141
|
+
notes: [envelope.summary, `reviewer: ${envelope.reviewer}`],
|
|
142
|
+
details: {
|
|
143
|
+
type: "task_review",
|
|
144
|
+
envelope
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
let verificationPath = null;
|
|
149
|
+
if (options.writeVerification === true) {
|
|
150
|
+
verificationPath = path.join(resolved.changeDir, "verification.md");
|
|
151
|
+
fs.mkdirSync(path.dirname(verificationPath), { recursive: true });
|
|
152
|
+
const nextVerification = appendTaskReviewEvidence(readTextIfExists(verificationPath), envelope);
|
|
153
|
+
writeFileAtomic(verificationPath, nextVerification);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
status: envelope.status,
|
|
158
|
+
projectRoot,
|
|
159
|
+
changeId: resolved.changeId,
|
|
160
|
+
taskGroupId: envelope.taskGroupId,
|
|
161
|
+
stage: envelope.stage,
|
|
162
|
+
summary: envelope.summary,
|
|
163
|
+
issues: envelope.issues,
|
|
164
|
+
reviewer: envelope.reviewer,
|
|
165
|
+
signalPath,
|
|
166
|
+
verificationPath
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function formatTaskReviewReport(result) {
|
|
171
|
+
const lines = [
|
|
172
|
+
"Task review envelope",
|
|
173
|
+
`Project: ${result.projectRoot}`,
|
|
174
|
+
`Change: ${result.changeId}`,
|
|
175
|
+
`Task group: ${result.taskGroupId}`,
|
|
176
|
+
`Stage: ${result.stage}`,
|
|
177
|
+
`Status: ${result.status}`,
|
|
178
|
+
`Reviewer: ${result.reviewer}`,
|
|
179
|
+
`Summary: ${result.summary}`,
|
|
180
|
+
`Signal path: ${result.signalPath}`
|
|
181
|
+
];
|
|
182
|
+
if (result.issues.length > 0) {
|
|
183
|
+
lines.push(`Issues: ${result.issues.join("; ")}`);
|
|
184
|
+
}
|
|
185
|
+
if (result.verificationPath) {
|
|
186
|
+
lines.push(`Verification write-back: ${result.verificationPath}`);
|
|
187
|
+
}
|
|
188
|
+
return lines.join("\n");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
module.exports = {
|
|
192
|
+
VALID_REVIEW_STAGES,
|
|
193
|
+
VALID_REVIEW_STATUSES,
|
|
194
|
+
normalizeTaskReviewEnvelope,
|
|
195
|
+
writeTaskReviewEnvelope,
|
|
196
|
+
formatTaskReviewReport
|
|
197
|
+
};
|
package/lib/verify.js
CHANGED
|
@@ -13,6 +13,7 @@ const {
|
|
|
13
13
|
readChangeArtifacts,
|
|
14
14
|
readArtifactTexts
|
|
15
15
|
} = require("./planning-parsers");
|
|
16
|
+
const { readExecutionSignals, summarizeSignalsBySurface } = require("./execution-signals");
|
|
16
17
|
|
|
17
18
|
const CODE_FILE_EXTENSIONS = new Set([".js", ".jsx", ".ts", ".tsx", ".html", ".css", ".scss"]);
|
|
18
19
|
const NON_IMPLEMENTATION_DIR_NAMES = new Set([
|
|
@@ -47,6 +48,7 @@ const MAX_SCANNED_FILES = 2000;
|
|
|
47
48
|
const MAX_SCANNED_BYTES_PER_FILE = 512 * 1024;
|
|
48
49
|
const MAX_SCANNED_DIRECTORIES = 10000;
|
|
49
50
|
const MAX_SCAN_DEPTH = 32;
|
|
51
|
+
const DEFAULT_EVIDENCE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
50
52
|
|
|
51
53
|
function buildEnvelope(name, projectRoot, strict) {
|
|
52
54
|
return {
|
|
@@ -226,6 +228,138 @@ function allCovered(checks) {
|
|
|
226
228
|
return true;
|
|
227
229
|
}
|
|
228
230
|
|
|
231
|
+
function safeMtimeMs(filePath) {
|
|
232
|
+
try {
|
|
233
|
+
const stat = fs.statSync(filePath);
|
|
234
|
+
return Number(stat.mtimeMs) || 0;
|
|
235
|
+
} catch (_error) {
|
|
236
|
+
return 0;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function collectFreshnessBaseline(projectRoot, resolved, artifactPaths) {
|
|
241
|
+
const baselineCandidates = [];
|
|
242
|
+
if (artifactPaths) {
|
|
243
|
+
baselineCandidates.push(
|
|
244
|
+
artifactPaths.proposalPath,
|
|
245
|
+
artifactPaths.tasksPath,
|
|
246
|
+
artifactPaths.bindingsPath,
|
|
247
|
+
artifactPaths.pencilDesignPath,
|
|
248
|
+
artifactPaths.verificationPath
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
if (resolved && resolved.changeDir) {
|
|
252
|
+
const specsRoot = path.join(resolved.changeDir, "specs");
|
|
253
|
+
if (fs.existsSync(specsRoot)) {
|
|
254
|
+
const stack = [specsRoot];
|
|
255
|
+
while (stack.length > 0) {
|
|
256
|
+
const current = stack.pop();
|
|
257
|
+
let entries = [];
|
|
258
|
+
try {
|
|
259
|
+
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
260
|
+
} catch (_error) {
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
for (const entry of entries) {
|
|
264
|
+
const absolutePath = path.join(current, entry.name);
|
|
265
|
+
if (entry.isDirectory() && !entry.isSymbolicLink()) {
|
|
266
|
+
stack.push(absolutePath);
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (entry.isFile() && entry.name === "spec.md") {
|
|
270
|
+
baselineCandidates.push(absolutePath);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const baselineMs = baselineCandidates.reduce((latest, candidate) => Math.max(latest, safeMtimeMs(candidate)), 0);
|
|
277
|
+
return {
|
|
278
|
+
baselineMs,
|
|
279
|
+
baselineIso: baselineMs > 0 ? new Date(baselineMs).toISOString() : ""
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function collectVerificationFreshness(projectPathInput, options = {}) {
|
|
284
|
+
const projectRoot = path.resolve(projectPathInput || process.cwd());
|
|
285
|
+
const changeId = options.changeId ? String(options.changeId).trim() : "";
|
|
286
|
+
if (!changeId) {
|
|
287
|
+
return {
|
|
288
|
+
fresh: false,
|
|
289
|
+
changeId: null,
|
|
290
|
+
requiredSurfaces: [],
|
|
291
|
+
surfaces: {},
|
|
292
|
+
staleReasons: ["Missing change id for verification freshness checks."],
|
|
293
|
+
baselineIso: ""
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const requiredSurfaces =
|
|
298
|
+
Array.isArray(options.requiredSurfaces) && options.requiredSurfaces.length > 0
|
|
299
|
+
? options.requiredSurfaces
|
|
300
|
+
: ["verify-bindings", "verify-implementation", "verify-structure", "verify-coverage"];
|
|
301
|
+
const maxAgeMs =
|
|
302
|
+
Number.isFinite(Number(options.maxAgeMs)) && Number(options.maxAgeMs) > 0
|
|
303
|
+
? Number(options.maxAgeMs)
|
|
304
|
+
: DEFAULT_EVIDENCE_MAX_AGE_MS;
|
|
305
|
+
const nowMs = Date.now();
|
|
306
|
+
const signals = readExecutionSignals(projectRoot, { changeId });
|
|
307
|
+
const summary = summarizeSignalsBySurface(signals);
|
|
308
|
+
const baseline = collectFreshnessBaseline(projectRoot, options.resolved, options.artifactPaths);
|
|
309
|
+
const staleReasons = [];
|
|
310
|
+
const surfaces = {};
|
|
311
|
+
|
|
312
|
+
for (const surface of requiredSurfaces) {
|
|
313
|
+
const key = String(surface || "").toLowerCase().replace(/[^a-z0-9._-]+/g, "-");
|
|
314
|
+
const signal = summary[key];
|
|
315
|
+
if (!signal) {
|
|
316
|
+
staleReasons.push(`Missing evidence signal: ${surface}.`);
|
|
317
|
+
surfaces[surface] = {
|
|
318
|
+
present: false,
|
|
319
|
+
stale: true
|
|
320
|
+
};
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
const timestampMs = Date.parse(String(signal.timestamp || ""));
|
|
324
|
+
if (!Number.isFinite(timestampMs)) {
|
|
325
|
+
staleReasons.push(`Invalid evidence timestamp for ${surface}.`);
|
|
326
|
+
surfaces[surface] = {
|
|
327
|
+
present: true,
|
|
328
|
+
stale: true,
|
|
329
|
+
timestamp: signal.timestamp || ""
|
|
330
|
+
};
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
const olderThanBaseline = baseline.baselineMs > 0 && timestampMs < baseline.baselineMs;
|
|
334
|
+
const olderThanMaxAge = nowMs - timestampMs > maxAgeMs;
|
|
335
|
+
if (olderThanBaseline) {
|
|
336
|
+
staleReasons.push(
|
|
337
|
+
`${surface} evidence is older than current artifact baseline (${new Date(timestampMs).toISOString()} < ${baseline.baselineIso}).`
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
if (olderThanMaxAge) {
|
|
341
|
+
staleReasons.push(
|
|
342
|
+
`${surface} evidence is older than freshness window (${Math.round((nowMs - timestampMs) / 1000)}s).`
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
surfaces[surface] = {
|
|
346
|
+
present: true,
|
|
347
|
+
stale: olderThanBaseline || olderThanMaxAge,
|
|
348
|
+
status: signal.status,
|
|
349
|
+
timestamp: signal.timestamp
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return {
|
|
354
|
+
fresh: staleReasons.length === 0,
|
|
355
|
+
changeId,
|
|
356
|
+
requiredSurfaces,
|
|
357
|
+
surfaces,
|
|
358
|
+
staleReasons: unique(staleReasons),
|
|
359
|
+
baselineIso: baseline.baselineIso
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
229
363
|
function createSharedSetup(projectPathInput, options = {}) {
|
|
230
364
|
const projectRoot = path.resolve(projectPathInput || process.cwd());
|
|
231
365
|
const strict = options && options.strict === true;
|
|
@@ -600,6 +734,22 @@ function verifyCoverage(projectPathInput, options = {}) {
|
|
|
600
734
|
}
|
|
601
735
|
};
|
|
602
736
|
|
|
737
|
+
const freshness = collectVerificationFreshness(projectRoot, {
|
|
738
|
+
changeId: result.changeId,
|
|
739
|
+
resolved: sharedSetup.resolved,
|
|
740
|
+
artifactPaths: sharedSetup.artifactPaths,
|
|
741
|
+
requiredSurfaces: ["verify-bindings", "verify-implementation", "verify-structure"]
|
|
742
|
+
});
|
|
743
|
+
result.freshness = freshness;
|
|
744
|
+
if (!freshness.fresh) {
|
|
745
|
+
result.warnings.push(
|
|
746
|
+
"Verification freshness is stale for completion-facing claims; re-run verify surfaces before completion wording."
|
|
747
|
+
);
|
|
748
|
+
for (const reason of freshness.staleReasons) {
|
|
749
|
+
result.warnings.push(reason);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
603
753
|
return finalize(result);
|
|
604
754
|
}
|
|
605
755
|
|
|
@@ -648,5 +798,6 @@ module.exports = {
|
|
|
648
798
|
verifyImplementation,
|
|
649
799
|
verifyStructure,
|
|
650
800
|
verifyCoverage,
|
|
651
|
-
formatVerifyReport
|
|
801
|
+
formatVerifyReport,
|
|
802
|
+
collectVerificationFreshness
|
|
652
803
|
};
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
2
|
const path = require("path");
|
|
3
3
|
|
|
4
|
-
const { isPathInside } = require("./fs-safety");
|
|
5
4
|
const { ensurePenFile } = require("./pen-persistence");
|
|
5
|
+
const { resolvePreferredRegisteredPenPath } = require("./design-source-registry");
|
|
6
6
|
const { pathExists, readTextIfExists, writeFileAtomic } = require("./utils");
|
|
7
7
|
|
|
8
8
|
const DEFAULT_PROJECT_PEN_RELATIVE_PATH = ".da-vinci/designs/project-baseline.pen";
|
|
@@ -20,17 +20,6 @@ function validateChangeId(changeId) {
|
|
|
20
20
|
return normalized;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
function extractRegisteredPenPath(projectRoot, registryText) {
|
|
24
|
-
const matches = String(registryText || "").match(/\.da-vinci\/designs\/[^\s`]+\.pen/g) || [];
|
|
25
|
-
for (const relativePath of matches) {
|
|
26
|
-
const resolvedPath = path.resolve(projectRoot, relativePath);
|
|
27
|
-
if (isPathInside(projectRoot, resolvedPath)) {
|
|
28
|
-
return resolvedPath;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
return "";
|
|
32
|
-
}
|
|
33
|
-
|
|
34
23
|
function buildProjectTemplates(preferredPenRelativePath) {
|
|
35
24
|
return {
|
|
36
25
|
"DA-VINCI.md": [
|
|
@@ -151,7 +140,7 @@ function bootstrapProjectArtifacts(projectPathInput, options = {}) {
|
|
|
151
140
|
fs.mkdirSync(path.join(projectRoot, ".da-vinci", "changes"), { recursive: true });
|
|
152
141
|
|
|
153
142
|
const existingRegistryText = readTextIfExists(path.join(projectRoot, ".da-vinci", "design-registry.md"));
|
|
154
|
-
const registeredPenPath =
|
|
143
|
+
const registeredPenPath = resolvePreferredRegisteredPenPath(projectRoot, existingRegistryText);
|
|
155
144
|
const preferredPenPath = registeredPenPath || path.join(projectRoot, DEFAULT_PROJECT_PEN_RELATIVE_PATH);
|
|
156
145
|
const preferredPenRelativePath = path.relative(projectRoot, preferredPenPath) || DEFAULT_PROJECT_PEN_RELATIVE_PATH;
|
|
157
146
|
|
|
@@ -2,7 +2,7 @@ const fs = require("fs");
|
|
|
2
2
|
const path = require("path");
|
|
3
3
|
const { writeFileAtomic, pathExists, readTextIfExists } = require("./utils");
|
|
4
4
|
|
|
5
|
-
const WORKFLOW_STATE_VERSION =
|
|
5
|
+
const WORKFLOW_STATE_VERSION = 2;
|
|
6
6
|
const DEFAULT_STALE_WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
7
7
|
const PERSISTED_NOTE_EXCLUDE_PATTERNS = [
|
|
8
8
|
/^No persisted workflow state found for this change; deriving from artifacts\.$/i,
|
|
@@ -38,7 +38,9 @@ function fingerprintForPath(targetPath) {
|
|
|
38
38
|
function buildWorkflowFingerprint(projectRoot, changeId) {
|
|
39
39
|
const changeRoot = changeId ? path.join(projectRoot, ".da-vinci", "changes", changeId) : "";
|
|
40
40
|
const candidates = [
|
|
41
|
+
path.join(projectRoot, "DA-VINCI.md"),
|
|
41
42
|
path.join(projectRoot, ".da-vinci", "page-map.md"),
|
|
43
|
+
path.join(projectRoot, ".da-vinci", "design-registry.md"),
|
|
42
44
|
path.join(changeRoot, "proposal.md"),
|
|
43
45
|
path.join(changeRoot, "design.md"),
|
|
44
46
|
path.join(changeRoot, "pencil-design.md"),
|