@dotdotgod/cli 0.1.43 → 0.1.45
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/package.json +1 -1
- package/src/commands/plan-stage-contract.mjs +5 -3
- package/src/commands/plan.mjs +115 -111
- package/src/validate/run.mjs +1 -0
package/package.json
CHANGED
|
@@ -41,8 +41,9 @@ export const PLAN_BLOCKER_LABELS = new Map([
|
|
|
41
41
|
['invalid-task-slug', 'Plan task directory must be kebab-case'],
|
|
42
42
|
['empty-plan', 'Plan README is empty'],
|
|
43
43
|
['missing-stage', 'Missing required stage directory'],
|
|
44
|
-
['missing-internal-stage', 'Missing internal stage file'],
|
|
45
44
|
['missing-stage-readme', 'Missing stage README.md'],
|
|
45
|
+
['missing-internal-stage', 'Missing internal stage checkpoint'],
|
|
46
|
+
['open-internal-stage', 'Internal stage checkpoint is not completed'],
|
|
46
47
|
['invalid-markdown-name', 'Invalid Markdown file name'],
|
|
47
48
|
['missing-section', 'Missing required section'],
|
|
48
49
|
['empty-section', 'Required section has no content'],
|
|
@@ -66,8 +67,9 @@ export function buildPlanValidationNextStagePrompt(planPath, stage, stagePath) {
|
|
|
66
67
|
return [
|
|
67
68
|
`Continue Plan Mode stage advancement for ${planPath}.`,
|
|
68
69
|
`Current stage: ${stage}.`,
|
|
69
|
-
`
|
|
70
|
-
|
|
70
|
+
`Update the durable plan for human readers, then make ${stagePath} the machine-readable checkpoint that satisfies the current stage validation contract. Do not create later checkpoint files yet.`,
|
|
71
|
+
`In ${stagePath}, set Status: completed and include the current stage required sections plus \`## Stage ${stage.slice(0, 2)} Construction Checklist\` rows in the canonical \`- [x] Category: evidence\` form.`,
|
|
72
|
+
stage === '05-workstream-handoff' ? 'For this stage checkpoint, record `Split decision: yes` or `Split decision: no` in `## Workstream Handoff`. If splitting, make `## Workstream Map`, `## Workstreams`, and `## Integration Sequence` describe multiple actionable workstreams with required context, allowed/forbidden edits, tasks, validation, dependencies, and handoff output. If not splitting, record the no-split rationale and keep `## Todo Contract` actionable so Pi can extract execution todos.' : undefined,
|
|
71
73
|
`After completing this stage, stop and rerun \`dotdotgod plan validate ${planPath} --stage ${stage} --json\`.`,
|
|
72
74
|
].filter(Boolean).join('\n\n');
|
|
73
75
|
}
|
package/src/commands/plan.mjs
CHANGED
|
@@ -20,12 +20,14 @@ export {
|
|
|
20
20
|
PLAN_STAGE_DIRECTORIES,
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
-
const PLACEHOLDER_RE = /^(?:[-*\s_`]*(?:tbd|todo|n\/a|placeholder|none)[.!?\s_`]*)+$/i;
|
|
24
|
-
const CHECKLIST_OPEN_RE = /\b(?:open|unresolved|pending|needs confirmation|needs user|unknown)\b/i;
|
|
25
23
|
function normalizeHeading(value) {
|
|
26
24
|
return String(value ?? '').trim().toLowerCase().replace(/`/g, '').replace(/[\s/_-]+/g, ' ');
|
|
27
25
|
}
|
|
28
26
|
|
|
27
|
+
function escapeRegExp(value) {
|
|
28
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
29
|
+
}
|
|
30
|
+
|
|
29
31
|
function projectRelative(root, file) {
|
|
30
32
|
return relative(root, file).replace(/\\/g, '/');
|
|
31
33
|
}
|
|
@@ -67,16 +69,6 @@ function contentHasValue(content) {
|
|
|
67
69
|
return stripped.length > 0;
|
|
68
70
|
}
|
|
69
71
|
|
|
70
|
-
function isPlaceholderContent(content) {
|
|
71
|
-
const stripped = content
|
|
72
|
-
.split(/\r?\n/)
|
|
73
|
-
.map((line) => line.replace(/^\s*[-*+]\s+/, '').trim())
|
|
74
|
-
.filter(Boolean)
|
|
75
|
-
.join(' ')
|
|
76
|
-
.trim();
|
|
77
|
-
return Boolean(stripped) && PLACEHOLDER_RE.test(stripped);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
72
|
function readMarkdownFiles(stageDir) {
|
|
81
73
|
if (!existsSync(stageDir)) return [];
|
|
82
74
|
return readdirSync(stageDir, { withFileTypes: true })
|
|
@@ -201,17 +193,37 @@ function readInternalStageState(taskDir, stageName) {
|
|
|
201
193
|
|
|
202
194
|
function validateInternalStageState(root, taskDir, stageName, blockers) {
|
|
203
195
|
const state = readInternalStageState(taskDir, stageName);
|
|
204
|
-
|
|
196
|
+
const expectedPath = internalStageFile(taskDir, stageName);
|
|
197
|
+
const rel = projectRelative(root, state?.file ?? expectedPath);
|
|
198
|
+
if (!state) {
|
|
199
|
+
addBlocker(blockers, 'missing-internal-stage', `Missing internal stage checkpoint: ${rel}`, { path: rel, stage: stageName });
|
|
200
|
+
return { state: undefined, valid: false };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
let valid = true;
|
|
205
204
|
const stage = contentMatch(state.content, /^Stage:\s*(\S+)\s*$/m);
|
|
206
205
|
const status = contentMatch(state.content, /^Status:\s*(created|blocked|completed)\s*$/m);
|
|
207
206
|
const updated = contentMatch(state.content, /^Updated:\s*(\S+)\s*$/m);
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
if (!stage)
|
|
213
|
-
|
|
214
|
-
|
|
207
|
+
if (stage && stage !== stageName) {
|
|
208
|
+
valid = false;
|
|
209
|
+
addBlocker(blockers, 'missing-internal-stage', `Internal stage file has wrong Stage field: ${rel}`, { path: rel, stage: stageName });
|
|
210
|
+
}
|
|
211
|
+
if (!stage) {
|
|
212
|
+
valid = false;
|
|
213
|
+
addBlocker(blockers, 'missing-internal-stage', `Internal stage file is missing Stage: ${rel}`, { path: rel, stage: stageName });
|
|
214
|
+
}
|
|
215
|
+
if (!status) {
|
|
216
|
+
valid = false;
|
|
217
|
+
addBlocker(blockers, 'missing-internal-stage', `Internal stage file is missing valid Status: ${rel}`, { path: rel, stage: stageName });
|
|
218
|
+
} else if (status !== 'completed') {
|
|
219
|
+
valid = false;
|
|
220
|
+
addBlocker(blockers, 'open-internal-stage', `Internal stage checkpoint is not completed: ${rel}`, { path: rel, stage: stageName });
|
|
221
|
+
}
|
|
222
|
+
if (!updated) {
|
|
223
|
+
valid = false;
|
|
224
|
+
addBlocker(blockers, 'missing-internal-stage', `Internal stage file is missing Updated: ${rel}`, { path: rel, stage: stageName });
|
|
225
|
+
}
|
|
226
|
+
return { state, valid };
|
|
215
227
|
}
|
|
216
228
|
|
|
217
229
|
function contentMatch(content, pattern) {
|
|
@@ -244,33 +256,21 @@ function validateMarkdownNames(root, stageName, stageDir, blockers) {
|
|
|
244
256
|
}
|
|
245
257
|
}
|
|
246
258
|
|
|
247
|
-
function
|
|
248
|
-
const
|
|
249
|
-
return
|
|
259
|
+
function sectionContainsField(content, labels) {
|
|
260
|
+
const text = removeCodeBlocks(content);
|
|
261
|
+
return labels.some((label) => new RegExp(`(?:^|\\b)${escapeRegExp(label)}\\s*:\\s*\\S`, 'im').test(text));
|
|
250
262
|
}
|
|
251
263
|
|
|
252
|
-
function
|
|
253
|
-
return content.split(/\r?\n/).some((line) =>
|
|
254
|
-
const normalized = line.toLowerCase();
|
|
255
|
-
if (!normalized.includes('pending')) return false;
|
|
256
|
-
return /\byes\b|\brequired\b/.test(normalized);
|
|
257
|
-
});
|
|
264
|
+
function hasUncheckedChecklistItem(content) {
|
|
265
|
+
return content.split(/\r?\n/).some((line) => /^\s*[-*+]\s*\[\s\]/.test(line));
|
|
258
266
|
}
|
|
259
267
|
|
|
260
|
-
function
|
|
261
|
-
return content.split(/\r?\n/).some((line) =>
|
|
262
|
-
const normalized = line.toLowerCase();
|
|
263
|
-
if (/^\s*-\s*\[\s\]/.test(line)) return true;
|
|
264
|
-
return /status\s*:\s*(open|research[-_ ]requested|plan[-_ ]revision[-_ ]requested|unresolved|pending)/.test(normalized);
|
|
265
|
-
});
|
|
268
|
+
function hasOpenStructuredStatus(content) {
|
|
269
|
+
return content.split(/\r?\n/).some((line) => /^\s*(?:[-*+]\s*)?(?:Status|DecisionState|Decision State)\s*:\s*(?:open|blocked|unresolved|pending|research[-_ ]requested|plan[-_ ]revision[-_ ]requested)\s*$/i.test(line));
|
|
266
270
|
}
|
|
267
271
|
|
|
268
|
-
function
|
|
269
|
-
return content
|
|
270
|
-
const normalized = line.toLowerCase();
|
|
271
|
-
if (/^\s*-\s*\[\s\]/.test(line)) return true;
|
|
272
|
-
return /\b(unresolved|pending|unknown|needs user|needs confirmation)\b/.test(normalized);
|
|
273
|
-
});
|
|
272
|
+
function hasUnresolvedStructuredItem(content) {
|
|
273
|
+
return hasUncheckedChecklistItem(content) || hasOpenStructuredStatus(content);
|
|
274
274
|
}
|
|
275
275
|
|
|
276
276
|
function workstreamSplitDecision(content) {
|
|
@@ -278,36 +278,32 @@ function workstreamSplitDecision(content) {
|
|
|
278
278
|
return match?.[1].toLowerCase();
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
+
function fieldPattern(label, flags = 'im') {
|
|
282
|
+
return new RegExp(`^\\s*(?:[-*+]\\s*)?${escapeRegExp(label)}\\s*:\\s*(\\S.*)$`, flags);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function fieldHasValue(content, label) {
|
|
286
|
+
return Boolean(fieldPattern(label).exec(removeCodeBlocks(content))?.[1]?.trim());
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function fieldOccurrences(content, label) {
|
|
290
|
+
return [...removeCodeBlocks(content).matchAll(fieldPattern(label, 'img'))].length;
|
|
291
|
+
}
|
|
292
|
+
|
|
281
293
|
function hasNoSplitRationale(content) {
|
|
282
|
-
return
|
|
294
|
+
return fieldHasValue(content, 'No-split rationale');
|
|
283
295
|
}
|
|
284
296
|
|
|
285
297
|
function hasMultipleWorkstreams(content) {
|
|
286
|
-
|
|
287
|
-
const explicitIds = new Set([...normalized.matchAll(/\b(?:workstream|ws)\s*[-:#]?\s*([a-z0-9]+)\b/g)].map((match) => match[1]));
|
|
288
|
-
if (explicitIds.size >= 2) return true;
|
|
289
|
-
const workstreamMentions = normalized.match(/\bworkstreams?\b/g) ?? [];
|
|
290
|
-
return workstreamMentions.length >= 2 && /\b(?:multiple|parallel|split)\b/.test(normalized);
|
|
298
|
+
return fieldOccurrences(content, 'Workstream ID') >= 2;
|
|
291
299
|
}
|
|
292
300
|
|
|
293
301
|
function hasActionableWorkstreamFields(content) {
|
|
294
|
-
|
|
295
|
-
const fieldGroups = [
|
|
296
|
-
['purpose', 'objective'],
|
|
297
|
-
['required context', 'read'],
|
|
298
|
-
['allowed edit'],
|
|
299
|
-
['forbidden edit', 'do not edit', 'do-not edit'],
|
|
300
|
-
['task'],
|
|
301
|
-
['validation', 'verify'],
|
|
302
|
-
['handoff output', 'output'],
|
|
303
|
-
['dependenc'],
|
|
304
|
-
];
|
|
305
|
-
return fieldGroups.every((group) => group.some((term) => normalized.includes(term)));
|
|
302
|
+
return ['Workstream ID', 'Purpose', 'Required context', 'Allowed edits', 'Forbidden edits', 'Tasks', 'Validation', 'Handoff output', 'Dependencies'].every((label) => fieldHasValue(content, label));
|
|
306
303
|
}
|
|
307
304
|
|
|
308
305
|
function hasActionableIntegrationFields(content) {
|
|
309
|
-
|
|
310
|
-
return ['sequence', 'integrat'].every((term) => normalized.includes(term)) && ['validation', 'verify', 'handoff'].some((term) => normalized.includes(term));
|
|
306
|
+
return ['Step', 'Validation', 'Handoff'].every((label) => fieldHasValue(content, label));
|
|
311
307
|
}
|
|
312
308
|
|
|
313
309
|
function validateWorkstreamHandoffContract(root, files, stageName, blockers) {
|
|
@@ -324,9 +320,9 @@ function validateWorkstreamHandoffContract(root, files, stageName, blockers) {
|
|
|
324
320
|
if (decision === 'no') {
|
|
325
321
|
if (!hasNoSplitRationale(handoff.content)) {
|
|
326
322
|
valid = false;
|
|
327
|
-
addBlocker(blockers, 'missing-workstream-no-split-rationale', 'Workstream Handoff with `Split decision: no` must include
|
|
323
|
+
addBlocker(blockers, 'missing-workstream-no-split-rationale', 'Workstream Handoff with `Split decision: no` must include `No-split rationale:`.', { path: relHandoff, stage: stageName, section: 'Workstream Handoff' });
|
|
328
324
|
}
|
|
329
|
-
if (!todo || !contentHasValue(todo.content)
|
|
325
|
+
if (!todo || !contentHasValue(todo.content)) valid = false;
|
|
330
326
|
return valid;
|
|
331
327
|
}
|
|
332
328
|
|
|
@@ -336,15 +332,15 @@ function validateWorkstreamHandoffContract(root, files, stageName, blockers) {
|
|
|
336
332
|
const splitContent = [map?.content, workstreams?.content, integration?.content].filter(Boolean).join('\n');
|
|
337
333
|
if (!hasMultipleWorkstreams(splitContent)) {
|
|
338
334
|
valid = false;
|
|
339
|
-
addBlocker(blockers, 'incomplete-workstream-split', 'Split Stage 05 handoff must
|
|
335
|
+
addBlocker(blockers, 'incomplete-workstream-split', 'Split Stage 05 handoff must include at least two `Workstream ID:` fields in Workstream Map, Workstreams, or Integration Sequence.', { path: projectRelative(root, map?.file ?? workstreams?.file ?? integration?.file ?? handoff.file), stage: stageName, section: 'Workstream Map' });
|
|
340
336
|
}
|
|
341
337
|
if (!workstreams || !hasActionableWorkstreamFields(workstreams.content)) {
|
|
342
338
|
valid = false;
|
|
343
|
-
addBlocker(blockers, 'incomplete-workstream-split', 'Workstreams must
|
|
339
|
+
addBlocker(blockers, 'incomplete-workstream-split', 'Workstreams must include non-empty structured fields: Workstream ID, Purpose, Required context, Allowed edits, Forbidden edits, Tasks, Validation, Handoff output, and Dependencies.', { path: projectRelative(root, workstreams?.file ?? handoff.file), stage: stageName, section: 'Workstreams' });
|
|
344
340
|
}
|
|
345
341
|
if (!integration || !hasActionableIntegrationFields(integration.content)) {
|
|
346
342
|
valid = false;
|
|
347
|
-
addBlocker(blockers, 'incomplete-workstream-split', 'Integration Sequence must
|
|
343
|
+
addBlocker(blockers, 'incomplete-workstream-split', 'Integration Sequence must include non-empty structured fields: Step, Validation, and Handoff.', { path: projectRelative(root, integration?.file ?? handoff.file), stage: stageName, section: 'Integration Sequence' });
|
|
348
344
|
}
|
|
349
345
|
return valid;
|
|
350
346
|
}
|
|
@@ -358,25 +354,42 @@ function normalizeChecklistCategory(value) {
|
|
|
358
354
|
return normalizeHeading(value).replace(/\s+/g, ' ');
|
|
359
355
|
}
|
|
360
356
|
|
|
361
|
-
function
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
357
|
+
function checklistStatusChecked(value) {
|
|
358
|
+
return /\[[xX]\]/.test(value) || /\bcompleted\b/i.test(value);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function parseChecklistLine(line, lineNumber) {
|
|
362
|
+
const trimmed = line.trim();
|
|
363
|
+
if (!trimmed) return undefined;
|
|
364
|
+
if (/^\|?\s*:?-{3,}:?\s*(?:\|\s*:?-{3,}:?\s*)+\|?$/.test(trimmed)) return undefined;
|
|
365
|
+
|
|
366
|
+
const canonical = /^\s*[-*+]\s*\[([^\]])\]\s*([^:]+):\s*(.*?)\s*$/.exec(line);
|
|
367
|
+
if (canonical) return { line, lineNumber, checked: canonical[1].trim().toLowerCase() === 'x', category: canonical[2].trim(), evidence: canonical[3].trim(), malformed: false };
|
|
368
|
+
|
|
369
|
+
const checkedDash = /^\s*[-*+]\s*\[([^\]])\]\s*(.+?)\s*[—–-]\s*(.*?)\s*$/.exec(line);
|
|
370
|
+
if (checkedDash) return { line, lineNumber, checked: checkedDash[1].trim().toLowerCase() === 'x', category: checkedDash[2].trim(), evidence: checkedDash[3].trim(), malformed: false };
|
|
371
|
+
|
|
372
|
+
const completedPrefix = /^\s*[-*+]\s*completed\s*[—–-]\s*\[([xX])\]\s*(.+?)(?::\s*(.*?))?\s*$/.exec(line);
|
|
373
|
+
if (completedPrefix) return { line, lineNumber, checked: true, category: completedPrefix[2].trim(), evidence: (completedPrefix[3] ?? 'completed').trim(), malformed: false };
|
|
374
|
+
|
|
375
|
+
const table = /^\s*\|(.+)\|\s*$/.exec(line);
|
|
376
|
+
if (table) {
|
|
377
|
+
const cells = table[1].split('|').map((cell) => cell.trim()).filter(Boolean);
|
|
378
|
+
if (cells.length >= 2 && !/^item$/i.test(cells[0]) && !/^category$/i.test(cells[0])) return { line, lineNumber, checked: checklistStatusChecked(cells.slice(1).join(' | ')), category: cells[0], evidence: cells.slice(1).join(' | '), malformed: false };
|
|
379
|
+
return undefined;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const colonStatus = /^\s*([^:|]+):\s*(completed\s*[—–-]\s*\[[xX]\]|\[[xX]\]|completed)(?:\s*[—–:\-]\s*(.*?))?\s*$/.exec(line);
|
|
383
|
+
if (colonStatus) return { line, lineNumber, checked: checklistStatusChecked(colonStatus[2]), category: colonStatus[1].trim(), evidence: (colonStatus[3] ?? colonStatus[2]).trim(), malformed: false };
|
|
384
|
+
|
|
385
|
+
return /^\s*[-*+]\s*/.test(line) ? { line, lineNumber, malformed: true } : undefined;
|
|
365
386
|
}
|
|
366
387
|
|
|
367
388
|
function parseChecklistRows(content) {
|
|
368
|
-
return content
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
line,
|
|
373
|
-
lineNumber: index + 1,
|
|
374
|
-
checked: match[1].trim().toLowerCase() === 'x',
|
|
375
|
-
category: match[2].trim(),
|
|
376
|
-
evidence: match[3].trim(),
|
|
377
|
-
malformed: false,
|
|
378
|
-
};
|
|
379
|
-
}).filter((row) => row.malformed || row.category);
|
|
389
|
+
return content
|
|
390
|
+
.split(/\r?\n/)
|
|
391
|
+
.map((line, index) => parseChecklistLine(line, index + 1))
|
|
392
|
+
.filter((row) => row?.malformed || row?.category);
|
|
380
393
|
}
|
|
381
394
|
|
|
382
395
|
function validateConstructionChecklist(root, files, stageName, blockers) {
|
|
@@ -395,22 +408,22 @@ function validateConstructionChecklist(root, files, stageName, blockers) {
|
|
|
395
408
|
for (const row of rows) {
|
|
396
409
|
if (row.malformed) {
|
|
397
410
|
valid = false;
|
|
398
|
-
addBlocker(blockers, 'malformed-checklist-item', `Malformed construction checklist item at line ${row.lineNumber}: expected "- [x] Category: evidence".`, { path: relFound, stage: stageName, section: header });
|
|
411
|
+
addBlocker(blockers, 'malformed-checklist-item', `Malformed construction checklist item at line ${row.lineNumber}: expected canonical "- [x] Category: evidence" or a completed table/status row with non-empty evidence.`, { path: relFound, stage: stageName, section: header });
|
|
399
412
|
continue;
|
|
400
413
|
}
|
|
401
|
-
if (!row.checked
|
|
414
|
+
if (!row.checked) {
|
|
402
415
|
valid = false;
|
|
403
|
-
addBlocker(blockers, 'open-checklist-item', `Construction checklist item is
|
|
416
|
+
addBlocker(blockers, 'open-checklist-item', `Construction checklist item is unchecked: ${row.category}`, { path: relFound, stage: stageName, section: header });
|
|
404
417
|
}
|
|
405
|
-
if (!contentHasValue(row.evidence)
|
|
418
|
+
if (!contentHasValue(row.evidence)) {
|
|
406
419
|
valid = false;
|
|
407
|
-
addBlocker(blockers, '
|
|
420
|
+
addBlocker(blockers, 'empty-checklist-item', `Construction checklist item has no evidence: ${row.category}`, { path: relFound, stage: stageName, section: header });
|
|
408
421
|
}
|
|
409
422
|
}
|
|
410
423
|
for (const category of requiredCategories) {
|
|
411
424
|
if (!byCategory.has(normalizeChecklistCategory(category))) {
|
|
412
425
|
valid = false;
|
|
413
|
-
addBlocker(blockers, 'missing-checklist-item', `Missing construction checklist item: ${category}
|
|
426
|
+
addBlocker(blockers, 'missing-checklist-item', `Missing construction checklist item: ${category}. Preferred format: "- [x] ${category}: evidence".`, { path: relFound, stage: stageName, section: header });
|
|
414
427
|
}
|
|
415
428
|
}
|
|
416
429
|
return valid;
|
|
@@ -492,57 +505,48 @@ export function validatePlanArtifact(planPath, options = {}) {
|
|
|
492
505
|
const durableFiles = discoverDurablePlanMarkdownFiles(taskDir);
|
|
493
506
|
if (durableFiles.length > 1) summary.splitFiles.detected = true;
|
|
494
507
|
for (const stageName of stagesToValidate) {
|
|
495
|
-
validateInternalStageState(root, taskDir, stageName, blockers);
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
508
|
+
const internal = validateInternalStageState(root, taskDir, stageName, blockers);
|
|
509
|
+
summary.stages.present += internal.state ? 1 : 0;
|
|
510
|
+
let stageValid = internal.valid;
|
|
511
|
+
const files = internal.state ? [internal.state.file] : [];
|
|
512
|
+
const fallbackPath = projectRelative(root, internalStageFile(taskDir, stageName));
|
|
500
513
|
for (const header of PLAN_REQUIRED_HEADERS[stageName]) {
|
|
501
|
-
const found = findSection(files, header);
|
|
514
|
+
const found = files.length > 0 ? findSection(files, header) : undefined;
|
|
502
515
|
if (!found) {
|
|
503
516
|
stageValid = false;
|
|
504
|
-
addBlocker(blockers, 'missing-section', `Missing required section: ## ${header}`, { path:
|
|
517
|
+
addBlocker(blockers, 'missing-section', `Missing required section in internal checkpoint: ## ${header}`, { path: fallbackPath, stage: stageName, section: header });
|
|
505
518
|
continue;
|
|
506
519
|
}
|
|
507
520
|
summary.requiredSections.present += 1;
|
|
508
521
|
const relFound = projectRelative(root, found.file);
|
|
509
522
|
if (!contentHasValue(found.content)) {
|
|
510
523
|
stageValid = false;
|
|
511
|
-
addBlocker(blockers, 'empty-section', `Required section has no content: ## ${header}`, { path: relFound, stage: stageName, section: header });
|
|
512
|
-
continue;
|
|
513
|
-
}
|
|
514
|
-
if (isPlaceholderContent(found.content)) {
|
|
515
|
-
stageValid = false;
|
|
516
|
-
addBlocker(blockers, 'placeholder-section', `Required section only contains placeholder content: ## ${header}`, { path: relFound, stage: stageName, section: header });
|
|
524
|
+
addBlocker(blockers, 'empty-section', `Required checkpoint section has no content: ## ${header}`, { path: relFound, stage: stageName, section: header });
|
|
517
525
|
continue;
|
|
518
526
|
}
|
|
519
527
|
summary.requiredSections.valid += 1;
|
|
520
528
|
|
|
521
529
|
if (header === 'Atomic Tasks') {
|
|
522
|
-
if (!
|
|
530
|
+
if (!sectionContainsField(found.content, ['Acceptance', 'Acceptance criteria'])) {
|
|
523
531
|
stageValid = false;
|
|
524
532
|
addBlocker(blockers, 'missing-atomic-acceptance', 'Atomic Tasks must define acceptance criteria.', { path: relFound, stage: stageName, section: header });
|
|
525
533
|
}
|
|
526
|
-
if (!
|
|
534
|
+
if (!sectionContainsField(found.content, ['Verification'])) {
|
|
527
535
|
stageValid = false;
|
|
528
536
|
addBlocker(blockers, 'missing-atomic-verification', 'Atomic Tasks must define verification.', { path: relFound, stage: stageName, section: header });
|
|
529
537
|
}
|
|
530
538
|
}
|
|
531
|
-
if (header === '
|
|
532
|
-
stageValid = false;
|
|
533
|
-
addBlocker(blockers, 'pending-workstream', 'Required role/area workstream is pending.', { path: relFound, stage: stageName, section: header });
|
|
534
|
-
}
|
|
535
|
-
if (header === 'Discussion Queue' && hasUnresolvedDiscussion(found.content)) {
|
|
539
|
+
if (header === 'Discussion Queue' && hasUnresolvedStructuredItem(found.content)) {
|
|
536
540
|
stageValid = false;
|
|
537
541
|
addBlocker(blockers, 'unresolved-discussion', 'Discussion Queue has unresolved items.', { path: relFound, stage: stageName, section: header });
|
|
538
542
|
}
|
|
539
|
-
if (header === 'Assumptions' &&
|
|
543
|
+
if (header === 'Assumptions' && hasUnresolvedStructuredItem(found.content)) {
|
|
540
544
|
stageValid = false;
|
|
541
545
|
addBlocker(blockers, 'unresolved-assumption', 'Assumptions contain unresolved items.', { path: relFound, stage: stageName, section: header });
|
|
542
546
|
}
|
|
543
547
|
}
|
|
544
|
-
if (!validateWorkstreamHandoffContract(root, files, stageName, blockers)) stageValid = false;
|
|
545
|
-
if (!validateConstructionChecklist(root, files, stageName, blockers)) stageValid = false;
|
|
548
|
+
if (files.length > 0 && !validateWorkstreamHandoffContract(root, files, stageName, blockers)) stageValid = false;
|
|
549
|
+
if (files.length > 0 && !validateConstructionChecklist(root, files, stageName, blockers)) stageValid = false;
|
|
546
550
|
if (stageValid) summary.stages.valid += 1;
|
|
547
551
|
}
|
|
548
552
|
|
package/src/validate/run.mjs
CHANGED
|
@@ -35,6 +35,7 @@ export function runValidate(argv) {
|
|
|
35
35
|
if (path.includes('/node_modules') || path === 'node_modules') return true;
|
|
36
36
|
if (path.includes('/.git') || path === '.git') return true;
|
|
37
37
|
if (path === CACHE_DIR || path.startsWith(`${CACHE_DIR}/`)) return true;
|
|
38
|
+
if (/^docs\/(?:plan|archive\/plan)\/[a-z0-9]+(?:-[a-z0-9]+)*\/\.dotdotgod-plan(?:\/|$)/.test(path)) return true;
|
|
38
39
|
if (!options.includeLocalMemory && (path === 'docs/plan' || path.startsWith('docs/plan/') || path === 'docs/archive' || path.startsWith('docs/archive/'))) return true;
|
|
39
40
|
return false;
|
|
40
41
|
};
|