roadmapsmith 0.7.0 → 0.7.1

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": "roadmapsmith",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "Evidence-backed ROADMAP.md generator and sync tool for AI coding agents.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -12,23 +12,40 @@ const CODE_EXTENSIONS = new Set([
12
12
  '.js', '.cjs', '.mjs', '.ts', '.tsx', '.jsx', '.py', '.go', '.rs', '.java', '.kt', '.swift', '.rb', '.php', '.cs'
13
13
  ]);
14
14
 
15
- const DOC_HINTS = ['readme', 'changelog', 'docs', 'documentation', 'spec', 'diagram', 'runbook'];
15
+ // "docs" omitted from DOC_HINTS it is a path prefix in scan tasks, not a doc-authoring keyword.
16
+ const DOC_HINTS = ['readme', 'changelog', 'documentation', 'spec', 'diagram', 'runbook'];
16
17
  const CODE_HINTS = ['implement', 'add', 'create', 'build', 'refactor', 'fix', 'module', 'function', 'api', 'endpoint', 'command'];
17
18
  const GENERIC_TASK_TOKENS = new Set([
18
- 'implement',
19
- 'implementation',
20
- 'module',
21
- 'function',
22
- 'class',
23
- 'method',
24
- 'command',
25
- 'create',
26
- 'add',
27
- 'build',
28
- 'refactor',
29
- 'fix',
30
- 'test',
31
- 'tests'
19
+ // Action verbs too broad to be evidence signals
20
+ 'implement', 'implementation', 'create', 'add', 'build', 'refactor', 'fix',
21
+ 'detect', 'detection', 'support', 'handle', 'handler', 'update', 'check', 'run',
22
+ 'process', 'processing', 'generate', 'generation', 'format', 'report',
23
+ // Structural concepts shared by every codebase
24
+ 'module', 'function', 'class', 'method', 'command', 'type', 'value', 'values',
25
+ 'output', 'input', 'data',
26
+ // Test vocabulary
27
+ 'test', 'tests',
28
+ // Infrastructure names present in nearly every Node/JS project
29
+ 'config', 'configuration', 'package', 'json', 'project', 'roadmap',
30
+ // Domain words specific to this tool that appear in non-feature source files
31
+ 'confidence', 'profile', 'validation', 'evidence',
32
+ // Package/module field names that appear naturally in any Node.js generator or config file
33
+ 'main', 'exports', 'files', 'fields', 'without', 'field',
34
+ // Terminology used in architecture/detection task descriptions that overlaps with source identifiers
35
+ 'signals', 'directory', 'directories', 'headers', 'site', 'shebang',
36
+ // Common directory names that appear in import paths — too generic for evidence
37
+ 'src', 'lib',
38
+ // Broad task-description verbs and nouns that pollute evidence matching across every codebase
39
+ 'task', 'tasks', 'file', 'source', 'code', 'artifact', 'artifacts',
40
+ 'generic', 'feature', 'features', 'section', 'sections',
41
+ 'user', 'users', 'workflow', 'workflows', 'mode', 'modes', 'replace',
42
+ // Tool-internal vocabulary that appears in non-feature implementation files
43
+ 'audit', 'debug', 'signal', 'signals', 'log',
44
+ // English stopwords and function words that appear everywhere — not useful as evidence signals
45
+ 'only', 'must', 'what', 'which', 'kind', 'never', 'also', 'each',
46
+ 'detected', 'generated', 'existing', 'available',
47
+ // Tool-commentary vocabulary that appears in source comments but describes past/intended behavior
48
+ 'phrases', 'conceptual',
32
49
  ]);
33
50
 
34
51
  const CANONICAL_FILES = {
@@ -38,9 +55,37 @@ const CANONICAL_FILES = {
38
55
  license: 'LICENSE'
39
56
  };
40
57
 
58
+ // The roadmap file must never be included in the evidence pool: its task descriptions
59
+ // contain the exact vocabulary of the tasks being validated, which would cause every
60
+ // task to validate itself.
61
+ const SELF_REFERENTIAL_FILES = new Set(['ROADMAP.md']);
62
+
63
+ // Maps task-ID namespace prefix to a predicate on (normalized) file paths.
64
+ // When a task ID has a known namespace, at least one evidence file must satisfy
65
+ // the predicate — otherwise generic token overlap alone cannot pass the task.
66
+ const NAMESPACE_STRUCTURAL_PATTERNS = {
67
+ cls: (p) => /classif(?:ier|y)|archetype/.test(p),
68
+ dsg: (p) => /generator[/\\](?:domain|web|landing|profiles?)|(?:domain|web|landing)[/\\](?:profile|generator)/.test(p),
69
+ evh2: (p) => p.includes('/validator/') || p.includes('\\validator\\'),
70
+ cst: (p) => /smoke|integration[-_]test|e2e/.test(p),
71
+ uxf: (p) => p.includes('/renderer/') || p.includes('\\renderer\\') || /renderer\.[jt]sx?$/.test(p),
72
+ cfgo: (p) => /config[/\\]|schema[/\\]|config\.[jt]s$|schema\.[jt]s$/.test(p),
73
+ doc3: (p) => /(?:^|[/\\])docs[/\\]|readme\.md$/i.test(p),
74
+ };
75
+
76
+ // Test fixture directories contain synthetic code created to drive test scenarios,
77
+ // not real implementations. Including them pollutes the evidence pool with vocabulary
78
+ // that was deliberately seeded for testing purposes (e.g. namespace-vocab fixtures).
79
+ function isFixturePath(relativePath) {
80
+ return /(?:^|[/\\])fixtures[/\\]/.test(relativePath);
81
+ }
82
+
41
83
  function readFileIndex(projectRoot, files) {
42
84
  const index = [];
43
85
  for (const relativePath of files) {
86
+ if (SELF_REFERENTIAL_FILES.has(relativePath)) continue;
87
+ if (isFixturePath(relativePath)) continue;
88
+
44
89
  const absolutePath = path.resolve(projectRoot, relativePath);
45
90
  const ext = path.extname(relativePath).toLowerCase();
46
91
  let content = '';
@@ -79,10 +124,31 @@ function isLikelyPath(token) {
79
124
  if (/^\.{1,2}\/|^\//.test(token)) return true;
80
125
  if (hasFileExtension(token)) return true;
81
126
  if (KNOWN_PATH_ROOTS.some((root) => token.startsWith(root))) return true;
82
- if ((token.match(/\//g) || []).length >= 2) return true;
127
+ // The ">= 2 slashes" rule was intentionally removed: it caused conceptual slash phrases
128
+ // like "code/test/artifact" or "build/test/deploy" to be treated as file paths.
129
+ // Real multi-segment paths are caught by the extension or known-root rules above.
83
130
  return false;
84
131
  }
85
132
 
133
+ // Matches standalone filenames without a slash — e.g. "roadmap-skill.config.json",
134
+ // "package.json", "vite.config.ts". These are path references whose component tokens
135
+ // (e.g. "roadmap", "skill") must be excluded from code evidence scoring to prevent
136
+ // circular vocabulary: a task mentioning a filename would otherwise score hits in any
137
+ // source file that happens to reference the same filename for unrelated reasons.
138
+ // Numeric-only tokens like "1.0.0" or "v0.8" are excluded via the leading-digit guard.
139
+ const STANDALONE_FILE_RE = /\b([A-Za-z][A-Za-z0-9_.+-]*\.[A-Za-z0-9]{2,10})\b/g;
140
+ const KNOWN_FILE_EXTENSIONS = new Set([
141
+ '.js', '.cjs', '.mjs', '.ts', '.tsx', '.jsx', '.py', '.go', '.rs',
142
+ '.java', '.kt', '.swift', '.rb', '.php', '.cs', '.json', '.yaml', '.yml',
143
+ '.toml', '.md', '.txt', '.sh', '.bash', '.env', '.html', '.css', '.scss', '.lock'
144
+ ]);
145
+
146
+ function hasKnownFileExtension(token) {
147
+ const lastDot = token.lastIndexOf('.');
148
+ if (lastDot < 0) return false;
149
+ return KNOWN_FILE_EXTENSIONS.has(token.slice(lastDot).toLowerCase());
150
+ }
151
+
86
152
  function extractExplicitPaths(text) {
87
153
  const results = new Set();
88
154
  const quoted = String(text).match(/`([^`]+)`/g) || [];
@@ -102,6 +168,25 @@ function extractExplicitPaths(text) {
102
168
  return Array.from(results).sort((left, right) => left.localeCompare(right));
103
169
  }
104
170
 
171
+ // Standalone filenames (no slash) mentioned in task prose — e.g. "roadmap-skill.config.json",
172
+ // "package.json". These are filename *references*, NOT path-existence assertions: the author
173
+ // is describing which file contains a feature, not asserting that the file must exist.
174
+ // Used only for pathDerivedToken extraction (to prevent circular vocabulary), never for
175
+ // findFilesByPathHints (which would pass any task whose config file already exists).
176
+ function extractStandaloneFilenames(text) {
177
+ const results = new Set();
178
+ STANDALONE_FILE_RE.lastIndex = 0;
179
+ let m = STANDALONE_FILE_RE.exec(String(text));
180
+ while (m) {
181
+ const token = m[1].replace(/[.,;:!?)]+$/, '');
182
+ if (hasKnownFileExtension(token) && !token.startsWith('.')) {
183
+ results.add(token);
184
+ }
185
+ m = STANDALONE_FILE_RE.exec(String(text));
186
+ }
187
+ return Array.from(results);
188
+ }
189
+
105
190
  function extractSymbolHints(text) {
106
191
  const symbols = new Set();
107
192
  const patterns = [
@@ -128,7 +213,12 @@ function isCodeTask(taskText) {
128
213
 
129
214
  function isDocTask(taskText) {
130
215
  const normalized = String(taskText).toLowerCase();
131
- return DOC_HINTS.some((hint) => normalized.includes(hint));
216
+ // Use word-boundary matching to avoid substring false positives (e.g. "specific" ≠ "spec").
217
+ const hasDocKeyword = DOC_HINTS.some((hint) => new RegExp(`(?<![a-z])${hint}(?![a-z])`).test(normalized));
218
+ if (!hasDocKeyword) return false;
219
+ // Also require a creation/update verb so that policy tasks mentioning doc files
220
+ // ("README must not be used as evidence") don't trigger doc-artifact evidence.
221
+ return /\b(add|create|write|update|init|initialize|introduce|setup|document)\b/.test(normalized);
132
222
  }
133
223
 
134
224
  function findFilesByPathHints(pathHints, fileIndex) {
@@ -166,9 +256,31 @@ function findFilesBySymbols(symbolHints, fileIndex) {
166
256
  return Array.from(matches).sort((left, right) => left.localeCompare(right));
167
257
  }
168
258
 
169
- function findCodeEvidence(taskText, fileIndex) {
259
+ // Tokens extracted from a referenced file path (e.g. "roadmap-skill" from
260
+ // "roadmap-skill.config.json") must not be reused as code evidence signals.
261
+ // Those tokens appear in any file that mentions the same path — creating circular
262
+ // vocabulary where a task about "X in path/to/file" passes because the source
263
+ // code references the same path for unrelated reasons.
264
+ function extractPathDerivedTokens(pathHints) {
265
+ const tokens = new Set();
266
+ for (const hint of pathHints) {
267
+ // Char-split: "roadmap-skill.config.json" → ["roadmap", "skill", "config", "json"]
268
+ const parts = hint.replace(/[.\-_/\\]/g, ' ').toLowerCase().split(/\s+/).filter(Boolean);
269
+ for (const part of parts) {
270
+ if (part.length >= 3) tokens.add(part);
271
+ }
272
+ // Tokenizer-split: also adds compound tokens the char-split misses, e.g. "roadmap-skill"
273
+ // (the tokenizer preserves hyphens in identifiers; the char-split strips them).
274
+ for (const token of tokenize(hint)) {
275
+ if (token.length >= 3) tokens.add(token);
276
+ }
277
+ }
278
+ return tokens;
279
+ }
280
+
281
+ function findCodeEvidence(taskText, fileIndex, pathDerivedTokens = new Set()) {
170
282
  const tokens = tokenize(taskText)
171
- .filter((token) => token.length >= 3 && !GENERIC_TASK_TOKENS.has(token))
283
+ .filter((token) => token.length >= 3 && !GENERIC_TASK_TOKENS.has(token) && !token.endsWith('/') && !pathDerivedTokens.has(token))
172
284
  .slice(0, 8);
173
285
  if (tokens.length === 0) {
174
286
  return [];
@@ -191,7 +303,9 @@ function findCodeEvidence(taskText, fileIndex) {
191
303
  }
192
304
  }
193
305
 
194
- const threshold = tokens.length === 1 ? 1 : 2;
306
+ // Require more matches proportional to how many specific tokens the task has.
307
+ // Tasks with 4+ meaningful tokens need 3 files to match to prevent vocabulary overlap.
308
+ const threshold = tokens.length >= 4 ? 3 : tokens.length >= 2 ? 2 : 1;
195
309
  if (score >= threshold) {
196
310
  matches.push(file.relativePath);
197
311
  }
@@ -202,18 +316,46 @@ function findCodeEvidence(taskText, fileIndex) {
202
316
 
203
317
  function findTestEvidence(taskText, fileIndex) {
204
318
  const tokens = tokenize(taskText)
205
- .filter((token) => token.length >= 3 && !GENERIC_TASK_TOKENS.has(token))
319
+ .filter((token) => token.length >= 3 && !GENERIC_TASK_TOKENS.has(token) && !token.endsWith('/'))
206
320
  .slice(0, 8);
321
+
322
+ if (tokens.length === 0) return [];
323
+
324
+ // Only tokens of length >= 4 are used for import-reference matching.
325
+ // Very short tokens (e.g. "app", "web") are too generic: they appear as substrings in
326
+ // many import paths that have nothing to do with the feature being validated.
327
+ // The single-short-token fallback below handles the narrow case of one-word module names.
328
+ const importTokens = tokens.filter((token) => token.length >= 4);
329
+
207
330
  const matches = [];
208
331
 
209
332
  for (const file of fileIndex) {
210
- if (!file.isTestFile) {
333
+ if (!file.isTestFile) continue;
334
+
335
+ // A test file counts as evidence only when it imports a module whose path contains
336
+ // one of the task's meaningful tokens. Content-keyword matching is intentionally absent:
337
+ // test content (descriptions, literals) can contain future-task vocabulary,
338
+ // producing self-referential false positives.
339
+ //
340
+ // Trailing slashes are NOT stripped: "app/" is a directory reference, not a module name.
341
+ // "../src/app" (a real import) does not contain the string "app/" so it won't match.
342
+ const importRefs = (
343
+ file.content.match(/require\s*\(\s*['"`]([^'"`]+)['"`]\s*\)|from\s+['"`]([^'"`]+)['"`]/g) || []
344
+ ).join(' ').toLowerCase();
345
+
346
+ if (importTokens.length > 0 && importTokens.some((token) => importRefs.includes(token))) {
347
+ matches.push(file.relativePath);
211
348
  continue;
212
349
  }
213
- const lowered = file.content.toLowerCase();
214
- const hasMatch = tokens.some((token) => lowered.includes(token));
215
- if (hasMatch) {
216
- matches.push(file.relativePath);
350
+
351
+ // Narrow fallback: single very-short token (e.g. "app", "cli").
352
+ // Import paths for these are too short to distinguish reliably, so fall back to a
353
+ // content match — but only when there is exactly one such token (no multi-token dilution).
354
+ if (tokens.length === 1 && tokens[0].length < 4) {
355
+ const lowered = file.content.toLowerCase();
356
+ if (lowered.includes(tokens[0])) {
357
+ matches.push(file.relativePath);
358
+ }
217
359
  }
218
360
  }
219
361
 
@@ -225,19 +367,26 @@ function findArtifactEvidence(taskText, fileIndex) {
225
367
  const files = [];
226
368
  const heuristicArtifacts = [];
227
369
 
228
- for (const [keyword, filename] of Object.entries(CANONICAL_FILES)) {
229
- if (normalized.includes(keyword)) {
230
- const hit = fileIndex.find(
231
- (f) => f.relativePath === filename || f.relativePath.endsWith('/' + filename)
232
- );
233
- if (hit) {
234
- files.push(hit.relativePath);
235
- heuristicArtifacts.push(hit.relativePath);
370
+ // Canonical file detection only applies to short tasks (≤8 words) that are about
371
+ // creating or referencing that specific file. Long sentences that merely MENTION
372
+ // "readme" or "security" in a policy/constraint context are excluded.
373
+ const wordCount = normalized.trim().split(/\s+/).length;
374
+ if (wordCount <= 8) {
375
+ for (const [keyword, filename] of Object.entries(CANONICAL_FILES)) {
376
+ // Use hyphen-aware word boundaries: "security-headers" must not match "security".
377
+ if (new RegExp(`(?<![a-z-])${keyword}(?![a-z-])`).test(normalized)) {
378
+ const hit = fileIndex.find(
379
+ (f) => f.relativePath === filename || f.relativePath.endsWith('/' + filename)
380
+ );
381
+ if (hit) {
382
+ files.push(hit.relativePath);
383
+ heuristicArtifacts.push(hit.relativePath);
384
+ }
236
385
  }
237
386
  }
238
387
  }
239
388
 
240
- if (!isDocTask(taskText) && !normalized.includes('artifact') && !normalized.includes('release')) {
389
+ if (!isDocTask(taskText)) {
241
390
  return { files, heuristicArtifacts };
242
391
  }
243
392
 
@@ -259,6 +408,82 @@ function findArtifactEvidence(taskText, fileIndex) {
259
408
  return { files: files.slice(0, 20), heuristicArtifacts };
260
409
  }
261
410
 
411
+ function extractTaskNamespace(taskId) {
412
+ if (!taskId) return null;
413
+ const match = String(taskId).match(/^([a-z][a-z0-9]*)-/);
414
+ return match ? match[1] : null;
415
+ }
416
+
417
+ function isAcceptanceCriteria(taskId) {
418
+ return /ph\d+[_-]st\d+[_-]exit/.test(String(taskId || ''));
419
+ }
420
+
421
+ // Gate: returns { applicable, passed, structuralFiles, reason }.
422
+ // For namespaces with a defined structural pattern:
423
+ // 1. If no files in fileIndex match the pattern → immediate fail.
424
+ // 2. For acceptance-criteria tasks (phN-stN-exit IDs): path match alone is enough.
425
+ // 3. For implementation tasks: feature tokens from task text must score ≥ ceil(n/2)
426
+ // against namespace-matched files, preventing vocabulary overlap from generic
427
+ // infrastructure code (io.js, generator/index.js) from serving as evidence.
428
+ function checkNamespaceStructuralEvidence(taskId, taskText, fileIndex) {
429
+ const namespace = extractTaskNamespace(taskId);
430
+ if (!namespace || !NAMESPACE_STRUCTURAL_PATTERNS[namespace]) {
431
+ return { applicable: false, passed: true, structuralFiles: [], reason: null };
432
+ }
433
+
434
+ const predicate = NAMESPACE_STRUCTURAL_PATTERNS[namespace];
435
+ const namespaceFiles = fileIndex.filter((f) => predicate(f.relativePath));
436
+
437
+ if (namespaceFiles.length === 0) {
438
+ return {
439
+ applicable: true,
440
+ passed: false,
441
+ structuralFiles: [],
442
+ reason: `namespace "${namespace}" has no implementation files`,
443
+ };
444
+ }
445
+
446
+ const featureTokens = tokenize(taskText)
447
+ .filter((t) => t.length >= 4 && !GENERIC_TASK_TOKENS.has(t) && !t.endsWith('/'))
448
+ .slice(0, 8);
449
+
450
+ if (featureTokens.length === 0) {
451
+ return {
452
+ applicable: true,
453
+ passed: true,
454
+ structuralFiles: namespaceFiles.map((f) => f.relativePath),
455
+ reason: null,
456
+ };
457
+ }
458
+
459
+ let bestScore = 0;
460
+ for (const nsFile of namespaceFiles) {
461
+ const lowered = nsFile.content.toLowerCase();
462
+ let score = 0;
463
+ for (const token of featureTokens) {
464
+ if (lowered.includes(token)) score++;
465
+ }
466
+ if (score > bestScore) bestScore = score;
467
+ }
468
+
469
+ const threshold = Math.max(1, Math.ceil(featureTokens.length / 2));
470
+ if (bestScore >= threshold) {
471
+ return {
472
+ applicable: true,
473
+ passed: true,
474
+ structuralFiles: namespaceFiles.map((f) => f.relativePath),
475
+ reason: null,
476
+ };
477
+ }
478
+
479
+ return {
480
+ applicable: true,
481
+ passed: false,
482
+ structuralFiles: namespaceFiles.map((f) => f.relativePath),
483
+ reason: `structural token score ${bestScore}/${threshold} in "${namespace}" files — token overlap insufficient`,
484
+ };
485
+ }
486
+
262
487
  function evaluateRule(rule, task, context) {
263
488
  if (!rule) {
264
489
  return { passed: true, reasons: [], evidence: {} };
@@ -342,14 +567,21 @@ function buildValidationContext(projectRoot, config, plugins) {
342
567
 
343
568
  function validateTask(task, context, config, plugins) {
344
569
  const pathHints = extractExplicitPaths(task.text);
570
+ const standaloneFilenames = extractStandaloneFilenames(task.text);
345
571
  const symbolHints = extractSymbolHints(task.text);
346
572
 
347
573
  const filesFromPaths = findFilesByPathHints(pathHints, context.fileIndex);
348
574
  const filesFromSymbols = findFilesBySymbols(symbolHints, context.fileIndex);
349
- const filesFromCode = findCodeEvidence(task.text, context.fileIndex);
575
+ // Combine path hints AND standalone filenames for token exclusion so that tokens
576
+ // derived from any referenced filename (e.g. "roadmap-skill" from
577
+ // "roadmap-skill.config.json") are excluded from code evidence scoring.
578
+ const pathDerivedTokens = extractPathDerivedTokens([...pathHints, ...standaloneFilenames]);
579
+ const filesFromCode = findCodeEvidence(task.text, context.fileIndex, pathDerivedTokens);
350
580
  const filesFromTests = findTestEvidence(task.text, context.fileIndex);
351
581
  const { files: filesFromArtifacts, heuristicArtifacts } = findArtifactEvidence(task.text, context.fileIndex);
352
582
 
583
+ const structuralCheck = checkNamespaceStructuralEvidence(task.id, task.text, context.fileIndex);
584
+
353
585
  const evidence = {
354
586
  code: filesFromCode.length > 0 || filesFromSymbols.length > 0,
355
587
  test: filesFromTests.length > 0,
@@ -359,7 +591,9 @@ function validateTask(task, context, config, plugins) {
359
591
  codeFiles: filesFromCode,
360
592
  testFiles: filesFromTests,
361
593
  artifactFiles: filesFromArtifacts,
362
- heuristicArtifacts
594
+ heuristicArtifacts,
595
+ structuralEvidence: structuralCheck.applicable ? structuralCheck.passed : null,
596
+ structuralFiles: structuralCheck.structuralFiles,
363
597
  };
364
598
 
365
599
  const reasons = [];
@@ -370,8 +604,16 @@ function validateTask(task, context, config, plugins) {
370
604
  reasons.push(`missing symbol(s): ${symbolHints.join(', ')}`);
371
605
  }
372
606
 
607
+ // Namespace-structural gate: for known namespaces, token overlap alone is insufficient.
608
+ // The task must have evidence files whose paths match the namespace pattern.
609
+ if (structuralCheck.applicable && !structuralCheck.passed) {
610
+ reasons.push(structuralCheck.reason || `no structural evidence for namespace "${extractTaskNamespace(task.id)}"`);
611
+ }
612
+
373
613
  const hasEvidence = evidence.code || evidence.test || evidence.artifact || evidence.files.length > 0;
374
- if (!hasEvidence) {
614
+ if (!hasEvidence && !structuralCheck.applicable) {
615
+ reasons.push('no code, test, or artifact evidence found');
616
+ } else if (!hasEvidence && structuralCheck.applicable && structuralCheck.passed) {
375
617
  reasons.push('no code, test, or artifact evidence found');
376
618
  }
377
619
 
@@ -395,12 +637,17 @@ function validateTask(task, context, config, plugins) {
395
637
  const evidenceCount = [evidence.code, evidence.test, evidence.artifact].filter(Boolean).length;
396
638
  const confidence = evidenceCount >= 2 ? 'high' : evidenceCount === 1 ? 'medium' : 'low';
397
639
 
640
+ // True when the only passing evidence is artifact/doc files and the task is not a doc task.
641
+ // Used by auditValidation to flag implementation tasks that pass solely via documentation.
642
+ const evidenceIsDocOnly = !evidence.code && !evidence.test && evidence.artifact && !isDocTask(task.text);
643
+
398
644
  return {
399
645
  taskId: task.id,
400
646
  passed: uniqueReasons.length === 0,
401
647
  confidence,
402
648
  reasons: uniqueReasons,
403
649
  evidence,
650
+ evidenceIsDocOnly,
404
651
  requiresTest,
405
652
  hasEvidence,
406
653
  attempted
@@ -418,12 +665,13 @@ function validateTasks(tasks, context, config, plugins) {
418
665
  function auditValidation(tasks, results) {
419
666
  const checkedWithoutEvidence = [];
420
667
  const readyButUnchecked = [];
668
+ const checkedWithWeakEvidence = [];
669
+ const documentationOnlyEvidenceForImplementation = [];
670
+ const checkedWithNoStructuralEvidence = [];
421
671
 
422
672
  for (const task of tasks) {
423
673
  const result = results[task.id];
424
- if (!result) {
425
- continue;
426
- }
674
+ if (!result) continue;
427
675
 
428
676
  if (task.checked && !result.passed) {
429
677
  checkedWithoutEvidence.push({ task, result });
@@ -432,11 +680,27 @@ function auditValidation(tasks, results) {
432
680
  if (!task.checked && result.passed) {
433
681
  readyButUnchecked.push({ task, result });
434
682
  }
683
+
684
+ if (task.checked && result.passed && result.confidence === 'low') {
685
+ checkedWithWeakEvidence.push({ task, result });
686
+ }
687
+
688
+ if (task.checked && result.passed && result.evidenceIsDocOnly) {
689
+ documentationOnlyEvidenceForImplementation.push({ task, result });
690
+ }
691
+
692
+ // Checked task that failed specifically because structural evidence is missing.
693
+ if (task.checked && !result.passed && result.evidence.structuralEvidence === false) {
694
+ checkedWithNoStructuralEvidence.push({ task, result });
695
+ }
435
696
  }
436
697
 
437
698
  return {
438
699
  checkedWithoutEvidence,
439
- readyButUnchecked
700
+ readyButUnchecked,
701
+ checkedWithWeakEvidence,
702
+ documentationOnlyEvidenceForImplementation,
703
+ checkedWithNoStructuralEvidence,
440
704
  };
441
705
  }
442
706
 
@@ -460,5 +724,7 @@ module.exports = {
460
724
  validateTask,
461
725
  validateTasks,
462
726
  CONFIDENCE_RANK,
463
- applyMinimumConfidence
727
+ applyMinimumConfidence,
728
+ extractTaskNamespace,
729
+ isAcceptanceCriteria,
464
730
  };