@dotdotgod/cli 0.1.44 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotdotgod/cli",
3
- "version": "0.1.44",
3
+ "version": "0.1.45",
4
4
  "description": "CLI for dotdotgod validation, project memory snapshots, and graph indexing.",
5
5
  "type": "module",
6
6
  "license": "Elastic-2.0",
@@ -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 })
@@ -264,33 +256,21 @@ function validateMarkdownNames(root, stageName, stageDir, blockers) {
264
256
  }
265
257
  }
266
258
 
267
- function sectionContains(content, words) {
268
- const normalized = content.toLowerCase();
269
- return words.some((word) => normalized.includes(word));
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));
270
262
  }
271
263
 
272
- function hasPendingRequiredWorkstream(content) {
273
- return content.split(/\r?\n/).some((line) => {
274
- const normalized = line.toLowerCase();
275
- if (!normalized.includes('pending')) return false;
276
- return /\byes\b|\brequired\b/.test(normalized);
277
- });
264
+ function hasUncheckedChecklistItem(content) {
265
+ return content.split(/\r?\n/).some((line) => /^\s*[-*+]\s*\[\s\]/.test(line));
278
266
  }
279
267
 
280
- function hasUnresolvedDiscussion(content) {
281
- return content.split(/\r?\n/).some((line) => {
282
- const normalized = line.toLowerCase();
283
- if (/^\s*-\s*\[\s\]/.test(line)) return true;
284
- return /status\s*:\s*(open|research[-_ ]requested|plan[-_ ]revision[-_ ]requested|unresolved|pending)/.test(normalized);
285
- });
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));
286
270
  }
287
271
 
288
- function hasUnresolvedAssumption(content) {
289
- return content.split(/\r?\n/).some((line) => {
290
- const normalized = line.toLowerCase();
291
- if (/^\s*-\s*\[\s\]/.test(line)) return true;
292
- return /\b(unresolved|pending|unknown|needs user|needs confirmation)\b/.test(normalized);
293
- });
272
+ function hasUnresolvedStructuredItem(content) {
273
+ return hasUncheckedChecklistItem(content) || hasOpenStructuredStatus(content);
294
274
  }
295
275
 
296
276
  function workstreamSplitDecision(content) {
@@ -298,36 +278,32 @@ function workstreamSplitDecision(content) {
298
278
  return match?.[1].toLowerCase();
299
279
  }
300
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
+
301
293
  function hasNoSplitRationale(content) {
302
- return /\b(?:rationale|reason|because)\b/i.test(content);
294
+ return fieldHasValue(content, 'No-split rationale');
303
295
  }
304
296
 
305
297
  function hasMultipleWorkstreams(content) {
306
- const normalized = removeCodeBlocks(content).toLowerCase();
307
- const explicitIds = new Set([...normalized.matchAll(/\b(?:workstream|ws)\s*[-:#]?\s*([a-z0-9]+)\b/g)].map((match) => match[1]));
308
- if (explicitIds.size >= 2) return true;
309
- const workstreamMentions = normalized.match(/\bworkstreams?\b/g) ?? [];
310
- return workstreamMentions.length >= 2 && /\b(?:multiple|parallel|split)\b/.test(normalized);
298
+ return fieldOccurrences(content, 'Workstream ID') >= 2;
311
299
  }
312
300
 
313
301
  function hasActionableWorkstreamFields(content) {
314
- const normalized = content.toLowerCase();
315
- const fieldGroups = [
316
- ['purpose', 'objective'],
317
- ['required context', 'read'],
318
- ['allowed edit'],
319
- ['forbidden edit', 'do not edit', 'do-not edit'],
320
- ['task'],
321
- ['validation', 'verify'],
322
- ['handoff output', 'output'],
323
- ['dependenc'],
324
- ];
325
- 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));
326
303
  }
327
304
 
328
305
  function hasActionableIntegrationFields(content) {
329
- const normalized = content.toLowerCase();
330
- 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));
331
307
  }
332
308
 
333
309
  function validateWorkstreamHandoffContract(root, files, stageName, blockers) {
@@ -344,9 +320,9 @@ function validateWorkstreamHandoffContract(root, files, stageName, blockers) {
344
320
  if (decision === 'no') {
345
321
  if (!hasNoSplitRationale(handoff.content)) {
346
322
  valid = false;
347
- addBlocker(blockers, 'missing-workstream-no-split-rationale', 'Workstream Handoff with `Split decision: no` must include a rationale.', { path: relHandoff, stage: stageName, section: 'Workstream Handoff' });
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' });
348
324
  }
349
- if (!todo || !contentHasValue(todo.content) || isPlaceholderContent(todo.content)) valid = false;
325
+ if (!todo || !contentHasValue(todo.content)) valid = false;
350
326
  return valid;
351
327
  }
352
328
 
@@ -356,15 +332,15 @@ function validateWorkstreamHandoffContract(root, files, stageName, blockers) {
356
332
  const splitContent = [map?.content, workstreams?.content, integration?.content].filter(Boolean).join('\n');
357
333
  if (!hasMultipleWorkstreams(splitContent)) {
358
334
  valid = false;
359
- addBlocker(blockers, 'incomplete-workstream-split', 'Split Stage 05 handoff must indicate multiple workstreams in Workstream Map, Workstreams, or Integration Sequence.', { path: projectRelative(root, map?.file ?? workstreams?.file ?? integration?.file ?? handoff.file), stage: stageName, section: 'Workstream Map' });
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' });
360
336
  }
361
337
  if (!workstreams || !hasActionableWorkstreamFields(workstreams.content)) {
362
338
  valid = false;
363
- addBlocker(blockers, 'incomplete-workstream-split', 'Workstreams must mention purpose/objective, required context/read, allowed edits, forbidden edits/do-not-edit, tasks, validation, handoff output, and dependencies.', { path: projectRelative(root, workstreams?.file ?? handoff.file), stage: stageName, section: 'Workstreams' });
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' });
364
340
  }
365
341
  if (!integration || !hasActionableIntegrationFields(integration.content)) {
366
342
  valid = false;
367
- addBlocker(blockers, 'incomplete-workstream-split', 'Integration Sequence must describe actionable integration sequence and validation/handoff expectations.', { path: projectRelative(root, integration?.file ?? handoff.file), stage: stageName, section: 'Integration Sequence' });
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' });
368
344
  }
369
345
  return valid;
370
346
  }
@@ -378,25 +354,42 @@ function normalizeChecklistCategory(value) {
378
354
  return normalizeHeading(value).replace(/\s+/g, ' ');
379
355
  }
380
356
 
381
- function hasOpenChecklistEvidence(evidence) {
382
- const normalized = evidence.toLowerCase();
383
- if (/\bno\s+(?:open|unresolved|pending|unknown)\b/.test(normalized)) return false;
384
- return CHECKLIST_OPEN_RE.test(normalized);
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;
385
386
  }
386
387
 
387
388
  function parseChecklistRows(content) {
388
- return content.split(/\r?\n/).map((line, index) => {
389
- const match = /^\s*[-*+]\s*\[([^\]])\]\s*([^:]+):\s*(.*?)\s*$/.exec(line);
390
- if (!match) return { line, lineNumber: index + 1, malformed: /^\s*[-*+]\s*/.test(line) && line.trim().length > 0 };
391
- return {
392
- line,
393
- lineNumber: index + 1,
394
- checked: match[1].trim().toLowerCase() === 'x',
395
- category: match[2].trim(),
396
- evidence: match[3].trim(),
397
- malformed: false,
398
- };
399
- }).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);
400
393
  }
401
394
 
402
395
  function validateConstructionChecklist(root, files, stageName, blockers) {
@@ -415,22 +408,22 @@ function validateConstructionChecklist(root, files, stageName, blockers) {
415
408
  for (const row of rows) {
416
409
  if (row.malformed) {
417
410
  valid = false;
418
- 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 });
419
412
  continue;
420
413
  }
421
- if (!row.checked || hasOpenChecklistEvidence(row.evidence)) {
414
+ if (!row.checked) {
422
415
  valid = false;
423
- addBlocker(blockers, 'open-checklist-item', `Construction checklist item is open: ${row.category}`, { path: relFound, stage: stageName, section: header });
416
+ addBlocker(blockers, 'open-checklist-item', `Construction checklist item is unchecked: ${row.category}`, { path: relFound, stage: stageName, section: header });
424
417
  }
425
- if (!contentHasValue(row.evidence) || isPlaceholderContent(row.evidence)) {
418
+ if (!contentHasValue(row.evidence)) {
426
419
  valid = false;
427
- addBlocker(blockers, 'placeholder-checklist-item', `Construction checklist item only contains placeholder content: ${row.category}`, { path: relFound, stage: stageName, section: header });
420
+ addBlocker(blockers, 'empty-checklist-item', `Construction checklist item has no evidence: ${row.category}`, { path: relFound, stage: stageName, section: header });
428
421
  }
429
422
  }
430
423
  for (const category of requiredCategories) {
431
424
  if (!byCategory.has(normalizeChecklistCategory(category))) {
432
425
  valid = false;
433
- addBlocker(blockers, 'missing-checklist-item', `Missing construction checklist item: ${category}`, { path: relFound, stage: stageName, section: header });
426
+ addBlocker(blockers, 'missing-checklist-item', `Missing construction checklist item: ${category}. Preferred format: "- [x] ${category}: evidence".`, { path: relFound, stage: stageName, section: header });
434
427
  }
435
428
  }
436
429
  return valid;
@@ -531,32 +524,23 @@ export function validatePlanArtifact(planPath, options = {}) {
531
524
  addBlocker(blockers, 'empty-section', `Required checkpoint section has no content: ## ${header}`, { path: relFound, stage: stageName, section: header });
532
525
  continue;
533
526
  }
534
- if (isPlaceholderContent(found.content)) {
535
- stageValid = false;
536
- addBlocker(blockers, 'placeholder-section', `Required checkpoint section only contains placeholder content: ## ${header}`, { path: relFound, stage: stageName, section: header });
537
- continue;
538
- }
539
527
  summary.requiredSections.valid += 1;
540
528
 
541
529
  if (header === 'Atomic Tasks') {
542
- if (!sectionContains(found.content, ['acceptance'])) {
530
+ if (!sectionContainsField(found.content, ['Acceptance', 'Acceptance criteria'])) {
543
531
  stageValid = false;
544
532
  addBlocker(blockers, 'missing-atomic-acceptance', 'Atomic Tasks must define acceptance criteria.', { path: relFound, stage: stageName, section: header });
545
533
  }
546
- if (!sectionContains(found.content, ['verification', 'verify'])) {
534
+ if (!sectionContainsField(found.content, ['Verification'])) {
547
535
  stageValid = false;
548
536
  addBlocker(blockers, 'missing-atomic-verification', 'Atomic Tasks must define verification.', { path: relFound, stage: stageName, section: header });
549
537
  }
550
538
  }
551
- if (header === 'Role / Area Workstreams' && hasPendingRequiredWorkstream(found.content)) {
552
- stageValid = false;
553
- addBlocker(blockers, 'pending-workstream', 'Required role/area workstream is pending.', { path: relFound, stage: stageName, section: header });
554
- }
555
- if (header === 'Discussion Queue' && hasUnresolvedDiscussion(found.content)) {
539
+ if (header === 'Discussion Queue' && hasUnresolvedStructuredItem(found.content)) {
556
540
  stageValid = false;
557
541
  addBlocker(blockers, 'unresolved-discussion', 'Discussion Queue has unresolved items.', { path: relFound, stage: stageName, section: header });
558
542
  }
559
- if (header === 'Assumptions' && hasUnresolvedAssumption(found.content)) {
543
+ if (header === 'Assumptions' && hasUnresolvedStructuredItem(found.content)) {
560
544
  stageValid = false;
561
545
  addBlocker(blockers, 'unresolved-assumption', 'Assumptions contain unresolved items.', { path: relFound, stage: stageName, section: header });
562
546
  }