roadmapsmith 0.9.29 → 0.9.31

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.
@@ -14,6 +14,7 @@ const CODE_EXTENSIONS = new Set([
14
14
  const TRANSLATION_DIR_SEGMENTS = ['locale', 'locales', 'i18n', 'translations'];
15
15
  const DEFAULT_EXCLUDED_PATH_PREFIXES = ['.claude/', '.agent/', 'roadmap-skill/'];
16
16
  const GENERATED_OUTPUT_PREFIXES = ['dist-electron/', 'dist/', 'build/', 'out/', '.next/', 'coverage/'];
17
+ const AUXILIARY_HEURISTIC_PATH_SEGMENTS = new Set(['scripts', 'tools', 'tooling', 'demo', 'demos']);
17
18
 
18
19
  // "docs" omitted from DOC_HINTS — it is a path prefix in scan tasks, not a doc-authoring keyword.
19
20
  const DOC_HINTS = ['readme', 'changelog', 'documentation', 'spec', 'diagram', 'runbook'];
@@ -157,6 +158,88 @@ function isGeneratedOutputPath(relativePath) {
157
158
  return GENERATED_OUTPUT_PREFIXES.some((prefix) => normalized.startsWith(prefix));
158
159
  }
159
160
 
161
+ function splitNormalizedPathSegments(relativePath) {
162
+ return normalizePathForMatch(relativePath).split('/').filter(Boolean);
163
+ }
164
+
165
+ function isAuxiliaryHeuristicPath(relativePath) {
166
+ return splitNormalizedPathSegments(relativePath).some((segment) => AUXILIARY_HEURISTIC_PATH_SEGMENTS.has(segment));
167
+ }
168
+
169
+ function relativePathExplicitlyReferenced(relativePath, options = {}) {
170
+ const normalized = normalizeReferencedPath(relativePath);
171
+ const explicitPaths = Array.isArray(options.explicitPaths) ? options.explicitPaths : [];
172
+ const explicitFilenames = Array.isArray(options.explicitFilenames) ? options.explicitFilenames : [];
173
+ if (explicitPaths.some((entry) => referencedPathMatches(normalized, entry))) {
174
+ return true;
175
+ }
176
+
177
+ const baseName = path.basename(normalized);
178
+ return explicitFilenames.some((entry) => normalizeReferencedPath(entry) === baseName);
179
+ }
180
+
181
+ function shouldSkipHeuristicFile(relativePath, options = {}) {
182
+ return isAuxiliaryHeuristicPath(relativePath) && !relativePathExplicitlyReferenced(relativePath, options);
183
+ }
184
+
185
+ function authoredSiblingGroupKey(relativePath) {
186
+ const normalized = normalizePathForMatch(relativePath);
187
+ const ext = path.extname(normalized);
188
+ return ext ? normalized.slice(0, -ext.length) : normalized;
189
+ }
190
+
191
+ function authoredSiblingExtensionRank(relativePath) {
192
+ const ext = path.extname(String(relativePath || '')).toLowerCase();
193
+ switch (ext) {
194
+ case '.ts':
195
+ return 0;
196
+ case '.tsx':
197
+ return 1;
198
+ case '.jsx':
199
+ return 2;
200
+ case '.js':
201
+ return 3;
202
+ case '.mjs':
203
+ return 4;
204
+ case '.cjs':
205
+ return 5;
206
+ case '.py':
207
+ return 6;
208
+ default:
209
+ return 10;
210
+ }
211
+ }
212
+
213
+ function dedupeAuthoredCompiledSiblings(relativePaths, fileIndex) {
214
+ const indexedPaths = new Set(fileIndex.map((file) => file.relativePath));
215
+ const groups = new Map();
216
+ for (const relativePath of Array.from(new Set(relativePaths)).filter(Boolean)) {
217
+ if (!indexedPaths.has(relativePath)) {
218
+ continue;
219
+ }
220
+ const key = authoredSiblingGroupKey(relativePath);
221
+ const group = groups.get(key) || [];
222
+ group.push(relativePath);
223
+ groups.set(key, group);
224
+ }
225
+
226
+ const selected = [];
227
+ for (const group of groups.values()) {
228
+ group.sort((left, right) => {
229
+ const rankDelta = authoredSiblingExtensionRank(left) - authoredSiblingExtensionRank(right);
230
+ if (rankDelta !== 0) return rankDelta;
231
+ return left.localeCompare(right);
232
+ });
233
+ selected.push(group[0]);
234
+ }
235
+
236
+ return selected.sort((left, right) => left.localeCompare(right));
237
+ }
238
+
239
+ function finalizeHeuristicMatches(relativePaths, fileIndex, limit = 20) {
240
+ return dedupeAuthoredCompiledSiblings(relativePaths, fileIndex).slice(0, limit);
241
+ }
242
+
160
243
  function readFileIndex(projectRoot, files, config) {
161
244
  const index = [];
162
245
  for (const relativePath of files) {
@@ -633,7 +716,7 @@ function findFilesByPathHints(pathHints, pathHintResolver) {
633
716
  return Array.from(new Set(matches)).sort((left, right) => left.localeCompare(right));
634
717
  }
635
718
 
636
- function findFilesBySymbols(symbolHints, fileIndex) {
719
+ function findFilesBySymbols(symbolHints, fileIndex, heuristicOptions = {}) {
637
720
  const matches = new Set();
638
721
  for (const symbol of symbolHints) {
639
722
  const regex = new RegExp(`\\b${escapeRegExp(symbol)}\\b`, 'i');
@@ -642,15 +725,18 @@ function findFilesBySymbols(symbolHints, fileIndex) {
642
725
  if (!CODE_EXTENSIONS.has(file.ext)) {
643
726
  continue;
644
727
  }
728
+ if (shouldSkipHeuristicFile(file.relativePath, heuristicOptions)) {
729
+ continue;
730
+ }
645
731
  if (regex.test(file.content)) {
646
732
  matches.add(file.relativePath);
647
733
  }
648
734
  }
649
735
  }
650
- return Array.from(matches).sort((left, right) => left.localeCompare(right));
736
+ return finalizeHeuristicMatches(Array.from(matches), fileIndex);
651
737
  }
652
738
 
653
- function findFilesByTaskPathTokens(taskText, fileIndex, pathDerivedTokens = new Set()) {
739
+ function findFilesByTaskPathTokens(taskText, fileIndex, pathDerivedTokens = new Set(), heuristicOptions = {}) {
654
740
  const tokens = tokenize(taskText)
655
741
  .filter((token) => token.length >= 6 && !GENERIC_TASK_TOKENS.has(token) && !pathDerivedTokens.has(token))
656
742
  .slice(0, 10);
@@ -659,6 +745,9 @@ function findFilesByTaskPathTokens(taskText, fileIndex, pathDerivedTokens = new
659
745
  const matches = new Set();
660
746
  for (const file of fileIndex) {
661
747
  if (file.generatedOutput) continue;
748
+ if (shouldSkipHeuristicFile(file.relativePath, heuristicOptions)) {
749
+ continue;
750
+ }
662
751
  const pathSegments = normalizePathForMatch(file.relativePath).split('/').filter(Boolean);
663
752
  for (const token of tokens) {
664
753
  if (pathSegments.some((segment) => segment === token || segment.includes(token))) {
@@ -668,16 +757,16 @@ function findFilesByTaskPathTokens(taskText, fileIndex, pathDerivedTokens = new
668
757
  }
669
758
  if (matches.size >= 20) break;
670
759
  }
671
- return Array.from(matches).sort((left, right) => left.localeCompare(right));
760
+ return finalizeHeuristicMatches(Array.from(matches), fileIndex);
672
761
  }
673
762
 
674
- function extractTaskEvidenceTokens(taskText, pathDerivedTokens = new Set()) {
763
+ function extractTaskEvidenceTokens(taskText, pathDerivedTokens = new Set(), minimumLength = 3) {
675
764
  return tokenize(taskText)
676
- .filter((token) => token.length >= 3 && !GENERIC_TASK_TOKENS.has(token) && !token.endsWith('/') && !pathDerivedTokens.has(token))
765
+ .filter((token) => token.length >= minimumLength && !GENERIC_TASK_TOKENS.has(token) && !token.endsWith('/') && !pathDerivedTokens.has(token))
677
766
  .slice(0, 8);
678
767
  }
679
768
 
680
- function findWeakPathContentSpecificTokens(taskText, fileIndex, weakPathFiles, pathDerivedTokens = new Set()) {
769
+ function findWeakPathContentSpecificTokens(taskText, fileIndex, weakPathFiles, pathDerivedTokens = new Set(), heuristicOptions = {}) {
681
770
  const tokens = extractTaskEvidenceTokens(taskText, pathDerivedTokens);
682
771
  if (tokens.length === 0 || weakPathFiles.length === 0) return [];
683
772
 
@@ -687,6 +776,9 @@ function findWeakPathContentSpecificTokens(taskText, fileIndex, weakPathFiles, p
687
776
  if (!weakFiles.has(file.relativePath) || !CODE_EXTENSIONS.has(file.ext) || file.isTestFile) {
688
777
  continue;
689
778
  }
779
+ if (shouldSkipHeuristicFile(file.relativePath, heuristicOptions)) {
780
+ continue;
781
+ }
690
782
 
691
783
  const normalizedPath = normalizePathForMatch(file.relativePath);
692
784
  const lowered = file.content.toLowerCase();
@@ -745,7 +837,7 @@ function extractPathDerivedTokens(pathHints) {
745
837
  return tokens;
746
838
  }
747
839
 
748
- function findCodeEvidence(taskText, fileIndex, pathDerivedTokens = new Set()) {
840
+ function findCodeEvidence(taskText, fileIndex, pathDerivedTokens = new Set(), heuristicOptions = {}) {
749
841
  const tokens = extractTaskEvidenceTokens(taskText, pathDerivedTokens);
750
842
  if (tokens.length === 0) {
751
843
  return [];
@@ -756,6 +848,9 @@ function findCodeEvidence(taskText, fileIndex, pathDerivedTokens = new Set()) {
756
848
  if (file.generatedOutput || !CODE_EXTENSIONS.has(file.ext) || file.isTestFile) {
757
849
  continue;
758
850
  }
851
+ if (shouldSkipHeuristicFile(file.relativePath, heuristicOptions)) {
852
+ continue;
853
+ }
759
854
 
760
855
  let score = 0;
761
856
  const lowered = file.content.toLowerCase();
@@ -776,7 +871,7 @@ function findCodeEvidence(taskText, fileIndex, pathDerivedTokens = new Set()) {
776
871
  }
777
872
  }
778
873
 
779
- return matches.slice(0, 20);
874
+ return finalizeHeuristicMatches(matches, fileIndex);
780
875
  }
781
876
 
782
877
  function normalizeReferencedPath(rawPath) {
@@ -811,7 +906,7 @@ function extractTestReadReferences(content) {
811
906
  return refs;
812
907
  }
813
908
 
814
- function findTestEvidence(taskText, fileIndex, referencedPaths = []) {
909
+ function findTestEvidence(taskText, fileIndex, referencedPaths = [], heuristicOptions = {}) {
815
910
  const tokens = tokenize(taskText)
816
911
  .filter((token) => token.length >= 3 && !GENERIC_TASK_TOKENS.has(token) && !token.endsWith('/'))
817
912
  .slice(0, 8);
@@ -829,6 +924,9 @@ function findTestEvidence(taskText, fileIndex, referencedPaths = []) {
829
924
 
830
925
  for (const file of fileIndex) {
831
926
  if (file.generatedOutput || !file.isTestFile) continue;
927
+ if (shouldSkipHeuristicFile(file.relativePath, heuristicOptions)) {
928
+ continue;
929
+ }
832
930
 
833
931
  // A test file counts as evidence only when it imports a module whose path contains
834
932
  // one of the task's meaningful tokens. Content-keyword matching is intentionally absent:
@@ -865,7 +963,7 @@ function findTestEvidence(taskText, fileIndex, referencedPaths = []) {
865
963
  }
866
964
  }
867
965
 
868
- return matches.slice(0, 20);
966
+ return finalizeHeuristicMatches(matches, fileIndex);
869
967
  }
870
968
 
871
969
  function findArtifactEvidence(taskText, fileIndex) {
@@ -1460,24 +1558,104 @@ function testCoversEndpoint(testFile, route, context) {
1460
1558
  return segments.length > 0 && segments.every((segment) => normalizedContent.includes(segment));
1461
1559
  }
1462
1560
 
1561
+ function extractPathDomainTokens(relativePath) {
1562
+ return splitNormalizedPathSegments(relativePath)
1563
+ .flatMap((segment) => tokenize(segment))
1564
+ .filter((token) => token.length >= 2 && !GENERIC_TASK_TOKENS.has(token));
1565
+ }
1566
+
1567
+ function collectRecipeLineContext(content, index) {
1568
+ const source = String(content || '');
1569
+ const lineStart = source.lastIndexOf('\n', index);
1570
+ const lineEnd = source.indexOf('\n', index);
1571
+ const previousLineStart = lineStart > 0 ? source.lastIndexOf('\n', lineStart - 1) : -1;
1572
+ const start = previousLineStart >= 0 ? previousLineStart + 1 : 0;
1573
+ const end = lineEnd >= 0 ? lineEnd : source.length;
1574
+ return source.slice(start, end);
1575
+ }
1576
+
1577
+ function appendUniqueDiagnostic(diagnostics, diagnostic) {
1578
+ if (!diagnostic || !diagnostic.code) {
1579
+ return diagnostics;
1580
+ }
1581
+ if (diagnostics.some((item) => item.code === diagnostic.code)) {
1582
+ return diagnostics;
1583
+ }
1584
+ diagnostics.push(diagnostic);
1585
+ return diagnostics;
1586
+ }
1587
+
1588
+ function buildTaskRecipeCandidatePool(task, context) {
1589
+ const { paths: pathHints, externalPaths } = extractExplicitPaths(task.text);
1590
+ const standaloneFilenames = extractStandaloneFilenames(task.text);
1591
+ const symbolHints = extractSymbolHints(task.text);
1592
+ const pathDerivedTokens = extractPathDerivedTokens([...pathHints, ...externalPaths, ...standaloneFilenames]);
1593
+ const heuristicOptions = {
1594
+ explicitPaths: [...pathHints, ...externalPaths],
1595
+ explicitFilenames: standaloneFilenames
1596
+ };
1597
+ const candidatePaths = unionArrays(
1598
+ findFilesByPathHints(pathHints, context.pathHintResolver),
1599
+ findFilesBySymbols(symbolHints, context.fileIndex, heuristicOptions),
1600
+ findCodeEvidence(task.text, context.fileIndex, pathDerivedTokens, heuristicOptions),
1601
+ findFilesByTaskPathTokens(task.text, context.fileIndex, pathDerivedTokens, heuristicOptions)
1602
+ );
1603
+ const indexedFiles = new Map(context.fileIndex.map((file) => [file.relativePath, file]));
1604
+ return candidatePaths.map((relativePath) => indexedFiles.get(relativePath)).filter(Boolean);
1605
+ }
1606
+
1463
1607
  function findVerificationRecipe(task, context) {
1608
+ if ((Array.isArray(task.verifyLines) && task.verifyLines.length > 0) || (Array.isArray(task.evidenceLines) && task.evidenceLines.length > 0)) {
1609
+ return { recipe: null, specificityFailure: false, foundStaticSignal: false };
1610
+ }
1611
+
1464
1612
  const patterns = [
1465
1613
  /disabled\s*=\s*\{[^}]+\}/i,
1466
1614
  /<(?:dialog|alertdialog)\b[^>]*\bopen\s*=/i,
1467
1615
  /abortcontroller|abortsignal\.timeout/i,
1468
1616
  /router\.push\s*\(/i
1469
1617
  ];
1470
- for (const file of context.fileIndex) {
1618
+ const taskTokens = extractTaskEvidenceTokens(task.text, new Set(), 2);
1619
+ const candidateFiles = buildTaskRecipeCandidatePool(task, context);
1620
+ const candidates = [];
1621
+ let foundStaticSignal = false;
1622
+
1623
+ for (const file of candidateFiles) {
1471
1624
  if (file.generatedOutput || file.isTestFile || !CODE_EXTENSIONS.has(file.ext)) continue;
1472
- const match = patterns.map((pattern) => pattern.exec(file.content)).find(Boolean);
1473
- if (!match) continue;
1474
- const line = file.content.slice(0, match.index).split(/\r?\n/).length;
1475
- const command = context.config.validation && context.config.validation.recipeCommand
1476
- ? `; run ${String(context.config.validation.recipeCommand).replace('{testFile}', '<test-file>')}`
1477
- : '';
1478
- return `${file.relativePath}:${line} inspect ${match[0].trim()}${command}`;
1625
+ for (const pattern of patterns) {
1626
+ const match = pattern.exec(file.content);
1627
+ if (!match) {
1628
+ continue;
1629
+ }
1630
+ foundStaticSignal = true;
1631
+ const pathTokens = extractPathDomainTokens(file.relativePath);
1632
+ if (!pathTokens.some((token) => taskTokens.includes(token))) {
1633
+ continue;
1634
+ }
1635
+ const lineContext = collectRecipeLineContext(file.content, match.index);
1636
+ const contextTokens = extractTaskEvidenceTokens(`${match[0]} ${lineContext}`, new Set(), 2);
1637
+ if (!contextTokens.some((token) => taskTokens.includes(token))) {
1638
+ continue;
1639
+ }
1640
+ const line = file.content.slice(0, match.index).split(/\r?\n/).length;
1641
+ const command = context.config.validation && context.config.validation.recipeCommand
1642
+ ? `; run ${String(context.config.validation.recipeCommand).replace('{testFile}', '<test-file>')}`
1643
+ : '';
1644
+ candidates.push(`${file.relativePath}:${line} inspect ${match[0].trim()}${command}`);
1645
+ break;
1646
+ }
1479
1647
  }
1480
- return null;
1648
+
1649
+ const uniqueCandidates = Array.from(new Set(candidates));
1650
+ if (uniqueCandidates.length === 1) {
1651
+ return { recipe: uniqueCandidates[0], specificityFailure: false, foundStaticSignal };
1652
+ }
1653
+
1654
+ return {
1655
+ recipe: null,
1656
+ specificityFailure: foundStaticSignal || candidates.length > 0,
1657
+ foundStaticSignal
1658
+ };
1481
1659
  }
1482
1660
 
1483
1661
  function isBehavioralTask(taskText) {
@@ -1490,17 +1668,32 @@ function evaluateDeterministicVerification(task, context) {
1490
1668
  if (!isBehavioralTask(task.text)) {
1491
1669
  return { applicable: false, passed: false, reasons: [], diagnostics: [], recipe: null };
1492
1670
  }
1493
- const recipe = findVerificationRecipe(task, context);
1671
+ const recipeResult = findVerificationRecipe(task, context);
1672
+ const diagnostics = [];
1673
+ if (recipeResult.specificityFailure) {
1674
+ diagnostics.push({
1675
+ code: 'REQUIRES_HUMAN_EVIDENCE',
1676
+ severity: 'warning',
1677
+ message: 'behavioral task requires explicit human or test evidence'
1678
+ });
1679
+ diagnostics.push({
1680
+ code: 'NO_SPECIFIC_RECIPE',
1681
+ severity: 'warning',
1682
+ message: 'no task-specific verification recipe could be generated'
1683
+ });
1684
+ } else {
1685
+ diagnostics.push({
1686
+ code: recipeResult.recipe ? 'REQUIRES_HUMAN_EVIDENCE' : 'NO_STATIC_SIGNAL',
1687
+ severity: 'warning',
1688
+ message: recipeResult.recipe ? 'behavioral task requires explicit human or test evidence' : 'no static implementation signal was found for behavioral task'
1689
+ });
1690
+ }
1494
1691
  return {
1495
1692
  applicable: false,
1496
1693
  passed: false,
1497
1694
  reasons: [],
1498
- diagnostics: [{
1499
- code: recipe ? 'REQUIRES_HUMAN_EVIDENCE' : 'NO_STATIC_SIGNAL',
1500
- severity: 'warning',
1501
- message: recipe ? 'behavioral task requires explicit human or test evidence' : 'no static implementation signal was found for behavioral task'
1502
- }],
1503
- recipe
1695
+ diagnostics,
1696
+ recipe: recipeResult.recipe
1504
1697
  };
1505
1698
  }
1506
1699
  const reasons = [];
@@ -1576,11 +1769,11 @@ function evaluateDeterministicVerification(task, context) {
1576
1769
  reasons,
1577
1770
  diagnostics,
1578
1771
  generatedTestEvidence,
1579
- recipe: passed ? null : findVerificationRecipe(task, context)
1772
+ recipe: null
1580
1773
  };
1581
1774
  }
1582
1775
 
1583
- function buildValidationContext(projectRoot, config, plugins) {
1776
+ function buildValidationContext(projectRoot, config, plugins, options = {}) {
1584
1777
  const files = walkFiles(projectRoot);
1585
1778
  const fileIndex = readFileIndex(projectRoot, files, config);
1586
1779
  const testFrameworks = detectTestFrameworks(projectRoot, files);
@@ -1594,7 +1787,8 @@ function buildValidationContext(projectRoot, config, plugins) {
1594
1787
  fileIndex,
1595
1788
  pathHintResolver,
1596
1789
  testFrameworks,
1597
- testReportRecords: readTestReportRecords(projectRoot, config.validation)
1790
+ testReportRecords: readTestReportRecords(projectRoot, config.validation),
1791
+ strictValidation: options.strictValidation === true
1598
1792
  };
1599
1793
  }
1600
1794
 
@@ -1668,21 +1862,26 @@ function validateTask(task, context, config, plugins) {
1668
1862
  const purePathHints = pathHints.filter((p) => !lineReferenceHints.has(p));
1669
1863
  const standaloneFilenames = extractStandaloneFilenames(task.text);
1670
1864
  const symbolHints = extractSymbolHints(task.text);
1865
+ const heuristicOptions = {
1866
+ explicitPaths: [...pathHints, ...externalPaths],
1867
+ explicitFilenames: standaloneFilenames
1868
+ };
1869
+ const strictValidation = context.strictValidation === true;
1671
1870
  const authoritativeEvidence = evaluateAuthoritativeEvidence(task, context.pathHintResolver);
1672
1871
  const deterministicVerification = evaluateDeterministicVerification(task, context);
1673
1872
  const hasExplicitPendingItems = Array.isArray(task.explicitPendingItems) && task.explicitPendingItems.length > 0;
1674
1873
 
1675
1874
  const filesFromPaths = findFilesByPathHints(pathHints, context.pathHintResolver);
1676
1875
  const filesFromPurePathHints = findFilesByPathHints(purePathHints, context.pathHintResolver);
1677
- const filesFromSymbols = findFilesBySymbols(symbolHints, context.fileIndex);
1876
+ const filesFromSymbols = findFilesBySymbols(symbolHints, context.fileIndex, heuristicOptions);
1678
1877
  // Combine path hints AND standalone filenames for token exclusion so that tokens
1679
1878
  // derived from any referenced filename (e.g. "roadmap-skill" from
1680
1879
  // "roadmap-skill.config.json") are excluded from code evidence scoring.
1681
1880
  const pathDerivedTokens = extractPathDerivedTokens([...pathHints, ...externalPaths, ...standaloneFilenames]);
1682
- const filesFromCode = findCodeEvidence(task.text, context.fileIndex, pathDerivedTokens);
1683
- const filesFromWeakPathTokens = findFilesByTaskPathTokens(task.text, context.fileIndex, pathDerivedTokens);
1684
- const weakPathContentTokens = findWeakPathContentSpecificTokens(task.text, context.fileIndex, filesFromWeakPathTokens, pathDerivedTokens);
1685
- const filesFromTests = findTestEvidence(task.text, context.fileIndex, [...pathHints, ...standaloneFilenames]);
1881
+ const filesFromCode = findCodeEvidence(task.text, context.fileIndex, pathDerivedTokens, heuristicOptions);
1882
+ const filesFromWeakPathTokens = findFilesByTaskPathTokens(task.text, context.fileIndex, pathDerivedTokens, heuristicOptions);
1883
+ const weakPathContentTokens = findWeakPathContentSpecificTokens(task.text, context.fileIndex, filesFromWeakPathTokens, pathDerivedTokens, heuristicOptions);
1884
+ const filesFromTests = findTestEvidence(task.text, context.fileIndex, [...pathHints, ...standaloneFilenames], heuristicOptions);
1686
1885
  const { files: filesFromArtifacts, heuristicArtifacts } = findArtifactEvidence(task.text, context.fileIndex);
1687
1886
 
1688
1887
  const structuralCheck = checkNamespaceStructuralEvidence(task.id, task.text, context.fileIndex);
@@ -1855,7 +2054,9 @@ function validateTask(task, context, config, plugins) {
1855
2054
  // Heuristic proximity remains useful to locate candidates, but is never sufficient to
1856
2055
  // complete an unchecked task. Completion requires human Evidence, a trusted rule, or a
1857
2056
  // typed deterministic verifier.
1858
- let passed = authoritativeEvidence.passed || hasTrustedRuleEvidencePass || deterministicVerification.passed;
2057
+ let passed = strictValidation
2058
+ ? (authoritativeEvidence.passed || deterministicVerification.passed)
2059
+ : (authoritativeEvidence.passed || hasTrustedRuleEvidencePass || deterministicVerification.passed);
1859
2060
 
1860
2061
  if (!task.checked && !authoritativeEvidence.passed && !hasTrustedRuleEvidencePass && !deterministicVerification.passed && hasHighConfidenceImplementationEvidence) {
1861
2062
  uniqueReasons.push('implementation task requires deterministic Verify metadata or explicit Evidence to be marked complete');
@@ -1926,6 +2127,7 @@ function validateTask(task, context, config, plugins) {
1926
2127
  const shouldPreserveCheckedTask =
1927
2128
  task.checked &&
1928
2129
  !passed &&
2130
+ !strictValidation &&
1929
2131
  !authoritativeEvidence.active &&
1930
2132
  symbolHints.length === 0 &&
1931
2133
  negativeSignalMatches.length === 0 &&
@@ -1943,7 +2145,9 @@ function validateTask(task, context, config, plugins) {
1943
2145
  // Used by auditValidation to flag implementation tasks that pass solely via documentation.
1944
2146
  const evidenceIsDocOnly = !evidence.code && !evidence.test && evidence.artifact && !isDocTask(task.text);
1945
2147
 
1946
- const finalPassed = overrideResult ? (overrideResult.passed !== false && !hasExplicitPendingItems) : (passed && uniqueReasons.length === 0 && !hasExplicitPendingItems);
2148
+ const finalPassed = (!strictValidation && overrideResult)
2149
+ ? (overrideResult.passed !== false && !hasExplicitPendingItems)
2150
+ : (passed && uniqueReasons.length === 0 && !hasExplicitPendingItems);
1947
2151
  return {
1948
2152
  taskId: task.id,
1949
2153
  passed: finalPassed,
@@ -1992,6 +2196,33 @@ function validateTasks(tasks, context, config, plugins) {
1992
2196
  }
1993
2197
  }
1994
2198
 
2199
+ const recipeOwners = new Map();
2200
+ for (const [taskId, taskResult] of Object.entries(result)) {
2201
+ if (!taskResult.verificationRecipe) {
2202
+ continue;
2203
+ }
2204
+ const owners = recipeOwners.get(taskResult.verificationRecipe) || [];
2205
+ owners.push(taskId);
2206
+ recipeOwners.set(taskResult.verificationRecipe, owners);
2207
+ }
2208
+
2209
+ for (const [recipe, owners] of recipeOwners.entries()) {
2210
+ if (owners.length < 2) {
2211
+ continue;
2212
+ }
2213
+ for (const taskId of owners) {
2214
+ if (!result[taskId] || result[taskId].verificationRecipe !== recipe) {
2215
+ continue;
2216
+ }
2217
+ result[taskId].verificationRecipe = null;
2218
+ appendUniqueDiagnostic(result[taskId].diagnostics, {
2219
+ code: 'NO_SPECIFIC_RECIPE',
2220
+ severity: 'warning',
2221
+ message: 'generated verification recipe was not unique to a single task'
2222
+ });
2223
+ }
2224
+ }
2225
+
1995
2226
  return result;
1996
2227
  }
1997
2228