@nebulacomponents/citable 1.19.0 → 1.20.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.
Files changed (239) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +12 -1
  3. package/dist/universal/.agents/skills/citable/SKILL.md +11 -2
  4. package/dist/universal/.agents/skills/citable/VERSION +1 -1
  5. package/dist/universal/.agents/skills/citable/manifest.json +15 -12
  6. package/dist/universal/.agents/skills/citable/schemas/action-plan.schema.json +38 -1
  7. package/dist/universal/.agents/skills/citable/schemas/audit-coverage.schema.json +118 -0
  8. package/dist/universal/.agents/skills/citable/schemas/collection-result.schema.json +35 -0
  9. package/dist/universal/.agents/skills/citable/schemas/config.schema.json +2 -0
  10. package/dist/universal/.agents/skills/citable/schemas/cro-report.schema.json +8 -1
  11. package/dist/universal/.agents/skills/citable/schemas/finding.schema.json +105 -2
  12. package/dist/universal/.agents/skills/citable/schemas/remediation-verification.schema.json +12 -1
  13. package/dist/universal/.agents/skills/citable/schemas/run.schema.json +15 -2
  14. package/dist/universal/.agents/skills/citable/schemas/search-report.schema.json +8 -1
  15. package/dist/universal/.agents/skills/citable/schemas/summary.schema.json +41 -0
  16. package/dist/universal/.claude/agents/citable/manifest.json +2 -2
  17. package/dist/universal/.claude/skills/citable/SKILL.md +11 -2
  18. package/dist/universal/.claude/skills/citable/VERSION +1 -1
  19. package/dist/universal/.claude/skills/citable/manifest.json +15 -12
  20. package/dist/universal/.claude/skills/citable/schemas/action-plan.schema.json +38 -1
  21. package/dist/universal/.claude/skills/citable/schemas/audit-coverage.schema.json +118 -0
  22. package/dist/universal/.claude/skills/citable/schemas/collection-result.schema.json +35 -0
  23. package/dist/universal/.claude/skills/citable/schemas/config.schema.json +2 -0
  24. package/dist/universal/.claude/skills/citable/schemas/cro-report.schema.json +8 -1
  25. package/dist/universal/.claude/skills/citable/schemas/finding.schema.json +105 -2
  26. package/dist/universal/.claude/skills/citable/schemas/remediation-verification.schema.json +12 -1
  27. package/dist/universal/.claude/skills/citable/schemas/run.schema.json +15 -2
  28. package/dist/universal/.claude/skills/citable/schemas/search-report.schema.json +8 -1
  29. package/dist/universal/.claude/skills/citable/schemas/summary.schema.json +41 -0
  30. package/dist/universal/.cursor/skills/citable/SKILL.md +11 -2
  31. package/dist/universal/.cursor/skills/citable/VERSION +1 -1
  32. package/dist/universal/.cursor/skills/citable/manifest.json +15 -12
  33. package/dist/universal/.cursor/skills/citable/schemas/action-plan.schema.json +38 -1
  34. package/dist/universal/.cursor/skills/citable/schemas/audit-coverage.schema.json +118 -0
  35. package/dist/universal/.cursor/skills/citable/schemas/collection-result.schema.json +35 -0
  36. package/dist/universal/.cursor/skills/citable/schemas/config.schema.json +2 -0
  37. package/dist/universal/.cursor/skills/citable/schemas/cro-report.schema.json +8 -1
  38. package/dist/universal/.cursor/skills/citable/schemas/finding.schema.json +105 -2
  39. package/dist/universal/.cursor/skills/citable/schemas/remediation-verification.schema.json +12 -1
  40. package/dist/universal/.cursor/skills/citable/schemas/run.schema.json +15 -2
  41. package/dist/universal/.cursor/skills/citable/schemas/search-report.schema.json +8 -1
  42. package/dist/universal/.cursor/skills/citable/schemas/summary.schema.json +41 -0
  43. package/dist/universal/.gemini/skills/citable/SKILL.md +11 -2
  44. package/dist/universal/.gemini/skills/citable/VERSION +1 -1
  45. package/dist/universal/.gemini/skills/citable/manifest.json +15 -12
  46. package/dist/universal/.gemini/skills/citable/schemas/action-plan.schema.json +38 -1
  47. package/dist/universal/.gemini/skills/citable/schemas/audit-coverage.schema.json +118 -0
  48. package/dist/universal/.gemini/skills/citable/schemas/collection-result.schema.json +35 -0
  49. package/dist/universal/.gemini/skills/citable/schemas/config.schema.json +2 -0
  50. package/dist/universal/.gemini/skills/citable/schemas/cro-report.schema.json +8 -1
  51. package/dist/universal/.gemini/skills/citable/schemas/finding.schema.json +105 -2
  52. package/dist/universal/.gemini/skills/citable/schemas/remediation-verification.schema.json +12 -1
  53. package/dist/universal/.gemini/skills/citable/schemas/run.schema.json +15 -2
  54. package/dist/universal/.gemini/skills/citable/schemas/search-report.schema.json +8 -1
  55. package/dist/universal/.gemini/skills/citable/schemas/summary.schema.json +41 -0
  56. package/dist/universal/.github/skills/citable/SKILL.md +11 -2
  57. package/dist/universal/.github/skills/citable/VERSION +1 -1
  58. package/dist/universal/.github/skills/citable/manifest.json +15 -12
  59. package/dist/universal/.github/skills/citable/schemas/action-plan.schema.json +38 -1
  60. package/dist/universal/.github/skills/citable/schemas/audit-coverage.schema.json +118 -0
  61. package/dist/universal/.github/skills/citable/schemas/collection-result.schema.json +35 -0
  62. package/dist/universal/.github/skills/citable/schemas/config.schema.json +2 -0
  63. package/dist/universal/.github/skills/citable/schemas/cro-report.schema.json +8 -1
  64. package/dist/universal/.github/skills/citable/schemas/finding.schema.json +105 -2
  65. package/dist/universal/.github/skills/citable/schemas/remediation-verification.schema.json +12 -1
  66. package/dist/universal/.github/skills/citable/schemas/run.schema.json +15 -2
  67. package/dist/universal/.github/skills/citable/schemas/search-report.schema.json +8 -1
  68. package/dist/universal/.github/skills/citable/schemas/summary.schema.json +41 -0
  69. package/dist/universal/.kiro/skills/citable/SKILL.md +11 -2
  70. package/dist/universal/.kiro/skills/citable/VERSION +1 -1
  71. package/dist/universal/.kiro/skills/citable/manifest.json +15 -12
  72. package/dist/universal/.kiro/skills/citable/schemas/action-plan.schema.json +38 -1
  73. package/dist/universal/.kiro/skills/citable/schemas/audit-coverage.schema.json +118 -0
  74. package/dist/universal/.kiro/skills/citable/schemas/collection-result.schema.json +35 -0
  75. package/dist/universal/.kiro/skills/citable/schemas/config.schema.json +2 -0
  76. package/dist/universal/.kiro/skills/citable/schemas/cro-report.schema.json +8 -1
  77. package/dist/universal/.kiro/skills/citable/schemas/finding.schema.json +105 -2
  78. package/dist/universal/.kiro/skills/citable/schemas/remediation-verification.schema.json +12 -1
  79. package/dist/universal/.kiro/skills/citable/schemas/run.schema.json +15 -2
  80. package/dist/universal/.kiro/skills/citable/schemas/search-report.schema.json +8 -1
  81. package/dist/universal/.kiro/skills/citable/schemas/summary.schema.json +41 -0
  82. package/dist/universal/.opencode/skills/citable/SKILL.md +11 -2
  83. package/dist/universal/.opencode/skills/citable/VERSION +1 -1
  84. package/dist/universal/.opencode/skills/citable/manifest.json +15 -12
  85. package/dist/universal/.opencode/skills/citable/schemas/action-plan.schema.json +38 -1
  86. package/dist/universal/.opencode/skills/citable/schemas/audit-coverage.schema.json +118 -0
  87. package/dist/universal/.opencode/skills/citable/schemas/collection-result.schema.json +35 -0
  88. package/dist/universal/.opencode/skills/citable/schemas/config.schema.json +2 -0
  89. package/dist/universal/.opencode/skills/citable/schemas/cro-report.schema.json +8 -1
  90. package/dist/universal/.opencode/skills/citable/schemas/finding.schema.json +105 -2
  91. package/dist/universal/.opencode/skills/citable/schemas/remediation-verification.schema.json +12 -1
  92. package/dist/universal/.opencode/skills/citable/schemas/run.schema.json +15 -2
  93. package/dist/universal/.opencode/skills/citable/schemas/search-report.schema.json +8 -1
  94. package/dist/universal/.opencode/skills/citable/schemas/summary.schema.json +41 -0
  95. package/dist/universal/.pi/agent/skills/citable/SKILL.md +11 -2
  96. package/dist/universal/.pi/agent/skills/citable/VERSION +1 -1
  97. package/dist/universal/.pi/agent/skills/citable/manifest.json +15 -12
  98. package/dist/universal/.pi/agent/skills/citable/schemas/action-plan.schema.json +38 -1
  99. package/dist/universal/.pi/agent/skills/citable/schemas/audit-coverage.schema.json +118 -0
  100. package/dist/universal/.pi/agent/skills/citable/schemas/collection-result.schema.json +35 -0
  101. package/dist/universal/.pi/agent/skills/citable/schemas/config.schema.json +2 -0
  102. package/dist/universal/.pi/agent/skills/citable/schemas/cro-report.schema.json +8 -1
  103. package/dist/universal/.pi/agent/skills/citable/schemas/finding.schema.json +105 -2
  104. package/dist/universal/.pi/agent/skills/citable/schemas/remediation-verification.schema.json +12 -1
  105. package/dist/universal/.pi/agent/skills/citable/schemas/run.schema.json +15 -2
  106. package/dist/universal/.pi/agent/skills/citable/schemas/search-report.schema.json +8 -1
  107. package/dist/universal/.pi/agent/skills/citable/schemas/summary.schema.json +41 -0
  108. package/dist/universal/.qoder/skills/citable/SKILL.md +11 -2
  109. package/dist/universal/.qoder/skills/citable/VERSION +1 -1
  110. package/dist/universal/.qoder/skills/citable/manifest.json +15 -12
  111. package/dist/universal/.qoder/skills/citable/schemas/action-plan.schema.json +38 -1
  112. package/dist/universal/.qoder/skills/citable/schemas/audit-coverage.schema.json +118 -0
  113. package/dist/universal/.qoder/skills/citable/schemas/collection-result.schema.json +35 -0
  114. package/dist/universal/.qoder/skills/citable/schemas/config.schema.json +2 -0
  115. package/dist/universal/.qoder/skills/citable/schemas/cro-report.schema.json +8 -1
  116. package/dist/universal/.qoder/skills/citable/schemas/finding.schema.json +105 -2
  117. package/dist/universal/.qoder/skills/citable/schemas/remediation-verification.schema.json +12 -1
  118. package/dist/universal/.qoder/skills/citable/schemas/run.schema.json +15 -2
  119. package/dist/universal/.qoder/skills/citable/schemas/search-report.schema.json +8 -1
  120. package/dist/universal/.qoder/skills/citable/schemas/summary.schema.json +41 -0
  121. package/dist/universal/.rovodev/skills/citable/SKILL.md +11 -2
  122. package/dist/universal/.rovodev/skills/citable/VERSION +1 -1
  123. package/dist/universal/.rovodev/skills/citable/manifest.json +15 -12
  124. package/dist/universal/.rovodev/skills/citable/schemas/action-plan.schema.json +38 -1
  125. package/dist/universal/.rovodev/skills/citable/schemas/audit-coverage.schema.json +118 -0
  126. package/dist/universal/.rovodev/skills/citable/schemas/collection-result.schema.json +35 -0
  127. package/dist/universal/.rovodev/skills/citable/schemas/config.schema.json +2 -0
  128. package/dist/universal/.rovodev/skills/citable/schemas/cro-report.schema.json +8 -1
  129. package/dist/universal/.rovodev/skills/citable/schemas/finding.schema.json +105 -2
  130. package/dist/universal/.rovodev/skills/citable/schemas/remediation-verification.schema.json +12 -1
  131. package/dist/universal/.rovodev/skills/citable/schemas/run.schema.json +15 -2
  132. package/dist/universal/.rovodev/skills/citable/schemas/search-report.schema.json +8 -1
  133. package/dist/universal/.rovodev/skills/citable/schemas/summary.schema.json +41 -0
  134. package/dist/universal/.trae/skills/citable/SKILL.md +11 -2
  135. package/dist/universal/.trae/skills/citable/VERSION +1 -1
  136. package/dist/universal/.trae/skills/citable/manifest.json +15 -12
  137. package/dist/universal/.trae/skills/citable/schemas/action-plan.schema.json +38 -1
  138. package/dist/universal/.trae/skills/citable/schemas/audit-coverage.schema.json +118 -0
  139. package/dist/universal/.trae/skills/citable/schemas/collection-result.schema.json +35 -0
  140. package/dist/universal/.trae/skills/citable/schemas/config.schema.json +2 -0
  141. package/dist/universal/.trae/skills/citable/schemas/cro-report.schema.json +8 -1
  142. package/dist/universal/.trae/skills/citable/schemas/finding.schema.json +105 -2
  143. package/dist/universal/.trae/skills/citable/schemas/remediation-verification.schema.json +12 -1
  144. package/dist/universal/.trae/skills/citable/schemas/run.schema.json +15 -2
  145. package/dist/universal/.trae/skills/citable/schemas/search-report.schema.json +8 -1
  146. package/dist/universal/.trae/skills/citable/schemas/summary.schema.json +41 -0
  147. package/dist/universal/.trae-cn/skills/citable/SKILL.md +11 -2
  148. package/dist/universal/.trae-cn/skills/citable/VERSION +1 -1
  149. package/dist/universal/.trae-cn/skills/citable/manifest.json +15 -12
  150. package/dist/universal/.trae-cn/skills/citable/schemas/action-plan.schema.json +38 -1
  151. package/dist/universal/.trae-cn/skills/citable/schemas/audit-coverage.schema.json +118 -0
  152. package/dist/universal/.trae-cn/skills/citable/schemas/collection-result.schema.json +35 -0
  153. package/dist/universal/.trae-cn/skills/citable/schemas/config.schema.json +2 -0
  154. package/dist/universal/.trae-cn/skills/citable/schemas/cro-report.schema.json +8 -1
  155. package/dist/universal/.trae-cn/skills/citable/schemas/finding.schema.json +105 -2
  156. package/dist/universal/.trae-cn/skills/citable/schemas/remediation-verification.schema.json +12 -1
  157. package/dist/universal/.trae-cn/skills/citable/schemas/run.schema.json +15 -2
  158. package/dist/universal/.trae-cn/skills/citable/schemas/search-report.schema.json +8 -1
  159. package/dist/universal/.trae-cn/skills/citable/schemas/summary.schema.json +41 -0
  160. package/dist/universal/manifest.json +184 -148
  161. package/package.json +1 -1
  162. package/schemas/action-plan.schema.json +38 -1
  163. package/schemas/audit-coverage.schema.json +118 -0
  164. package/schemas/collection-result.schema.json +35 -0
  165. package/schemas/config.schema.json +2 -0
  166. package/schemas/cro-report.schema.json +8 -1
  167. package/schemas/finding.schema.json +105 -2
  168. package/schemas/remediation-verification.schema.json +12 -1
  169. package/schemas/run.schema.json +15 -2
  170. package/schemas/search-report.schema.json +8 -1
  171. package/schemas/summary.schema.json +41 -0
  172. package/skill/SKILL.md +11 -2
  173. package/src/acceptance/reproducibility.js +5 -25
  174. package/src/analysis/croRoadmap.js +3 -3
  175. package/src/analysis/strategicRoadmap.js +3 -3
  176. package/src/artifacts/interchange.js +2 -6
  177. package/src/cli/index.js +15 -3
  178. package/src/commands/actionPlan.js +46 -5
  179. package/src/commands/applyRemediation.js +5 -5
  180. package/src/commands/audit.js +75 -8
  181. package/src/commands/ciWorkflow.js +7 -2
  182. package/src/commands/compareSnapshots.js +155 -47
  183. package/src/commands/connect.js +24 -4
  184. package/src/commands/context.js +41 -2
  185. package/src/commands/croSuite.js +51 -13
  186. package/src/commands/delivery.js +7 -7
  187. package/src/commands/governance.js +9 -10
  188. package/src/commands/implementationKit.js +2 -1
  189. package/src/commands/init.js +8 -1
  190. package/src/commands/previewCro.js +1 -1
  191. package/src/commands/prioritize.js +3 -2
  192. package/src/commands/reviews.js +31 -5
  193. package/src/commands/roadmapCmd.js +7 -13
  194. package/src/commands/sweep.js +15 -2
  195. package/src/commands/verifyRemediation.js +94 -19
  196. package/src/connectors/collectionResult.js +80 -0
  197. package/src/connectors/ga4.js +46 -11
  198. package/src/connectors/gsc.js +41 -6
  199. package/src/connectors/index.js +1 -1
  200. package/src/connectors/webflow.js +140 -10
  201. package/src/connectors/wordpress.js +92 -8
  202. package/src/crawler/fetch.js +220 -24
  203. package/src/crawler/resourceValidity.js +119 -0
  204. package/src/crawler/sitemap.js +435 -16
  205. package/src/crawler/sitemapCollector.js +364 -0
  206. package/src/crawler/urlIdentity.js +97 -0
  207. package/src/detectors/agent.js +11 -0
  208. package/src/detectors/ans.js +16 -1
  209. package/src/detectors/arch.js +6 -0
  210. package/src/detectors/claim.js +9 -0
  211. package/src/detectors/crawl.js +7 -0
  212. package/src/detectors/cro.js +21 -0
  213. package/src/detectors/cwv.js +4 -0
  214. package/src/detectors/entity.js +7 -0
  215. package/src/detectors/evd.js +9 -0
  216. package/src/detectors/framework.js +28 -2
  217. package/src/detectors/geoReco.js +16 -0
  218. package/src/detectors/hreflang.js +3 -0
  219. package/src/detectors/lifeMeas.js +9 -0
  220. package/src/detectors/link.js +6 -1
  221. package/src/detectors/page.js +11 -0
  222. package/src/detectors/schemaData.js +16 -1
  223. package/src/detectors/tech.js +34 -3
  224. package/src/evidence/coverage.js +448 -0
  225. package/src/evidence/determination.js +157 -0
  226. package/src/evidence/downstream.js +110 -0
  227. package/src/evidence/hashes.js +109 -0
  228. package/src/evidence/run.js +68 -8
  229. package/src/extractor/page.js +29 -2
  230. package/src/extractor/site.js +298 -50
  231. package/src/reporting/executiveCroReport.js +36 -13
  232. package/src/reporting/executiveExport.js +45 -16
  233. package/src/reporting/executiveSearchReport.js +56 -22
  234. package/src/reporting/report.js +56 -9
  235. package/src/shared/epistemicLanguage.js +86 -0
  236. package/src/shared/evidenceSourceResolver.js +48 -17
  237. package/src/shared/htmlEscape.js +8 -4
  238. package/src/shared/runPackageVerifier.js +99 -8
  239. package/src/shared/verifiedRunLoader.js +129 -0
@@ -3,6 +3,7 @@ import { defineDetector, indexTargets, pageSubject, entrySubject, sitePageFor }
3
3
  const D = [];
4
4
 
5
5
  D.push(defineDetector({
6
+ coverage_requirement: 'evaluated_subset',
6
7
  id: 'ARCH-001', name: 'Orphan page', namespace: 'ARCH',
7
8
  description: 'An index-target page receives no internal links from any other audited page.',
8
9
  discipline: ['seo', 'aeo'], severity: 'high', deterministic: true, requires: ['site'],
@@ -24,6 +25,7 @@ D.push(defineDetector({
24
25
  }));
25
26
 
26
27
  D.push(defineDetector({
28
+ coverage_requirement: 'evaluated_subset',
27
29
  id: 'ARCH-002', name: 'Dead-end page', namespace: 'ARCH',
28
30
  description: 'A page has no outbound internal links; users and crawlers cannot continue anywhere.',
29
31
  discipline: ['seo'], severity: 'medium', deterministic: true, requires: ['site'],
@@ -43,6 +45,7 @@ D.push(defineDetector({
43
45
  }));
44
46
 
45
47
  D.push(defineDetector({
48
+ coverage_requirement: 'evaluated_subset',
46
49
  id: 'ARCH-003', name: 'Excessive crawl depth', namespace: 'ARCH',
47
50
  description: 'An index-target page requires more than the configured number of clicks from the home page.',
48
51
  discipline: ['seo'], severity: 'medium', deterministic: true, requires: ['site'],
@@ -66,6 +69,7 @@ D.push(defineDetector({
66
69
  }));
67
70
 
68
71
  D.push(defineDetector({
72
+ coverage_requirement: 'evaluated_subset',
69
73
  id: 'ARCH-004', name: 'Duplicate query target', namespace: 'ARCH',
70
74
  description: 'Two or more registry pages target the same query — internal competition for one intent.',
71
75
  discipline: ['seo', 'aeo'], severity: 'high', deterministic: true, requires: ['registries'],
@@ -93,6 +97,7 @@ D.push(defineDetector({
93
97
  }));
94
98
 
95
99
  D.push(defineDetector({
100
+ coverage_requirement: 'evaluated_subset',
96
101
  id: 'ARCH-005', name: 'Overlapping primary intent', namespace: 'ARCH',
97
102
  description: 'Two active registry pages declare the same primary intent and share a primary entity.',
98
103
  discipline: ['seo'], severity: 'medium', deterministic: true, requires: ['registries'],
@@ -120,6 +125,7 @@ D.push(defineDetector({
120
125
  }));
121
126
 
122
127
  D.push(defineDetector({
128
+ coverage_requirement: 'evaluated_subset',
123
129
  id: 'ARCH-006', name: 'Page without topic-hub relationship', namespace: 'ARCH',
124
130
  description: 'A non-hub content page has no internal link to any hub/category page.',
125
131
  discipline: ['seo', 'aeo'], severity: 'low', deterministic: true, requires: ['site', 'registries'],
@@ -10,6 +10,7 @@ function evidenceById(ctx) {
10
10
  }
11
11
 
12
12
  D.push(defineDetector({
13
+ coverage_requirement: 'evaluated_subset',
13
14
  id: 'CLAIM-001', name: 'Verified claim without evidence', namespace: 'CLAIM',
14
15
  description: 'A claim is marked verified but references no evidence entries.',
15
16
  discipline: ['aeo', 'geo'], severity: 'critical', deterministic: true, requires: ['registries'],
@@ -29,6 +30,7 @@ D.push(defineDetector({
29
30
  }));
30
31
 
31
32
  D.push(defineDetector({
33
+ coverage_requirement: 'evaluated_subset',
32
34
  id: 'CLAIM-002', name: 'Verified claim depends on expired or revoked evidence', namespace: 'CLAIM',
33
35
  description: 'All evidence supporting a verified claim is expired, stale, revoked, or inaccessible.',
34
36
  discipline: ['aeo', 'geo'], severity: 'high', deterministic: true, requires: ['registries'],
@@ -60,6 +62,7 @@ D.push(defineDetector({
60
62
  }));
61
63
 
62
64
  D.push(defineDetector({
65
+ coverage_requirement: 'evaluated_subset',
63
66
  id: 'CLAIM-003', name: 'Claim active after expiry', namespace: 'CLAIM',
64
67
  description: 'A claim has passed its expires date but is not marked expired/retired.',
65
68
  discipline: ['aeo', 'geo'], severity: 'high', deterministic: true, requires: ['registries'],
@@ -79,6 +82,7 @@ D.push(defineDetector({
79
82
  }));
80
83
 
81
84
  D.push(defineDetector({
85
+ coverage_requirement: 'evaluated_subset',
82
86
  id: 'CLAIM-004', name: 'Quantitative or comparative claim without evidence', namespace: 'CLAIM',
83
87
  description: 'A performance or comparative claim (any status except prohibited/retired) has no supporting evidence recorded.',
84
88
  discipline: ['seo', 'aeo', 'geo'], severity: 'high', deterministic: true, requires: ['registries'],
@@ -98,6 +102,7 @@ D.push(defineDetector({
98
102
  }));
99
103
 
100
104
  D.push(defineDetector({
105
+ coverage_requirement: 'evaluated_subset',
101
106
  id: 'CLAIM-005', name: 'Opinion or aspiration represented as verified fact', namespace: 'CLAIM',
102
107
  description: 'An opinion, position, or aspirational claim carries a "verified" status, flattening the claim hierarchy.',
103
108
  discipline: ['geo'], severity: 'medium', deterministic: true, requires: ['registries'],
@@ -117,6 +122,7 @@ D.push(defineDetector({
117
122
  }));
118
123
 
119
124
  D.push(defineDetector({
125
+ coverage_requirement: 'evaluated_subset',
120
126
  id: 'CLAIM-006', name: 'Regulated claim without required review', namespace: 'CLAIM',
121
127
  description: 'A legal/regulatory, security, or commercial claim has no legal_status assessment.',
122
128
  discipline: ['aeo', 'geo'], severity: 'high', deterministic: true, requires: ['registries'],
@@ -137,6 +143,7 @@ D.push(defineDetector({
137
143
  }));
138
144
 
139
145
  D.push(defineDetector({
146
+ coverage_requirement: 'evaluated_subset',
140
147
  id: 'CLAIM-007', name: 'Unbounded superlative in page text', namespace: 'CLAIM',
141
148
  description: 'Page text contains a superlative ("best", "industry-leading", "#1") with no registered comparative claim covering that page.',
142
149
  discipline: ['seo', 'aeo', 'geo'], severity: 'medium', deterministic: false, requires: ['site', 'registries'],
@@ -167,6 +174,7 @@ D.push(defineDetector({
167
174
  }));
168
175
 
169
176
  D.push(defineDetector({
177
+ coverage_requirement: 'evaluated_subset',
170
178
  id: 'CLAIM-008', name: 'Published claim missing from page text (surface drift)', namespace: 'CLAIM',
171
179
  description: 'A claim lists a page as a publication surface, but the page no longer contains recognizably similar text.',
172
180
  discipline: ['geo'], severity: 'low', deterministic: false, requires: ['site', 'registries'],
@@ -200,6 +208,7 @@ D.push(defineDetector({
200
208
  }));
201
209
 
202
210
  D.push(defineDetector({
211
+ coverage_requirement: 'evaluated_subset',
203
212
  id: 'CLAIM-009', name: 'Verified claim without semantic support assessment', namespace: 'CLAIM',
204
213
  description: 'A claim holds verified status without a recorded assessment that the evidence semantically supports the claim as worded (entailment, not just linkage). Valid evidence references, dates, and owners do not establish that the wording matches what the evidence proves.',
205
214
  discipline: ['aeo', 'geo'], severity: 'high', deterministic: true, requires: ['registries'],
@@ -6,6 +6,7 @@ import { verifyCrawlerIp } from '../crawler/ipRanges.js';
6
6
  const D = [];
7
7
 
8
8
  D.push(defineDetector({
9
+ coverage_requirement: 'evaluated_subset',
9
10
  id: 'CRAWL-001', name: 'robots.txt contradicts crawler policy decision', namespace: 'CRAWL',
10
11
  description: 'The deployed robots.txt outcome for a crawler differs from the decision recorded in the crawler policy registry.',
11
12
  discipline: ['seo', 'aeo', 'geo'], severity: 'high', deterministic: true, requires: ['site', 'registries'],
@@ -34,6 +35,7 @@ D.push(defineDetector({
34
35
  }));
35
36
 
36
37
  D.push(defineDetector({
38
+ coverage_requirement: 'evaluated_subset',
37
39
  id: 'CRAWL-002', name: 'Training-purpose access not decided separately', namespace: 'CRAWL',
38
40
  description: 'A vendor has an allow decision for search discovery, but no separate recorded decision for its model-training crawler.',
39
41
  discipline: ['geo'], severity: 'medium', deterministic: true, requires: ['registries'],
@@ -68,6 +70,7 @@ D.push(defineDetector({
68
70
  }));
69
71
 
70
72
  D.push(defineDetector({
73
+ coverage_requirement: 'exhaustive_requested_scope',
71
74
  id: 'CRAWL-003', name: 'robots.txt missing', namespace: 'CRAWL',
72
75
  description: 'No robots.txt was found; crawler access is uncontrolled and sitemap discovery is weakened.',
73
76
  discipline: ['seo', 'aeo', 'geo'], severity: 'medium', deterministic: true, requires: ['site'],
@@ -86,6 +89,7 @@ D.push(defineDetector({
86
89
  }));
87
90
 
88
91
  D.push(defineDetector({
92
+ coverage_requirement: 'evaluated_subset',
89
93
  id: 'CRAWL-004', name: 'robots.txt parse problems', namespace: 'CRAWL',
90
94
  description: 'robots.txt contains unparseable or misplaced directives.',
91
95
  discipline: ['seo'], severity: 'medium', deterministic: true, requires: ['site'],
@@ -104,6 +108,7 @@ D.push(defineDetector({
104
108
  }));
105
109
 
106
110
  D.push(defineDetector({
111
+ coverage_requirement: 'evaluated_subset',
107
112
  id: 'CRAWL-005', name: 'Crawler policy review overdue', namespace: 'CRAWL',
108
113
  description: 'A crawler policy decision has passed its next review date.',
109
114
  discipline: ['seo', 'aeo', 'geo'], severity: 'low', deterministic: true, requires: ['registries'],
@@ -123,6 +128,7 @@ D.push(defineDetector({
123
128
  }));
124
129
 
125
130
  D.push(defineDetector({
131
+ coverage_requirement: 'evaluated_subset',
126
132
  id: 'CRAWL-006', name: 'Undecided crawler with observed governance relevance', namespace: 'CRAWL',
127
133
  description: 'A crawler policy entry remains "undecided" — access is currently whatever robots.txt happens to say, not a governed decision.',
128
134
  discipline: ['geo'], severity: 'low', deterministic: true, requires: ['registries'],
@@ -142,6 +148,7 @@ D.push(defineDetector({
142
148
  }));
143
149
 
144
150
  D.push(defineDetector({
151
+ coverage_requirement: 'evaluated_subset',
145
152
  id: 'CRAWL-007', name: 'Spoofed crawler identity in access logs', namespace: 'CRAWL',
146
153
  description: 'An observed crawler log row claims an authoritative bot user-agent (Googlebot, Bingbot, GPTBot, ClaudeBot, etc.) but its source IP is outside published provider ranges or reverse-DNS verification failed.',
147
154
  discipline: ['seo', 'aeo', 'geo'], severity: 'high', deterministic: true, requires: ['site'],
@@ -3,6 +3,7 @@ import { defineDetector, indexTargets, pageSubject, registryPageFor } from './fr
3
3
  const D = [];
4
4
 
5
5
  D.push(defineDetector({
6
+ coverage_requirement: 'evaluated_subset',
6
7
  id: 'CRO-001', name: 'Declared conversion action missing visible interactive CTA', namespace: 'CRO',
7
8
  description: 'A page registry declares a conversion_action (e.g. "request demo", "start trial"), but the rendered page DOM contains no matching interactive CTA button, link, or form.',
8
9
  discipline: ['seo', 'aeo'], severity: 'medium', deterministic: true, requires: ['site', 'registries'],
@@ -54,6 +55,7 @@ D.push(defineDetector({
54
55
  }));
55
56
 
56
57
  D.push(defineDetector({
58
+ coverage_requirement: 'evaluated_subset',
57
59
  id: 'CRO-002', name: 'Friction-heavy or defective lead capture form', namespace: 'CRO',
58
60
  description: 'A form on the page is defective (missing submit mechanism) or contains excessive input fields (>7) on top-of-funnel conversion surfaces.',
59
61
  discipline: ['seo', 'aeo'], severity: 'medium', deterministic: true, requires: ['site'],
@@ -92,6 +94,7 @@ D.push(defineDetector({
92
94
  }));
93
95
 
94
96
  D.push(defineDetector({
97
+ coverage_requirement: 'evaluated_subset',
95
98
  id: 'CRO-003', name: 'Commercial intent page lacks proximate conversion pathway', namespace: 'CRO',
96
99
  description: 'A page with commercial or transactional intent (e.g. product, pricing, or vendor evaluation) contains zero interactive CTAs.',
97
100
  discipline: ['seo', 'aeo'], severity: 'medium', deterministic: true, requires: ['site'],
@@ -134,6 +137,7 @@ D.push(defineDetector({
134
137
  }));
135
138
 
136
139
  D.push(defineDetector({
140
+ coverage_requirement: 'evaluated_subset',
137
141
  id: 'CRO-004', name: 'Defective or unverified outbound conversion target', namespace: 'CRO',
138
142
  description: 'A primary CTA button or link points to a placeholder (#, javascript:void(0)), an unreachable internal destination (404), or a non-production staging/localhost URL.',
139
143
  discipline: ['seo', 'aeo'], severity: 'medium', deterministic: true, requires: ['site'],
@@ -177,6 +181,7 @@ D.push(defineDetector({
177
181
  }));
178
182
 
179
183
  D.push(defineDetector({
184
+ coverage_requirement: 'evaluated_subset',
180
185
  id: 'CRO-005', name: 'Search-to-landing scent gap: primary H1 fails to corroborate title', namespace: 'CRO',
181
186
  description: 'A commercial landing page H1 shares zero substantive keywords with the page title, creating a message-match scent gap that elevates bounce rates for search visitors.',
182
187
  discipline: ['seo', 'aeo'], severity: 'medium', deterministic: true, requires: ['site'],
@@ -218,6 +223,7 @@ D.push(defineDetector({
218
223
  }));
219
224
 
220
225
  D.push(defineDetector({
226
+ coverage_requirement: 'evaluated_subset',
221
227
  id: 'CRO-006', name: 'Commercial lead capture form lacks proximate trust proof or security badges', namespace: 'CRO',
222
228
  description: 'A lead capture or transaction form collects user information without proximate trust signals (compliance certifications, encryption, guarantees, or social proof), elevating funnel abandonment.',
223
229
  discipline: ['seo', 'aeo'], severity: 'low', deterministic: true, requires: ['site'],
@@ -254,6 +260,7 @@ D.push(defineDetector({
254
260
  }));
255
261
 
256
262
  D.push(defineDetector({
263
+ coverage_requirement: 'evaluated_subset',
257
264
  id: 'CRO-007', name: 'Identity or contact input fields lack HTML5 autocomplete attributes', namespace: 'CRO',
258
265
  description: 'Form input fields that collect standard personal identity or contact information (e.g. name, email, phone, organization) lack standard HTML5 autocomplete attributes, increasing typing friction and preventing browser autofill.',
259
266
  discipline: ['seo', 'aeo'], severity: 'low', deterministic: true, requires: ['site'],
@@ -291,6 +298,7 @@ D.push(defineDetector({
291
298
  }));
292
299
 
293
300
  D.push(defineDetector({
301
+ coverage_requirement: 'evaluated_subset',
294
302
  id: 'CRO-008', name: 'Form inputs rely solely on placeholder without accessible label', namespace: 'CRO',
295
303
  description: 'Input fields on conversion forms rely solely on disappearing placeholder text without an associated <label for="...">, <label> wrapper, or aria-label, degrading cognitive accessibility and mobile error recovery.',
296
304
  discipline: ['seo', 'aeo'], severity: 'low', deterministic: true, requires: ['site'],
@@ -326,6 +334,7 @@ D.push(defineDetector({
326
334
  }));
327
335
 
328
336
  D.push(defineDetector({
337
+ coverage_requirement: 'evaluated_subset',
329
338
  id: 'CRO-009', name: 'Mobile input type mismatch for email or telephone fields', namespace: 'CRO',
330
339
  description: 'An email or telephone input uses generic type="text" instead of semantic types (type="email", type="tel") or lacks inputmode="numeric", failing to trigger the appropriate mobile virtual keyboard.',
331
340
  discipline: ['seo', 'aeo'], severity: 'low', deterministic: true, requires: ['site'],
@@ -375,6 +384,7 @@ D.push(defineDetector({
375
384
  }));
376
385
 
377
386
  D.push(defineDetector({
387
+ coverage_requirement: 'evaluated_subset',
378
388
  id: 'CRO-010', name: 'Choice overload: excessive competing primary CTAs in hero conversion zone', namespace: 'CRO',
379
389
  description: 'The hero section or primary viewport of a commercial landing page presents 3 or more competing primary calls-to-action, creating choice paralysis and reducing click-through rates.',
380
390
  discipline: ['seo', 'aeo'], severity: 'medium', deterministic: true, requires: ['site'],
@@ -411,6 +421,7 @@ D.push(defineDetector({
411
421
  }));
412
422
 
413
423
  D.push(defineDetector({
424
+ coverage_requirement: 'evaluated_subset',
414
425
  id: 'CRO-011', name: 'Buried primary conversion pathway on long-form commercial page', namespace: 'CRO',
415
426
  description: 'A long-form commercial landing page (>1,000 words) has zero above-the-fold or early CTAs within the initial hero section and lacks a persistent navigation CTA, forcing visitors to scroll through extensive prose before reaching an action trigger.',
416
427
  discipline: ['seo', 'aeo'], severity: 'medium', deterministic: true, requires: ['site'],
@@ -451,6 +462,7 @@ D.push(defineDetector({
451
462
  }));
452
463
 
453
464
  D.push(defineDetector({
465
+ coverage_requirement: 'evaluated_subset',
454
466
  id: 'CRO-012', name: 'Low-intent generic CTA microcopy on primary commercial action', namespace: 'CRO',
455
467
  description: 'A primary CTA on a commercial landing page uses passive, low-commitment, or non-descriptive microcopy (such as "Submit", "Click Here", "Send", "Next", or "Go") rather than an action-oriented, value-delivering verb.',
456
468
  discipline: ['seo', 'aeo'], severity: 'low', deterministic: true, requires: ['site'],
@@ -490,6 +502,7 @@ D.push(defineDetector({
490
502
  }));
491
503
 
492
504
  D.push(defineDetector({
505
+ coverage_requirement: 'evaluated_subset',
493
506
  id: 'CRO-013', name: 'Enclosed checkout or lead funnel leak: distraction navigation present on conversion step', namespace: 'CRO',
494
507
  description: 'A dedicated checkout, payment, or final lead registration step retains full global navigation menus (>5 links), providing exit ramps that elevate funnel abandonment.',
495
508
  discipline: ['seo', 'aeo'], severity: 'medium', deterministic: true, requires: ['site'],
@@ -520,6 +533,7 @@ D.push(defineDetector({
520
533
  }));
521
534
 
522
535
  D.push(defineDetector({
536
+ coverage_requirement: 'evaluated_subset',
523
537
  id: 'CRO-014', name: 'Unprotected post-conversion confirmation page lacks noindex directive', namespace: 'CRO',
524
538
  description: 'A post-conversion confirmation, thank-you, or receipt page (/thank-you, /order-received, /welcome) lacks a "noindex" robots directive, exposing internal receipt URLs to organic search indexing and distorting conversion tracking.',
525
539
  discipline: ['seo', 'aeo'], severity: 'medium', deterministic: true, requires: ['site'],
@@ -551,6 +565,7 @@ D.push(defineDetector({
551
565
  }));
552
566
 
553
567
  D.push(defineDetector({
568
+ coverage_requirement: 'evaluated_subset',
554
569
  id: 'CRO-015', name: 'Mobile touch target size below recommended 48px threshold', namespace: 'CRO',
555
570
  description: 'An interactive conversion CTA button or link has explicit dimensions under 44px by 44px, violating mobile accessibility and tap-target usability standards (WCAG 2.5.5) and increasing tap error rates.',
556
571
  discipline: ['seo', 'aeo'], severity: 'low', deterministic: true, requires: ['site'],
@@ -583,6 +598,7 @@ D.push(defineDetector({
583
598
  }));
584
599
 
585
600
  D.push(defineDetector({
601
+ coverage_requirement: 'evaluated_subset',
586
602
  id: 'CRO-016', name: 'Diminutive font size on primary conversion CTA microcopy', namespace: 'CRO',
587
603
  description: 'A primary CTA button uses diminutive font sizing (< 12px), creating readability friction and lowering tap confidence for mobile and accessibility-sensitive visitors.',
588
604
  discipline: ['seo', 'aeo'], severity: 'low', deterministic: true, requires: ['site'],
@@ -615,6 +631,7 @@ D.push(defineDetector({
615
631
  }));
616
632
 
617
633
  D.push(defineDetector({
634
+ coverage_requirement: 'evaluated_subset',
618
635
  id: 'CRO-017', name: 'Multi-step conversion funnel break: intermediate step unavailable or defective', namespace: 'CRO',
619
636
  description: 'A multi-step funnel declared in funnels.yaml contains a step whose target page cannot be located in the audited site or returns an HTTP error status (4xx/5xx).',
620
637
  discipline: ['seo', 'aeo'], severity: 'high', deterministic: true, requires: ['site', 'registries'],
@@ -662,6 +679,7 @@ D.push(defineDetector({
662
679
  }));
663
680
 
664
681
  D.push(defineDetector({
682
+ coverage_requirement: 'evaluated_subset',
665
683
  id: 'CRO-018', name: 'Funnel progression link fails to preserve campaign attribution parameters', namespace: 'CRO',
666
684
  description: 'An intermediate CTA link leading to the next funnel step strips or omits declared campaign query parameters (such as utm_source, utm_campaign), breaking multi-touch attribution.',
667
685
  discipline: ['seo', 'aeo'], severity: 'low', deterministic: true, requires: ['site', 'registries'],
@@ -706,6 +724,7 @@ D.push(defineDetector({
706
724
  }));
707
725
 
708
726
  D.push(defineDetector({
727
+ coverage_requirement: 'evaluated_subset',
709
728
  id: 'CRO-019', name: 'AI Answer cited landing page lacks immediate claim corroboration or conversion pathway', namespace: 'CRO',
710
729
  description: 'A landing page cited by an answer engine or generative AI response contains no proximate interactive conversion trigger or fails to corroborate the cited entity claim in its primary visible text.',
711
730
  discipline: ['seo', 'aeo'], severity: 'medium', deterministic: true, requires: ['site'],
@@ -743,6 +762,7 @@ D.push(defineDetector({
743
762
  }));
744
763
 
745
764
  D.push(defineDetector({
765
+ coverage_requirement: 'evaluated_subset',
746
766
  id: 'CRO-020', name: 'Statistical underpower risk on low-traffic A/B experiment', namespace: 'CRO',
747
767
  description: 'An A/B experiment registered in experiments.yaml specifies a minimum observation window or sample size insufficient to detect the expected hypothesis effect at standard statistical power (80%).',
748
768
  discipline: ['seo', 'aeo'], severity: 'low', deterministic: true, requires: ['registries'],
@@ -775,6 +795,7 @@ D.push(defineDetector({
775
795
  }));
776
796
 
777
797
  D.push(defineDetector({
798
+ coverage_requirement: 'evaluated_subset',
778
799
  id: 'CRO-021', name: 'High-intent conversion step lacks express payment wallet options', namespace: 'CRO',
779
800
  version: 2,
780
801
  description: 'A high-intent conversion step (checkout, pricing, cart, subscription) contains traditional payment forms or checkout actions but provides no express payment wallet buttons (Apple Pay, Google Pay, PayPal Express). Payment wallets and WebAuthn passkey authentication are distinct capabilities: this detector reports payment-wallet readiness only; passkey/WebAuthn authentication readiness is reported separately by inspect cro as an informational index.',
@@ -19,6 +19,7 @@ import { defineDetector, indexTargets, pageSubject } from './framework.js';
19
19
  * Checks for common LCP blockers. Cannot measure actual LCP.
20
20
  */
21
21
  export const CWV_001 = defineDetector({
22
+ coverage_requirement: 'page_resource',
22
23
  id: 'CWV-001',
23
24
  name: 'LCP potential blockers',
24
25
  namespace: 'CWV',
@@ -77,6 +78,7 @@ export const CWV_001 = defineDetector({
77
78
  * CWV-002: Preconnect hints for critical origins
78
79
  */
79
80
  export const CWV_002 = defineDetector({
81
+ coverage_requirement: 'page_resource',
80
82
  id: 'CWV-002',
81
83
  name: 'preconnect hints',
82
84
  namespace: 'CWV',
@@ -111,6 +113,7 @@ export const CWV_002 = defineDetector({
111
113
  * CWV-003: Image optimization for LCP
112
114
  */
113
115
  export const CWV_003 = defineDetector({
116
+ coverage_requirement: 'page_resource',
114
117
  id: 'CWV-003',
115
118
  name: 'image optimization',
116
119
  namespace: 'CWV',
@@ -165,6 +168,7 @@ export const CWV_003 = defineDetector({
165
168
  });
166
169
 
167
170
  export const CWV_004 = defineDetector({
171
+ coverage_requirement: 'page_resource',
168
172
  id: 'CWV-004',
169
173
  name: 'Excessive DOM size and depth',
170
174
  namespace: 'CWV',
@@ -12,6 +12,7 @@ function allJsonLdBlocks(site) {
12
12
  }
13
13
 
14
14
  D.push(defineDetector({
15
+ coverage_requirement: 'evaluated_subset',
15
16
  id: 'ENTITY-001', name: 'Entity missing canonical URL', namespace: 'ENTITY',
16
17
  description: 'A registry entity has no canonical URL, so engines have no authoritative page to resolve it to.',
17
18
  discipline: ['geo', 'aeo'], severity: 'medium', deterministic: true, requires: ['registries'],
@@ -31,6 +32,7 @@ D.push(defineDetector({
31
32
  }));
32
33
 
33
34
  D.push(defineDetector({
35
+ coverage_requirement: 'evaluated_subset',
34
36
  id: 'ENTITY-002', name: 'Schema entity name conflicts with registry', namespace: 'ENTITY',
35
37
  description: 'A JSON-LD Organization/Product name does not match the registry canonical name or a documented alias.',
36
38
  discipline: ['geo', 'seo'], severity: 'high', deterministic: true, requires: ['site', 'registries'],
@@ -64,6 +66,7 @@ D.push(defineDetector({
64
66
  }));
65
67
 
66
68
  D.push(defineDetector({
69
+ coverage_requirement: 'evaluated_subset',
67
70
  id: 'ENTITY-003', name: 'Unstable @id for same entity', namespace: 'ENTITY',
68
71
  description: 'The same entity name appears in JSON-LD with different @id values across pages.',
69
72
  discipline: ['geo', 'seo'], severity: 'medium', deterministic: true, requires: ['site'],
@@ -90,6 +93,7 @@ D.push(defineDetector({
90
93
  }));
91
94
 
92
95
  D.push(defineDetector({
96
+ coverage_requirement: 'evaluated_subset',
93
97
  id: 'ENTITY-004', name: 'sameAs profile not in authoritative list', namespace: 'ENTITY',
94
98
  description: 'JSON-LD sameAs references a profile URL not recorded as an authoritative profile in the entity registry.',
95
99
  discipline: ['geo'], severity: 'low', deterministic: true, requires: ['site', 'registries'],
@@ -121,6 +125,7 @@ D.push(defineDetector({
121
125
  }));
122
126
 
123
127
  D.push(defineDetector({
128
+ coverage_requirement: 'evaluated_subset',
124
129
  id: 'ENTITY-005', name: 'Product entity without owner relationship', namespace: 'ENTITY',
125
130
  description: 'A product/product-family entity records no owning organization.',
126
131
  discipline: ['geo'], severity: 'medium', deterministic: true, requires: ['registries'],
@@ -141,6 +146,7 @@ D.push(defineDetector({
141
146
  }));
142
147
 
143
148
  D.push(defineDetector({
149
+ coverage_requirement: 'evaluated_subset',
144
150
  id: 'ENTITY-006', name: 'Unregistered name variant in page text', namespace: 'ENTITY',
145
151
  description: 'Page text uses a variant of an entity name that is neither the canonical name nor a documented alias.',
146
152
  discipline: ['geo', 'seo'], severity: 'low', deterministic: false, requires: ['site', 'registries'],
@@ -178,6 +184,7 @@ D.push(defineDetector({
178
184
  }));
179
185
 
180
186
  D.push(defineDetector({
187
+ coverage_requirement: 'evaluated_subset',
181
188
  id: 'ENTITY-007', name: 'Entity verification stale', namespace: 'ENTITY',
182
189
  description: 'An active entity has not been verified within the last 12 months (or last_verified missing).',
183
190
  discipline: ['geo'], severity: 'low', deterministic: true, requires: ['registries'],
@@ -6,6 +6,7 @@ import { isPastDate } from '../shared/io.js';
6
6
  const D = [];
7
7
 
8
8
  D.push(defineDetector({
9
+ coverage_requirement: 'evaluated_subset',
9
10
  id: 'EVD-001', name: 'Evidence past validity date', namespace: 'EVD',
10
11
  description: 'An evidence entry has passed valid_until but its verification_status is not stale/revoked.',
11
12
  discipline: ['aeo', 'geo'], severity: 'medium', deterministic: true, requires: ['registries'],
@@ -25,6 +26,7 @@ D.push(defineDetector({
25
26
  }));
26
27
 
27
28
  D.push(defineDetector({
29
+ coverage_requirement: 'evaluated_subset',
28
30
  id: 'EVD-002', name: 'Benchmark/test evidence missing methodology', namespace: 'EVD',
29
31
  description: 'Benchmark, test-result, or dataset evidence records no methodology or measurement period.',
30
32
  discipline: ['aeo', 'geo'], severity: 'medium', deterministic: true, requires: ['registries'],
@@ -45,6 +47,7 @@ D.push(defineDetector({
45
47
  }));
46
48
 
47
49
  D.push(defineDetector({
50
+ coverage_requirement: 'evaluated_subset',
48
51
  id: 'EVD-003', name: 'Claim references dangling or unverified evidence', namespace: 'EVD',
49
52
  description: 'A claim references an evidence id that does not exist or has verification_status unverified.',
50
53
  discipline: ['aeo', 'geo'], severity: 'high', deterministic: true, requires: ['registries'],
@@ -78,6 +81,7 @@ D.push(defineDetector({
78
81
  }));
79
82
 
80
83
  D.push(defineDetector({
84
+ coverage_requirement: 'evaluated_subset',
81
85
  id: 'EVD-004', name: 'Secondary evidence where primary is required', namespace: 'EVD',
82
86
  description: 'A performance or security claim is supported only by secondary evidence (e.g., third-party articles).',
83
87
  discipline: ['aeo', 'geo'], severity: 'medium', deterministic: true, requires: ['registries'],
@@ -103,6 +107,7 @@ D.push(defineDetector({
103
107
  }));
104
108
 
105
109
  D.push(defineDetector({
110
+ coverage_requirement: 'evaluated_subset',
106
111
  id: 'EVD-005', name: 'Customer evidence lacks attribution', namespace: 'EVD',
107
112
  description: 'Customer-reference or case-study evidence has no attribution recorded.',
108
113
  discipline: ['aeo', 'geo'], severity: 'low', deterministic: true, requires: ['registries'],
@@ -122,6 +127,7 @@ D.push(defineDetector({
122
127
  }));
123
128
 
124
129
  D.push(defineDetector({
130
+ coverage_requirement: 'evaluated_subset',
125
131
  id: 'EVD-006', name: 'Evidence integrity hash absent', namespace: 'EVD',
126
132
  description: 'Primary evidence (test results, benchmarks, datasets, certifications) has no integrity hash, so later tampering or drift cannot be detected.',
127
133
  discipline: ['aeo', 'geo'], severity: 'low', deterministic: true, requires: ['registries'],
@@ -141,6 +147,7 @@ D.push(defineDetector({
141
147
  }));
142
148
 
143
149
  D.push(defineDetector({
150
+ coverage_requirement: 'evaluated_subset',
144
151
  id: 'EVD-007', name: 'Evidence marked inaccessible still supporting claims', namespace: 'EVD',
145
152
  description: 'Evidence with access_status inaccessible (or verification_status inaccessible) is still listed as supporting active claims.',
146
153
  discipline: ['aeo', 'geo'], severity: 'medium', deterministic: true, requires: ['registries'],
@@ -164,6 +171,7 @@ D.push(defineDetector({
164
171
  }));
165
172
 
166
173
  D.push(defineDetector({
174
+ coverage_requirement: 'evaluated_subset',
167
175
  id: 'EVD-008', name: 'Dangling media reference in claim evidence', namespace: 'EVD',
168
176
  description: 'Claim evidence references a media asset (image, chart, diagram, or video) whose referenced source location or file is missing, unresolved, or unreachable.',
169
177
  discipline: ['aeo', 'geo'], severity: 'high', deterministic: true, requires: ['registries'],
@@ -227,6 +235,7 @@ D.push(defineDetector({
227
235
  }));
228
236
 
229
237
  D.push(defineDetector({
238
+ coverage_requirement: 'evaluated_subset',
230
239
  id: 'EVD-009', name: 'Unanchored PDF claim citation', namespace: 'EVD',
231
240
  description: 'Evidence backed by a PDF document lacks a specific page, fragment, or section anchor (#page=, section, or page number in methodology/test_conditions), leaving cited claims unanchored and unfalsifiable.',
232
241
  discipline: ['aeo', 'geo'], severity: 'low', deterministic: true, requires: ['registries'],
@@ -1,6 +1,7 @@
1
1
  import { sha256 } from '../shared/io.js';
2
2
  import { fileURLToPath } from 'node:url';
3
3
  import { readJson } from '../shared/io.js';
4
+ import { evaluateRequirement, isCoverageRequirement, requirementForDetector } from '../evidence/determination.js';
4
5
 
5
6
  const TOOL_VERSION = readJson(new URL('../../package.json', import.meta.url)).version;
6
7
 
@@ -22,6 +23,9 @@ export function defineDetector(def) {
22
23
  }
23
24
  if (!NAMESPACES.includes(def.namespace)) throw new Error(`detector ${def.id}: unknown namespace ${def.namespace}`);
24
25
  if (!def.id.startsWith(def.namespace + '-')) throw new Error(`detector id ${def.id} must be prefixed with namespace`);
26
+ if (!isCoverageRequirement(def.coverage_requirement)) {
27
+ throw new Error(`detector ${def.id ?? '?'} must explicitly declare a valid coverage_requirement`);
28
+ }
25
29
  return {
26
30
  version: 1,
27
31
  confidence_model: def.deterministic ? 'binary condition; confirmed when observed' : 'heuristic; confidence reported per finding',
@@ -65,7 +69,17 @@ export function runDetectors(detectors, ctx) {
65
69
  detectorsRun.push(d.id);
66
70
  for (const hit of hits) {
67
71
  const idSeed = `${d.id}|${hit.subject?.identifier ?? ''}|${hit.summary}`;
72
+ const coverageRequirement = requirementForDetector(d, hit);
73
+ const scope = ctx.coverage
74
+ ? evaluateRequirement(coverageRequirement, ctx.coverage, hit.subject)
75
+ : null;
76
+ const resourceIds = scope?.resource_id ? [scope.resource_id] : [];
77
+ const scopeSatisfied = scope?.status === 'supported' || scope?.status === 'qualified';
78
+ const determinationNote = scope && !scopeSatisfied
79
+ ? `determination not established: ${scope.reason || 'coverage requirement unsatisfied'}`
80
+ : null;
68
81
  findings.push({
82
+ ...(ctx.coverage ? { schema_version: 2 } : {}),
69
83
  finding_id: `F-${sha256(idSeed).slice(0, 12)}`,
70
84
  detector_id: d.id,
71
85
  detector_name: d.name,
@@ -78,11 +92,12 @@ export function runDetectors(detectors, ctx) {
78
92
  evidence: hit.evidence,
79
93
  captured_value: hit.captured ?? null,
80
94
  expected_value: hit.expected ?? null,
95
+ ...(scope ? { determination_status: scope.status, ...(determinationNote ? { determination_reason: determinationNote } : {}) } : {}),
81
96
  },
82
97
  classification: {
83
98
  finding_type: hit.finding_type ?? d.finding_type,
84
99
  severity: hit.severity ?? d.severity,
85
- confidence: hit.confidence ?? d.confidence,
100
+ confidence: scope && !scopeSatisfied ? 'unknown' : (hit.confidence ?? d.confidence),
86
101
  deterministic: d.deterministic,
87
102
  impact: { ...d.impact, ...(hit.impact || {}) },
88
103
  },
@@ -90,7 +105,10 @@ export function runDetectors(detectors, ctx) {
90
105
  applicable_requirement: d.applicable_requirement,
91
106
  explanation: d.description,
92
107
  assumptions: hit.assumptions ?? [],
93
- limitations: d.deterministic ? [] : ['heuristic detection; verify manually before acting'],
108
+ limitations: [
109
+ ...(d.deterministic ? [] : ['heuristic detection; verify manually before acting']),
110
+ ...(determinationNote ? [determinationNote] : []),
111
+ ],
94
112
  },
95
113
  remediation: {
96
114
  preferred: d.remediation,
@@ -118,6 +136,14 @@ export function runDetectors(detectors, ctx) {
118
136
  : 'heuristic judgment; human semantic review required before acting',
119
137
  revalidation_required: d.deterministic ? 'on_next_audit' : 'human_review_before_action',
120
138
  },
139
+ ...(ctx.coverage ? {
140
+ evidence_scope: {
141
+ requirement: coverageRequirement,
142
+ satisfaction: scope.status,
143
+ resource_ids: resourceIds,
144
+ coverage_ref: 'coverage.json',
145
+ },
146
+ } : {}),
121
147
  status: { state: 'open', first_seen: ts, last_seen: ts, resolved_at: null },
122
148
  });
123
149
  }