@devflow-core/dsh-devflow 0.1.0
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/NOTICE +13 -0
- package/README.md +82 -0
- package/assets/commands/devflow-adversarial.toml +11 -0
- package/assets/commands/devflow-audit.toml +32 -0
- package/assets/commands/devflow-debt.toml +42 -0
- package/assets/commands/devflow-find-fault.toml +11 -0
- package/assets/commands/devflow-learn.toml +21 -0
- package/assets/commands/devflow-plan.toml +58 -0
- package/assets/commands/devflow-prove.toml +20 -0
- package/assets/commands/devflow-pua.toml +40 -0
- package/assets/commands/devflow-review.toml +36 -0
- package/assets/commands/devflow-spec.toml +49 -0
- package/assets/commands/devflow.toml +35 -0
- package/assets/presets/devflow-2/NOTICE +4 -0
- package/assets/presets/devflow-2/README.md +71 -0
- package/assets/presets/devflow-2/agent.cordis.yml +337 -0
- package/assets/presets/devflow-2/custom-bash.mjs +213 -0
- package/assets/presets/devflow-2/preset.yml +3 -0
- package/assets/presets/devflow-2/tool-bootstrap.mjs +496 -0
- package/assets/scripts/devflow-audit.js +275 -0
- package/assets/scripts/devflow-debt.js +196 -0
- package/assets/scripts/devflow-doctor.js +90 -0
- package/assets/scripts/devflow-plan.js +638 -0
- package/assets/scripts/devflow-review.js +93 -0
- package/assets/scripts/devflow-spec.js +238 -0
- package/assets/skills/devflow-adversarial/SKILL.md +71 -0
- package/assets/skills/devflow-audit/SKILL.md +78 -0
- package/assets/skills/devflow-brainstorm/SKILL.md +176 -0
- package/assets/skills/devflow-brainstorm/references/interview-discipline.md +184 -0
- package/assets/skills/devflow-build/SKILL.md +238 -0
- package/assets/skills/devflow-build/references/build-methods.md +40 -0
- package/assets/skills/devflow-core/SKILL.md +93 -0
- package/assets/skills/devflow-core/references/core-methods.md +131 -0
- package/assets/skills/devflow-core/references/reference-projects.md +133 -0
- package/assets/skills/devflow-core/references/skill-guide.md +63 -0
- package/assets/skills/devflow-cut/SKILL.md +208 -0
- package/assets/skills/devflow-cut/references/cut-methods.md +65 -0
- package/assets/skills/devflow-cut/references/native-capability-checklist.md +112 -0
- package/assets/skills/devflow-docs-followup/SKILL.md +132 -0
- package/assets/skills/devflow-docs-followup/agents/openai.yaml +4 -0
- package/assets/skills/devflow-find-fault/SKILL.md +109 -0
- package/assets/skills/devflow-learn/SKILL.md +176 -0
- package/assets/skills/devflow-plan/SKILL.md +142 -0
- package/assets/skills/devflow-plan/references/plan-methods.md +74 -0
- package/assets/skills/devflow-project-knowledge/SKILL.md +354 -0
- package/assets/skills/devflow-prove/SKILL.md +216 -0
- package/assets/skills/devflow-prove/references/code-review-checklist.md +202 -0
- package/assets/skills/devflow-prove/references/flow-self-test.md +775 -0
- package/assets/skills/devflow-prove/references/proof-recovery-methods.md +26 -0
- package/assets/skills/devflow-pua/SKILL.md +197 -0
- package/assets/skills/devflow-pua/references/flavor-display.md +49 -0
- package/assets/skills/devflow-pua/references/methodology-library.md +193 -0
- package/assets/skills/devflow-pua/references/methodology-router.md +78 -0
- package/assets/skills/devflow-spec/SKILL.md +92 -0
- package/assets/skills/devflow-spec/references/spec-plan-methods.md +15 -0
- package/cordis.patch.yml +11 -0
- package/lib/dsh-home.js +33 -0
- package/lib/index.js +79 -0
- package/lib/mount-once.js +34 -0
- package/lib/sync.js +168 -0
- package/package.json +32 -0
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
const fs = require("node:fs");
|
|
2
|
+
|
|
3
|
+
const requiredGlobalFields = ["Goal", "Architecture", "Tech Stack", "Source", "Spec coverage", "External Skills"];
|
|
4
|
+
const requiredTaskFields = ["Task", "Task type", "Files", "Interfaces", "Steps", "Acceptance", "Verify", "Comments", "Not doing"];
|
|
5
|
+
const codeChangeFields = ["Current behavior", "Target behavior", "Change mechanics", "Call impact"];
|
|
6
|
+
const allFields = [...requiredGlobalFields, ...requiredTaskFields, ...codeChangeFields];
|
|
7
|
+
const taskTypes = ["Code change", "Documentation-only"];
|
|
8
|
+
// 可选生命周期状态字段:缺失视为 legacy(向后兼容),存在时值域受限;checker 只校验格式,不裁决状态转换。
|
|
9
|
+
const validStatuses = ["draft", "approved", "in-progress", "done"];
|
|
10
|
+
const statusPattern = /^\s*(?:\*\*)?Status(?:\*\*)?\s*:\s*([^\n]+)/im;
|
|
11
|
+
|
|
12
|
+
const fieldPatterns = Object.fromEntries(
|
|
13
|
+
allFields.map((field) => [field, new RegExp(`^(?:\\*\\*)?${field}(?:\\*\\*)?\\s*:`, "im")])
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
const unresolvedPatterns = [/\bTODO\b/i, /\bTBD\b/i, /\bcoming soon\b/i, /\?\?\?/, /<[^>\n]+>/];
|
|
17
|
+
const vaguePatterns = [/\badd tests\b/i, /\bhandle edge cases\b/i, /\bmake it work\b/i, /\bclean up\b/i, /\brefactor as needed\b/i, /\bsimilar to Task\b/i];
|
|
18
|
+
const fileOperationPattern = /^\s*-\s*(Create|Modify|Test):\s+([^|\n]+?)\s*\|\s*([^|\n]+?)\s*\|\s*(\S.*)$/i;
|
|
19
|
+
const checkboxPattern = /^\s*-\s*\[ \]\s+(.+)$/gim;
|
|
20
|
+
const concreteStepPattern = /(?:[A-Za-z0-9_.-]+\/)+[A-Za-z0-9_.-]+|`[^`]+`|\b(?:node|npm|git)\b|\b(?:function|class|method|API|command|behavior|symbol|anchor)\b/i;
|
|
21
|
+
const locationPattern = /^(?:(?:symbol|symbols|anchor|anchors)\s*:\s*)?(?:`[^`]{3,}`(?:\s*,\s*`[^`]{3,}`)*|(?:function|class|method|const|let|var|export|interface|type|enum|heading|section|key|keys)\s+[`#A-Za-z_$][\w.$#:/ -]*|#{1,6}\s+\S.+)$/i;
|
|
22
|
+
const implementationVerbPattern = /\b(?:parse|validate|require|reject|map|filter|return|add|remove|replace|insert|delete|set|check|compare|append|emit)\b|(?:增加|添加|解析|验证|拒绝|替换|输出|收集|要求|检查)/;
|
|
23
|
+
const genericMechanicsPattern = /^\s*(?:pseudocode|exact replacement)\s*:\s*(?:update|change|modify)\s+(?:it|this|implementation|code)\s*$/i;
|
|
24
|
+
const codeActionStepPattern = /\b(?:modify|create|update|replace|insert|delete|remove|add|change|refactor)\b/i;
|
|
25
|
+
const mechanicsPattern = /```[\s\S]*?```|\b(?:pseudocode|exact replacement|replace .* with|insert .* before|delete .* after)\b/i;
|
|
26
|
+
const verificationCommandPattern = /`[^`]+`|\b(?:node|npm|npx|pnpm|yarn|git)\b/i;
|
|
27
|
+
const verificationExpectationPattern = /\b(?:expect|expected|passes|pass|fails|fail|returns|result)\b/i;
|
|
28
|
+
const documentationPathPattern = /\.(?:md|mdc|toml|txt)$/i;
|
|
29
|
+
const fileStructureHeadingPattern = /^##\s+File Structure\s*$/im;
|
|
30
|
+
const prewalkPattern = /^\s*Prewalk\s*:\s*$/im;
|
|
31
|
+
const prewalkSections = ["Execution Trace", "Current Handoff Facts", "Remaining Structured Worklist"];
|
|
32
|
+
const handoffFactNames = ["Target anchors", "Nearby convention", "Direct path", "Current constraints", "Planned touch set", "Risks / stop conditions"];
|
|
33
|
+
// Read-basis / Live anchors:接力执行减少重读的必需交接字段;文档型任务经 checkTask 豁免(仅 Code change 调用 checkPrewalk)。
|
|
34
|
+
const handoffExtraFactNames = ["Read-basis", "Live anchors"];
|
|
35
|
+
const traceEntryPattern = /^\s*-\s*(Read|Traced|Ran|Edited|Verified):\s*(.+?)\s*→\s*(.+?)\.?\s*$/im;
|
|
36
|
+
const futureTracePattern = /\b(?:will|should|need to|plan to|to be done)\b|(?:将|需要|计划|待完成)/i;
|
|
37
|
+
const worklistItemPattern = /^\s*-\s*\[ \]\s+(.+)$/gim;
|
|
38
|
+
const worklistDetailNames = ["Anchors", "Verify", "Done when"];
|
|
39
|
+
const maximumWorklistItems = 12;
|
|
40
|
+
|
|
41
|
+
/** Print the checker command contract and default plan landing. */
|
|
42
|
+
function usage() {
|
|
43
|
+
console.log("Usage: node scripts/devflow-plan.js [plan-file] [--self-test] [--json]");
|
|
44
|
+
console.log("Checks whether a DevFlow Plan Pack has an executable header, task contracts, and plan landing.");
|
|
45
|
+
console.log("Default plan landing is docs/plans/YYYY-MM-DD-<short-kebab-name>.md unless the project documents another plan path.");
|
|
46
|
+
console.log("--json prints a single-line machine-readable summary; optional Status header values: " + validStatuses.join(" | "));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Split a plan at Task fields so each task can be validated independently. */
|
|
50
|
+
function splitTasks(body) {
|
|
51
|
+
const lines = body.split(/\r?\n/);
|
|
52
|
+
const taskStarts = [];
|
|
53
|
+
|
|
54
|
+
lines.forEach((line, index) => {
|
|
55
|
+
if (fieldPatterns.Task.test(line)) {
|
|
56
|
+
taskStarts.push(index);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return taskStarts.map((start, index) => {
|
|
61
|
+
const end = taskStarts[index + 1] ?? lines.length;
|
|
62
|
+
return { number: index + 1, body: lines.slice(start, end).join("\n") };
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Return matches that indicate an unresolved plan placeholder or vague executable instruction. */
|
|
67
|
+
function findMatches(body, patterns) {
|
|
68
|
+
return patterns.flatMap((pattern) => {
|
|
69
|
+
const matches = body.match(pattern);
|
|
70
|
+
return matches ? [matches[0]] : [];
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Extract the text between one structural field and the next structural field. */
|
|
75
|
+
function fieldBlock(body, field) {
|
|
76
|
+
const lines = body.split(/\r?\n/);
|
|
77
|
+
const start = lines.findIndex((line) => fieldPatterns[field].test(line));
|
|
78
|
+
if (start < 0) return "";
|
|
79
|
+
|
|
80
|
+
const value = lines[start].replace(fieldPatterns[field], "").trim();
|
|
81
|
+
const end = lines.findIndex((line, index) => index > start && allFields.some((name) => fieldPatterns[name].test(line)));
|
|
82
|
+
return [value, ...lines.slice(start + 1, end < 0 ? lines.length : end)].join("\n").trim();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Extract a Markdown section until the next heading at the same or higher level. */
|
|
86
|
+
function headingBlock(body, heading) {
|
|
87
|
+
const lines = body.split(/\r?\n/);
|
|
88
|
+
const start = lines.findIndex((line) => new RegExp(`^#{1,6}\\s+${heading}\\s*$`, "i").test(line));
|
|
89
|
+
if (start < 0) return "";
|
|
90
|
+
|
|
91
|
+
const end = lines.findIndex((line, index) => index > start && /^#{1,6}\s+/.test(line));
|
|
92
|
+
return lines.slice(start + 1, end < 0 ? lines.length : end).join("\n").trim();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Extract one named handoff subsection before the next named handoff subsection. */
|
|
96
|
+
function handoffBlock(prewalk, heading) {
|
|
97
|
+
const lines = prewalk.split(/\r?\n/);
|
|
98
|
+
const start = lines.findIndex((line) => new RegExp(`^\\s*${heading}\\s*:\\s*$`, "i").test(line));
|
|
99
|
+
if (start < 0) return "";
|
|
100
|
+
|
|
101
|
+
const end = lines.findIndex((line, index) =>
|
|
102
|
+
index > start && prewalkSections.some((name) => new RegExp(`^\\s*${name}\\s*:\\s*$`, "i").test(line))
|
|
103
|
+
);
|
|
104
|
+
return lines.slice(start + 1, end < 0 ? lines.length : end).join("\n").trim();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Validate the global responsibility map without inferring whether its design is correct. */
|
|
108
|
+
function checkFileStructure(body) {
|
|
109
|
+
const structure = headingBlock(body, "File Structure");
|
|
110
|
+
const rows = structure
|
|
111
|
+
.split(/\r?\n/)
|
|
112
|
+
.filter((line) => /^\s*\|/.test(line))
|
|
113
|
+
.filter((line) => !/^\s*\|\s*-/.test(line));
|
|
114
|
+
const dataRows = rows.slice(1).filter((line) => line.split("|").length >= 7);
|
|
115
|
+
const invalidRows = dataRows.filter((line) => {
|
|
116
|
+
const cells = line.split("|").slice(1, -1).map((cell) => cell.trim());
|
|
117
|
+
return cells.length !== 5 || cells.some((cell) => !cell || /\[|\]/.test(cell));
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
present: fileStructureHeadingPattern.test(body),
|
|
122
|
+
dataRows,
|
|
123
|
+
invalidRows,
|
|
124
|
+
paths: dataRows.map((line) => line.split("|")[1].replaceAll("`", "").trim()),
|
|
125
|
+
ok: fileStructureHeadingPattern.test(body) && dataRows.length > 0 && invalidRows.length === 0
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function hasHandoffSection(prewalk, heading) {
|
|
130
|
+
return new RegExp(`^\\s*${heading}\\s*:\\s*$`, "im").test(prewalk);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Validate actual trace evidence, current facts, and bounded unfinished work. */
|
|
134
|
+
function checkPrewalk(task) {
|
|
135
|
+
const start = task.body.search(prewalkPattern);
|
|
136
|
+
const prewalk = start < 0 ? "" : task.body.slice(start);
|
|
137
|
+
const trace = handoffBlock(prewalk, "Execution Trace");
|
|
138
|
+
const facts = handoffBlock(prewalk, "Current Handoff Facts");
|
|
139
|
+
const worklist = handoffBlock(prewalk, "Remaining Structured Worklist");
|
|
140
|
+
const traceLines = trace.split(/\r?\n/).filter((line) => line.trim().startsWith("-"));
|
|
141
|
+
const traceEntries = traceLines.map((line) => ({ line, match: line.match(traceEntryPattern) }));
|
|
142
|
+
const invalidTrace = traceEntries
|
|
143
|
+
.filter(({ line, match }) => !match || futureTracePattern.test(line))
|
|
144
|
+
.map(({ line }) => line);
|
|
145
|
+
const actualReadOrTrace = traceEntries.some(({ match }) => match && ["Read", "Traced"].includes(match[1]) && !/^none\b/i.test(match[2]));
|
|
146
|
+
const actualEdit = traceEntries.some(({ match }) => match && match[1] === "Edited" && !/^none\b/i.test(match[2]));
|
|
147
|
+
const actualVerification = traceEntries.some(({ match }) => match && match[1] === "Verified" && !/^none\b/i.test(match[2]));
|
|
148
|
+
const completed = actualEdit && actualVerification;
|
|
149
|
+
const missingFacts = handoffFactNames.concat(handoffExtraFactNames).filter(
|
|
150
|
+
(name) => !new RegExp(`^\\s*-\\s*${name}:\\s+\\S`, "im").test(facts)
|
|
151
|
+
);
|
|
152
|
+
const items = [...worklist.matchAll(worklistItemPattern)];
|
|
153
|
+
const incompleteWorklist = items
|
|
154
|
+
.map((item, index) => {
|
|
155
|
+
const next = items[index + 1];
|
|
156
|
+
const details = worklist.slice(item.index + item[0].length, next ? next.index : worklist.length);
|
|
157
|
+
const missing = worklistDetailNames.filter(
|
|
158
|
+
(name) => !new RegExp(`^\\s*${name}:\\s+\\S`, "im").test(details)
|
|
159
|
+
);
|
|
160
|
+
const actionable = concreteStepPattern.test(item[1]) && implementationVerbPattern.test(item[1]);
|
|
161
|
+
return { text: item[1], missing, actionable };
|
|
162
|
+
})
|
|
163
|
+
.filter((item) => item.missing.length > 0 || !item.actionable);
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
present: prewalkPattern.test(task.body),
|
|
167
|
+
missingSections: prewalkSections.filter((name) => !hasHandoffSection(prewalk, name)),
|
|
168
|
+
invalidTrace,
|
|
169
|
+
lacksActualReadOrTrace: !actualReadOrTrace,
|
|
170
|
+
missingFacts,
|
|
171
|
+
completed,
|
|
172
|
+
worklistCount: items.length,
|
|
173
|
+
incompleteWorklist,
|
|
174
|
+
ok:
|
|
175
|
+
prewalkPattern.test(task.body) &&
|
|
176
|
+
prewalkSections.every((name) => hasHandoffSection(prewalk, name)) &&
|
|
177
|
+
invalidTrace.length === 0 &&
|
|
178
|
+
actualReadOrTrace &&
|
|
179
|
+
missingFacts.length === 0 &&
|
|
180
|
+
(completed || (items.length > 0 && items.length <= maximumWorklistItems && incompleteWorklist.length === 0))
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Ensure each code-task file is represented in the approved responsibility map. */
|
|
185
|
+
function findUnmappedCodeFiles(entries, structurePaths) {
|
|
186
|
+
return entries
|
|
187
|
+
.filter(({ match }) => match)
|
|
188
|
+
.filter(({ match }) => !structurePaths.some((path) => path.includes(match[2].trim())))
|
|
189
|
+
.map(({ line }) => line);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Parse file-operation rows so location and documentation-only rules can be checked consistently. */
|
|
193
|
+
function parseFileEntries(files) {
|
|
194
|
+
return files
|
|
195
|
+
.split(/\r?\n/)
|
|
196
|
+
.filter((line) => line.trim())
|
|
197
|
+
.map((line) => ({ line, match: line.match(fileOperationPattern) }));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Require code-change file rows to identify a new file or a meaningful stable source location. */
|
|
201
|
+
function findUnlocatedCodeFiles(entries) {
|
|
202
|
+
return entries
|
|
203
|
+
.filter(({ match }) => match)
|
|
204
|
+
.filter(({ match }) => {
|
|
205
|
+
const [, operation, , location] = match;
|
|
206
|
+
return operation === "Create" ? location.trim().toLowerCase() !== "new file" : !locationPattern.test(location.trim());
|
|
207
|
+
})
|
|
208
|
+
.map(({ line }) => line);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Require mechanics markers to contain an executable operation instead of only a label. */
|
|
212
|
+
function hasImplementationMechanics(changeMechanics) {
|
|
213
|
+
return (
|
|
214
|
+
mechanicsPattern.test(changeMechanics) &&
|
|
215
|
+
implementationVerbPattern.test(changeMechanics) &&
|
|
216
|
+
!genericMechanicsPattern.test(changeMechanics)
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Require every code-editing checklist item to state its smallest implementation mechanism. */
|
|
221
|
+
function findCodeStepsWithoutMechanics(steps) {
|
|
222
|
+
return steps.filter((step) => codeActionStepPattern.test(step) && !mechanicsPattern.test(step));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Keep the documentation-only exception from bypassing runtime-code plan requirements. */
|
|
226
|
+
function findRuntimeFilesInDocumentationTask(entries) {
|
|
227
|
+
return entries
|
|
228
|
+
.filter(({ match }) => match && !documentationPathPattern.test(match[2].trim()))
|
|
229
|
+
.map(({ line }) => line);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Verify that a static plan states how code changes and how the result will be proven. */
|
|
233
|
+
function hasVerificationExpectation(verify) {
|
|
234
|
+
return verificationCommandPattern.test(verify) && verificationExpectationPattern.test(verify);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Validate one task's file operations, task type, code-level contract, and verification evidence. */
|
|
238
|
+
function checkTask(task, fileStructure) {
|
|
239
|
+
const missing = requiredTaskFields.filter((field) => !fieldPatterns[field].test(task.body));
|
|
240
|
+
const files = fieldBlock(task.body, "Files");
|
|
241
|
+
const interfaces = fieldBlock(task.body, "Interfaces");
|
|
242
|
+
const taskType = fieldBlock(task.body, "Task type");
|
|
243
|
+
const changeMechanics = fieldBlock(task.body, "Change mechanics");
|
|
244
|
+
const verify = fieldBlock(task.body, "Verify");
|
|
245
|
+
const steps = [...fieldBlock(task.body, "Steps").matchAll(checkboxPattern)].map((match) => match[1]);
|
|
246
|
+
const unresolved = findMatches(task.body, unresolvedPatterns);
|
|
247
|
+
const actionableText = [fieldBlock(task.body, "Task"), fieldBlock(task.body, "Acceptance"), verify, ...steps].join("\n");
|
|
248
|
+
const vague = findMatches(actionableText, vaguePatterns);
|
|
249
|
+
const fileEntries = parseFileEntries(files);
|
|
250
|
+
const invalidFiles = fileEntries.filter(({ match }) => !match).map(({ line }) => line);
|
|
251
|
+
const missingInterfaces = ["Consumes", "Produces"].filter(
|
|
252
|
+
(name) => !new RegExp(`^\\s*-\\s*${name}:\\s+\\S`, "im").test(interfaces)
|
|
253
|
+
);
|
|
254
|
+
const vagueSteps = steps.filter((step) => !concreteStepPattern.test(step));
|
|
255
|
+
const isCodeChange = taskType === "Code change";
|
|
256
|
+
const isDocumentationOnly = taskType === "Documentation-only";
|
|
257
|
+
const invalidTaskType = !taskTypes.includes(taskType);
|
|
258
|
+
const missingCodeFields = isCodeChange
|
|
259
|
+
? codeChangeFields.filter((field) => !fieldPatterns[field].test(task.body))
|
|
260
|
+
: [];
|
|
261
|
+
const unlocatedCodeFiles = isCodeChange ? findUnlocatedCodeFiles(fileEntries) : [];
|
|
262
|
+
const unmappedCodeFiles = isCodeChange ? findUnmappedCodeFiles(fileEntries, fileStructure.paths) : [];
|
|
263
|
+
const prewalk = isCodeChange ? checkPrewalk(task) : null;
|
|
264
|
+
const codeStepsWithoutMechanics = isCodeChange ? findCodeStepsWithoutMechanics(steps) : [];
|
|
265
|
+
const runtimeFilesInDocumentationTask = isDocumentationOnly ? findRuntimeFilesInDocumentationTask(fileEntries) : [];
|
|
266
|
+
// Documentation-only must declare both interfaces so it cannot silently bypass runtime-code requirements.
|
|
267
|
+
const documentationOnlyInterfaces = isDocumentationOnly && ["Consumes", "Produces"].some(
|
|
268
|
+
(name) => !new RegExp(`^\\s*-\\s*${name}:\\s*documentation-only\\s*$`, "im").test(interfaces)
|
|
269
|
+
);
|
|
270
|
+
// Require substantive mechanics because labels alone leave the implementation decision to Build.
|
|
271
|
+
const missingMechanics = isCodeChange && !hasImplementationMechanics(changeMechanics);
|
|
272
|
+
const incompleteVerification = !hasVerificationExpectation(verify);
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
number: task.number,
|
|
276
|
+
missing,
|
|
277
|
+
unresolved,
|
|
278
|
+
vague,
|
|
279
|
+
invalidFiles,
|
|
280
|
+
missingInterfaces,
|
|
281
|
+
invalidTaskType,
|
|
282
|
+
missingCodeFields,
|
|
283
|
+
unlocatedCodeFiles,
|
|
284
|
+
unmappedCodeFiles,
|
|
285
|
+
prewalk,
|
|
286
|
+
codeStepsWithoutMechanics,
|
|
287
|
+
runtimeFilesInDocumentationTask,
|
|
288
|
+
documentationOnlyInterfaces,
|
|
289
|
+
missingMechanics,
|
|
290
|
+
incompleteVerification,
|
|
291
|
+
insufficientSteps: steps.length < 2,
|
|
292
|
+
vagueSteps,
|
|
293
|
+
ok:
|
|
294
|
+
missing.length === 0 &&
|
|
295
|
+
unresolved.length === 0 &&
|
|
296
|
+
vague.length === 0 &&
|
|
297
|
+
invalidFiles.length === 0 &&
|
|
298
|
+
missingInterfaces.length === 0 &&
|
|
299
|
+
!invalidTaskType &&
|
|
300
|
+
missingCodeFields.length === 0 &&
|
|
301
|
+
unlocatedCodeFiles.length === 0 &&
|
|
302
|
+
unmappedCodeFiles.length === 0 &&
|
|
303
|
+
(!isCodeChange || prewalk.ok) &&
|
|
304
|
+
codeStepsWithoutMechanics.length === 0 &&
|
|
305
|
+
runtimeFilesInDocumentationTask.length === 0 &&
|
|
306
|
+
!documentationOnlyInterfaces &&
|
|
307
|
+
!missingMechanics &&
|
|
308
|
+
!incompleteVerification &&
|
|
309
|
+
steps.length >= 2 &&
|
|
310
|
+
vagueSteps.length === 0
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** Validate global plan headers and all independently scoped task contracts. */
|
|
315
|
+
function checkPlan(body) {
|
|
316
|
+
const fileStructure = checkFileStructure(body);
|
|
317
|
+
const tasks = splitTasks(body);
|
|
318
|
+
const requiresFileStructure = tasks.some((task) => fieldBlock(task.body, "Task type") === "Code change");
|
|
319
|
+
const taskResults = tasks.map((task) => checkTask(task, fileStructure));
|
|
320
|
+
const missingGlobal = requiredGlobalFields.filter((field) => !fieldPatterns[field].test(body));
|
|
321
|
+
const globalUnresolved = findMatches(body.split(/\r?\nTask:/i)[0], unresolvedPatterns);
|
|
322
|
+
const statusMatch = body.match(statusPattern);
|
|
323
|
+
const status = statusMatch ? statusMatch[1].trim() : "legacy";
|
|
324
|
+
const invalidStatus = statusMatch ? !validStatuses.includes(status) : false;
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
missingGlobal,
|
|
328
|
+
globalUnresolved,
|
|
329
|
+
fileStructure,
|
|
330
|
+
requiresFileStructure,
|
|
331
|
+
status,
|
|
332
|
+
invalidStatus,
|
|
333
|
+
tasks: taskResults,
|
|
334
|
+
ok:
|
|
335
|
+
missingGlobal.length === 0 &&
|
|
336
|
+
globalUnresolved.length === 0 &&
|
|
337
|
+
!invalidStatus &&
|
|
338
|
+
(!requiresFileStructure || fileStructure.ok) &&
|
|
339
|
+
tasks.length > 0 &&
|
|
340
|
+
taskResults.every((task) => task.ok)
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** Keep generated implementation plans out of feature-ledger storage. */
|
|
345
|
+
function checkPlanLanding(filePath) {
|
|
346
|
+
if (!filePath) return { ok: true, message: "Plan landing: stdin input, no file path checked" };
|
|
347
|
+
|
|
348
|
+
const normalized = filePath.replaceAll("\\", "/");
|
|
349
|
+
const isDocsPlans = /(^|\/)docs\/plans\/[^/]+\.md$/i.test(normalized);
|
|
350
|
+
const isFeatureLedger = /(^|\/)docs\/features\/[^/]+\.md$/i.test(normalized);
|
|
351
|
+
|
|
352
|
+
if (isDocsPlans) return { ok: true, message: "Plan landing: ok docs/plans/YYYY-MM-DD-<short-kebab-name>.md" };
|
|
353
|
+
if (isFeatureLedger) {
|
|
354
|
+
return { ok: false, message: "Plan landing: docs/features is for feature ledgers; put implementation plans under docs/plans/YYYY-MM-DD-<short-kebab-name>.md" };
|
|
355
|
+
}
|
|
356
|
+
return { ok: true, message: "Plan landing: warning expected docs/plans/YYYY-MM-DD-<short-kebab-name>.md unless this project has a documented plan path" };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** Print actionable field-level failures rather than a generic plan rejection. */
|
|
360
|
+
function report(body, filePath, json) {
|
|
361
|
+
const result = checkPlan(body);
|
|
362
|
+
const landing = checkPlanLanding(filePath);
|
|
363
|
+
const judgment = !result.ok || !landing.ok ? "FAIL" : "PASS";
|
|
364
|
+
|
|
365
|
+
if (json) {
|
|
366
|
+
console.log(
|
|
367
|
+
JSON.stringify({
|
|
368
|
+
checker: "plan",
|
|
369
|
+
landing: landing.message,
|
|
370
|
+
status: result.status,
|
|
371
|
+
invalidStatus: result.invalidStatus,
|
|
372
|
+
missingGlobal: result.missingGlobal,
|
|
373
|
+
globalUnresolved: result.globalUnresolved,
|
|
374
|
+
fileStructure: result.fileStructure.ok ? "ok" : "missing or invalid",
|
|
375
|
+
tasks: result.tasks.map((task) => ({ number: task.number, ok: task.ok })),
|
|
376
|
+
judgment
|
|
377
|
+
})
|
|
378
|
+
);
|
|
379
|
+
return judgment === "PASS" ? 0 : 1;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
console.log("DevFlow plan pack report");
|
|
383
|
+
console.log(landing.message);
|
|
384
|
+
console.log(`Status: ${result.invalidStatus ? "invalid" : result.status}`);
|
|
385
|
+
for (const field of requiredGlobalFields) console.log(`${field}: ${result.missingGlobal.includes(field) ? "missing" : "ok"}`);
|
|
386
|
+
console.log(`Global unresolved markers: ${result.globalUnresolved.join(", ") || "none"}`);
|
|
387
|
+
console.log(
|
|
388
|
+
`File Structure: ${
|
|
389
|
+
!result.requiresFileStructure ? "documentation-only exception" : result.fileStructure.ok ? "ok" : "missing or invalid"
|
|
390
|
+
}`
|
|
391
|
+
);
|
|
392
|
+
if (result.requiresFileStructure && !result.fileStructure.ok) {
|
|
393
|
+
console.log(`File Structure invalid rows: ${result.fileStructure.invalidRows.join("; ") || "none"}`);
|
|
394
|
+
}
|
|
395
|
+
console.log(`Tasks: ${result.tasks.length}`);
|
|
396
|
+
if (result.tasks.length === 0) console.log("Missing: at least one Task field");
|
|
397
|
+
|
|
398
|
+
for (const task of result.tasks) {
|
|
399
|
+
const issues = [
|
|
400
|
+
...task.missing.map((field) => `missing ${field}`),
|
|
401
|
+
...task.unresolved.map((match) => `unresolved ${match}`),
|
|
402
|
+
...task.vague.map((match) => `vague ${match}`),
|
|
403
|
+
...task.invalidFiles.map((line) => `unclassified file ${line}`),
|
|
404
|
+
...task.missingInterfaces.map((field) => `missing interface ${field}`),
|
|
405
|
+
...(task.invalidTaskType ? ["Task type must be Code change or Documentation-only"] : []),
|
|
406
|
+
...task.missingCodeFields.map((field) => `missing code-change field ${field}`),
|
|
407
|
+
...task.unlocatedCodeFiles.map((line) => `missing file symbol/anchor ${line}`),
|
|
408
|
+
...task.unmappedCodeFiles.map((line) => `file missing File Structure responsibility ${line}`),
|
|
409
|
+
...(task.prewalk ? task.prewalk.missingSections.map((name) => `Prewalk missing ${name}`) : []),
|
|
410
|
+
...(task.prewalk ? task.prewalk.invalidTrace.map((line) => `invalid Execution Trace ${line}`) : []),
|
|
411
|
+
...(task.prewalk?.lacksActualReadOrTrace ? ["Prewalk needs actual Read or Traced evidence"] : []),
|
|
412
|
+
...(task.prewalk ? task.prewalk.missingFacts.map((name) => `Prewalk missing handoff fact ${name}`) : []),
|
|
413
|
+
...(task.prewalk && task.prewalk.worklistCount === 0 && !task.prewalk.completed ? ["Prewalk needs remaining structured work"] : []),
|
|
414
|
+
...(task.prewalk && task.prewalk.worklistCount > maximumWorklistItems ? [`Prewalk worklist exceeds ${maximumWorklistItems} items`] : []),
|
|
415
|
+
...(task.prewalk
|
|
416
|
+
? task.prewalk.incompleteWorklist.map(
|
|
417
|
+
(item) => `Prewalk incomplete work item ${item.text}: ${item.missing.join(", ") || "needs a concrete action"}`
|
|
418
|
+
)
|
|
419
|
+
: []),
|
|
420
|
+
...task.codeStepsWithoutMechanics.map((step) => `code step needs snippet, pseudocode, or exact replacement ${step}`),
|
|
421
|
+
...task.runtimeFilesInDocumentationTask.map((line) => `documentation-only task has runtime file ${line}`),
|
|
422
|
+
...(task.documentationOnlyInterfaces ? ["documentation-only task must declare documentation-only interfaces"] : []),
|
|
423
|
+
...(task.missingMechanics ? ["code change needs code snippet, pseudocode, or exact replacement"] : []),
|
|
424
|
+
...(task.incompleteVerification ? ["Verify needs command/scenario and expected result"] : []),
|
|
425
|
+
...(task.insufficientSteps ? ["fewer than two checkbox Steps"] : []),
|
|
426
|
+
...task.vagueSteps.map((step) => `vague step ${step}`)
|
|
427
|
+
];
|
|
428
|
+
console.log(`Task ${task.number}: ${issues.length === 0 ? "ok" : issues.join("; ")}`);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (!result.ok || !landing.ok) {
|
|
432
|
+
console.log("Judgment: FAIL");
|
|
433
|
+
return 1;
|
|
434
|
+
}
|
|
435
|
+
console.log("Judgment: PASS");
|
|
436
|
+
return 0;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/** Read a named plan file or allow pipeline input for ad-hoc validation. */
|
|
440
|
+
function readInput(args) {
|
|
441
|
+
const targetArg = args.find((arg) => !arg.startsWith("-"));
|
|
442
|
+
return targetArg ? fs.readFileSync(targetArg, "utf8") : fs.readFileSync(0, "utf8");
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/** Exercise accepted and rejected forms of the code-level static plan contract without fixture files. */
|
|
446
|
+
function selfTest() {
|
|
447
|
+
const validPlan = [
|
|
448
|
+
"Goal: Validate a code-level plan contract",
|
|
449
|
+
"Architecture: Static Node checker",
|
|
450
|
+
"Tech Stack: Node.js",
|
|
451
|
+
"Source: docs/specs/2026-07-14-add-plan-scanner.md",
|
|
452
|
+
"Spec coverage: Requirements map to Task 1",
|
|
453
|
+
"External Skills: none",
|
|
454
|
+
"## File Structure",
|
|
455
|
+
"",
|
|
456
|
+
"| File / symbol | Operation | Responsibility | Why here | Not responsible for |",
|
|
457
|
+
"|---|---|---|---|---|",
|
|
458
|
+
"| `scripts/devflow-plan.js` / `checkTask` | Modify | validate code-level task contracts | existing checker owns validation | architecture judgment |",
|
|
459
|
+
"",
|
|
460
|
+
"Task: Add plan scanner",
|
|
461
|
+
"Task type: Code change",
|
|
462
|
+
"Files:",
|
|
463
|
+
"- Modify: scripts/devflow-plan.js | function checkTask | validate code-level task contracts",
|
|
464
|
+
"Interfaces:",
|
|
465
|
+
"- Consumes: checkTask(task: { body: string })",
|
|
466
|
+
"- Produces: { ok: boolean, missing: string[] }",
|
|
467
|
+
"Current behavior: only legacy structural fields are checked",
|
|
468
|
+
"Target behavior: code tasks require location, mechanics, and proof",
|
|
469
|
+
"Change mechanics: pseudocode: parse task type, validate required code fields, report failures",
|
|
470
|
+
"Call impact: node scripts/devflow-plan.js keeps the same CLI contract",
|
|
471
|
+
"Steps:",
|
|
472
|
+
"- [ ] Modify `scripts/devflow-plan.js` function checkTask using pseudocode: require code fields and precise file locations",
|
|
473
|
+
"- [ ] Run `node scripts/devflow-plan.js --self-test` with the valid task and expect DevFlow plan self-test passed",
|
|
474
|
+
"Acceptance: reports PASS for a complete code-level task",
|
|
475
|
+
"Verify: Run `node scripts/devflow-plan.js --self-test`; expect DevFlow plan self-test passed",
|
|
476
|
+
"Comments: checkTask has a function comment explaining task-boundary validation.",
|
|
477
|
+
"Not doing: generating plans or judging architecture",
|
|
478
|
+
"",
|
|
479
|
+
"Prewalk:",
|
|
480
|
+
"",
|
|
481
|
+
"Execution Trace:",
|
|
482
|
+
"- Read: `scripts/devflow-plan.js` / `checkTask` → existing validation is flat task-field checking.",
|
|
483
|
+
"- Traced: `report` → CLI prints aggregated task issues.",
|
|
484
|
+
"- Ran: `node scripts/devflow-plan.js --self-test` → baseline self-test command is available.",
|
|
485
|
+
"- Edited: none yet → validator change remains pending.",
|
|
486
|
+
"- Verified: `checkPlan` → current valid plan fixture is accepted.",
|
|
487
|
+
"",
|
|
488
|
+
"Current Handoff Facts:",
|
|
489
|
+
"- Target anchors: `scripts/devflow-plan.js` / `checkTask`.",
|
|
490
|
+
"- Nearby convention: `checkPlan` aggregates issue arrays.",
|
|
491
|
+
"- Direct path: CLI invokes `report`.",
|
|
492
|
+
"- Current constraints: documentation-only tasks stay exempt.",
|
|
493
|
+
"- Planned touch set: `scripts/devflow-plan.js` / `checkTask`.",
|
|
494
|
+
"- Risks / stop conditions: parser boundary contradiction returns to Core.",
|
|
495
|
+
"- Read-basis: scripts/devflow-plan.js.",
|
|
496
|
+
"- Live anchors: scripts/devflow-plan.js / checkTask.",
|
|
497
|
+
"",
|
|
498
|
+
"Remaining Structured Worklist:",
|
|
499
|
+
"- [ ] Modify `scripts/devflow-plan.js` / `checkTask` using pseudocode: validate handoff fields.",
|
|
500
|
+
" Anchors: `scripts/devflow-plan.js` / `checkTask`.",
|
|
501
|
+
" Verify: Run `node scripts/devflow-plan.js --self-test`; expect DevFlow plan self-test passed.",
|
|
502
|
+
" Done when: handoff failures are reported."
|
|
503
|
+
].join("\n");
|
|
504
|
+
const validDocumentationPlan = [
|
|
505
|
+
"Goal: Document a plan contract",
|
|
506
|
+
"Architecture: Markdown-only change",
|
|
507
|
+
"Tech Stack: Markdown",
|
|
508
|
+
"Source: approved design",
|
|
509
|
+
"Spec coverage: Documentation requirement maps to Task 1",
|
|
510
|
+
"External Skills: none",
|
|
511
|
+
"Task: Document plan fields",
|
|
512
|
+
"Task type: Documentation-only",
|
|
513
|
+
"Files:",
|
|
514
|
+
"- Modify: README.md | Plan Pack section | describe code-level fields",
|
|
515
|
+
"Interfaces:",
|
|
516
|
+
"- Consumes: documentation-only",
|
|
517
|
+
"- Produces: documentation-only",
|
|
518
|
+
"Steps:",
|
|
519
|
+
"- [ ] Modify `README.md` at Plan Pack section to list the code-level contract",
|
|
520
|
+
"- [ ] Run `node scripts/devflow-plan.js --self-test` and expect the static self-test to pass",
|
|
521
|
+
"Acceptance: README explains the code-level plan fields",
|
|
522
|
+
"Verify: Run `node scripts/devflow-plan.js --self-test`; expect DevFlow plan self-test passed",
|
|
523
|
+
"Comments: none — trivial documentation change",
|
|
524
|
+
"Not doing: changing runtime code"
|
|
525
|
+
].join("\n");
|
|
526
|
+
const genericLocationPlan = validPlan.replace("function checkTask", "abc");
|
|
527
|
+
const missingCurrentBehaviorPlan = validPlan.replace("Current behavior: only legacy structural fields are checked\n", "");
|
|
528
|
+
const missingTargetBehaviorPlan = validPlan.replace("Target behavior: code tasks require location, mechanics, and proof\n", "");
|
|
529
|
+
const missingMechanicsPlan = validPlan.replace("Change mechanics: pseudocode: parse task type, validate required code fields, report failures\n", "");
|
|
530
|
+
const missingCallImpactPlan = validPlan.replace("Call impact: node scripts/devflow-plan.js keeps the same CLI contract\n", "");
|
|
531
|
+
const genericMechanicsPlan = validPlan.replace("pseudocode: parse task type, validate required code fields, report failures", "pseudocode: update it");
|
|
532
|
+
const codeStepWithoutMechanicsPlan = validPlan.replace(
|
|
533
|
+
"- [ ] Modify `scripts/devflow-plan.js` function checkTask using pseudocode: require code fields and precise file locations",
|
|
534
|
+
"- [ ] Modify `scripts/devflow-plan.js` function checkTask"
|
|
535
|
+
);
|
|
536
|
+
const incompleteVerificationPlan = validPlan.replace("Verify: Run `node scripts/devflow-plan.js --self-test`; expect DevFlow plan self-test passed", "Verify: run a check");
|
|
537
|
+
const invalidDocumentationTaskPlan = validDocumentationPlan.replace("README.md", "scripts/devflow-plan.js");
|
|
538
|
+
const missingDocumentationInterfacePlan = validDocumentationPlan.replace("- Produces: documentation-only\n", "- Produces: plan documentation\n");
|
|
539
|
+
const missingTaskTypePlan = validPlan.replace("Task type: Code change\n", "");
|
|
540
|
+
const unclassifiedFilePlan = validPlan.replace(
|
|
541
|
+
"- Modify: scripts/devflow-plan.js | function checkTask | validate code-level task contracts",
|
|
542
|
+
"- scripts/devflow-plan.js"
|
|
543
|
+
);
|
|
544
|
+
const insufficientStepsPlan = validPlan.replace(
|
|
545
|
+
"- [ ] Run `node scripts/devflow-plan.js --self-test` with the valid task and expect DevFlow plan self-test passed\n",
|
|
546
|
+
""
|
|
547
|
+
);
|
|
548
|
+
const vagueStepPlan = validPlan.replace(
|
|
549
|
+
"- [ ] Modify `scripts/devflow-plan.js` function checkTask using pseudocode: require code fields and precise file locations",
|
|
550
|
+
"- [ ] Make it work"
|
|
551
|
+
);
|
|
552
|
+
const missingFieldPlan = validPlan.replace("Verify: Run `node scripts/devflow-plan.js --self-test`; expect DevFlow plan self-test passed\n", "");
|
|
553
|
+
const missingSourcePlan = validPlan.replace("Source: docs/specs/2026-07-14-add-plan-scanner.md\n", "");
|
|
554
|
+
const missingExternalSkillsPlan = validPlan.replace("External Skills: none\n", "");
|
|
555
|
+
const missingFileStructurePlan = validPlan.replace("## File Structure\n\n| File / symbol | Operation | Responsibility | Why here | Not responsible for |\n|---|---|---|---|---|\n| `scripts/devflow-plan.js` / `checkTask` | Modify | validate code-level task contracts | existing checker owns validation | architecture judgment |\n\n", "");
|
|
556
|
+
const futureTracePlan = validPlan.replace(
|
|
557
|
+
"- Read: `scripts/devflow-plan.js` / `checkTask` → existing validation is flat task-field checking.",
|
|
558
|
+
"- Read: `scripts/devflow-plan.js` / `checkTask` → will inspect validation later."
|
|
559
|
+
);
|
|
560
|
+
const missingTraceResultPlan = validPlan.replace(
|
|
561
|
+
"- Traced: `report` → CLI prints aggregated task issues.",
|
|
562
|
+
"- Traced: `report`"
|
|
563
|
+
);
|
|
564
|
+
const incompleteWorklistPlan = validPlan.replace(
|
|
565
|
+
" Done when: handoff failures are reported.",
|
|
566
|
+
""
|
|
567
|
+
);
|
|
568
|
+
const overCapWorklistPlan = validPlan.replace(
|
|
569
|
+
" Done when: handoff failures are reported.",
|
|
570
|
+
Array.from({ length: 12 }, (_, index) =>
|
|
571
|
+
[
|
|
572
|
+
" Done when: handoff failures are reported.",
|
|
573
|
+
"- [ ] Modify scripts/devflow-plan.js / checkTask" + index + " using pseudocode: validate handoff fields.",
|
|
574
|
+
" Anchors: scripts/devflow-plan.js / checkTask" + index + ".",
|
|
575
|
+
" Verify: Run node scripts/devflow-plan.js --self-test; expect DevFlow plan self-test passed.",
|
|
576
|
+
" Done when: handoff failures are reported."
|
|
577
|
+
].join("\n")
|
|
578
|
+
).join("\n")
|
|
579
|
+
);
|
|
580
|
+
|
|
581
|
+
const missingReadBasisPlan = validPlan.replace("- Read-basis: scripts/devflow-plan.js.\n", "");
|
|
582
|
+
const missingLiveAnchorsPlan = validPlan.replace("- Live anchors: scripts/devflow-plan.js / checkTask.\n", "");
|
|
583
|
+
|
|
584
|
+
if (!checkPlan(validPlan).ok) throw new Error("Self-test expected complete code-level plan to pass");
|
|
585
|
+
if (!checkPlan(validDocumentationPlan).ok) throw new Error("Self-test expected documentation-only plan to pass");
|
|
586
|
+
const withValidStatusPlan = validPlan.replace(
|
|
587
|
+
"Goal: Validate a code-level plan contract",
|
|
588
|
+
"Status: approved\nGoal: Validate a code-level plan contract"
|
|
589
|
+
);
|
|
590
|
+
const withInvalidStatusPlan = validPlan.replace(
|
|
591
|
+
"Goal: Validate a code-level plan contract",
|
|
592
|
+
"Status: shipped\nGoal: Validate a code-level plan contract"
|
|
593
|
+
);
|
|
594
|
+
if (!checkPlan(withValidStatusPlan).ok) throw new Error("Self-test expected valid Status to pass");
|
|
595
|
+
if (checkPlan(withInvalidStatusPlan).ok) throw new Error("Self-test expected invalid Status to fail");
|
|
596
|
+
if (checkPlan(validPlan).status !== "legacy") throw new Error("Self-test expected missing Status to stay legacy");
|
|
597
|
+
if (checkPlan(genericLocationPlan).ok) throw new Error("Self-test expected generic location to fail");
|
|
598
|
+
if (checkPlan(missingCurrentBehaviorPlan).ok) throw new Error("Self-test expected missing Current behavior to fail");
|
|
599
|
+
if (checkPlan(missingTargetBehaviorPlan).ok) throw new Error("Self-test expected missing Target behavior to fail");
|
|
600
|
+
if (checkPlan(missingMechanicsPlan).ok) throw new Error("Self-test expected missing Change mechanics to fail");
|
|
601
|
+
if (checkPlan(missingCallImpactPlan).ok) throw new Error("Self-test expected missing Call impact to fail");
|
|
602
|
+
if (checkPlan(genericMechanicsPlan).ok) throw new Error("Self-test expected generic mechanics to fail");
|
|
603
|
+
if (checkPlan(codeStepWithoutMechanicsPlan).ok) throw new Error("Self-test expected code step without mechanics to fail");
|
|
604
|
+
if (checkPlan(incompleteVerificationPlan).ok) throw new Error("Self-test expected incomplete verification to fail");
|
|
605
|
+
if (checkPlan(invalidDocumentationTaskPlan).ok) throw new Error("Self-test expected runtime file in documentation-only task to fail");
|
|
606
|
+
if (checkPlan(missingDocumentationInterfacePlan).ok) throw new Error("Self-test expected documentation-only interface to fail");
|
|
607
|
+
if (checkPlan(missingTaskTypePlan).ok) throw new Error("Self-test expected missing Task type to fail");
|
|
608
|
+
if (checkPlan(unclassifiedFilePlan).ok) throw new Error("Self-test expected unclassified file to fail");
|
|
609
|
+
if (checkPlan(insufficientStepsPlan).ok) throw new Error("Self-test expected insufficient steps to fail");
|
|
610
|
+
if (checkPlan(vagueStepPlan).ok) throw new Error("Self-test expected vague step to fail");
|
|
611
|
+
if (checkPlan(missingFieldPlan).ok) throw new Error("Self-test expected missing Verify to fail");
|
|
612
|
+
if (checkPlan(missingSourcePlan).ok) throw new Error("Self-test expected missing Source to fail");
|
|
613
|
+
if (checkPlan(missingExternalSkillsPlan).ok) throw new Error("Self-test expected missing External Skills to fail");
|
|
614
|
+
if (checkPlan(missingFileStructurePlan).ok) throw new Error("Self-test expected missing File Structure to fail");
|
|
615
|
+
if (checkPlan(futureTracePlan).ok) throw new Error("Self-test expected future-tense trace to fail");
|
|
616
|
+
if (checkPlan(missingTraceResultPlan).ok) throw new Error("Self-test expected trace without observed result to fail");
|
|
617
|
+
if (checkPlan(missingReadBasisPlan).ok) throw new Error("Self-test expected missing Read-basis handoff fact to fail");
|
|
618
|
+
if (checkPlan(missingLiveAnchorsPlan).ok) throw new Error("Self-test expected missing Live anchors handoff fact to fail");
|
|
619
|
+
if (checkPlan(incompleteWorklistPlan).ok) throw new Error("Self-test expected incomplete structured worklist to fail");
|
|
620
|
+
if (checkPlan(overCapWorklistPlan).ok) throw new Error("Self-test expected over-cap structured worklist to fail");
|
|
621
|
+
if (!checkPlanLanding("docs/plans/2026-07-14-add-plan-scanner.md").ok) throw new Error("Self-test expected docs/plans landing to pass");
|
|
622
|
+
if (checkPlanLanding("docs/features/add-plan-scanner.md").ok) throw new Error("Self-test expected docs/features plan landing to fail");
|
|
623
|
+
|
|
624
|
+
console.log("DevFlow plan self-test passed");
|
|
625
|
+
console.log("Checked code-level fields, precise file locations, mechanics evidence, verification expectations, Read-basis/Live anchors handoff facts, documentation-only exception, external-skill declaration, and plan landing guidance");
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const args = process.argv.slice(2);
|
|
629
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
630
|
+
usage();
|
|
631
|
+
process.exit(0);
|
|
632
|
+
}
|
|
633
|
+
if (args.includes("--self-test")) {
|
|
634
|
+
selfTest();
|
|
635
|
+
process.exit(0);
|
|
636
|
+
}
|
|
637
|
+
const targetArg = args.find((arg) => !arg.startsWith("-"));
|
|
638
|
+
process.exitCode = report(readInput(args), targetArg, args.includes("--json"));
|