@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.
- package/CHANGELOG.md +18 -0
- package/README.md +12 -1
- package/dist/universal/.agents/skills/citable/SKILL.md +11 -2
- package/dist/universal/.agents/skills/citable/VERSION +1 -1
- package/dist/universal/.agents/skills/citable/manifest.json +15 -12
- package/dist/universal/.agents/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.agents/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.agents/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.agents/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.agents/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.agents/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.agents/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.agents/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.agents/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.agents/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/.claude/agents/citable/manifest.json +2 -2
- package/dist/universal/.claude/skills/citable/SKILL.md +11 -2
- package/dist/universal/.claude/skills/citable/VERSION +1 -1
- package/dist/universal/.claude/skills/citable/manifest.json +15 -12
- package/dist/universal/.claude/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.claude/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.claude/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.claude/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.claude/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.claude/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.claude/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.claude/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.claude/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.claude/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/.cursor/skills/citable/SKILL.md +11 -2
- package/dist/universal/.cursor/skills/citable/VERSION +1 -1
- package/dist/universal/.cursor/skills/citable/manifest.json +15 -12
- package/dist/universal/.cursor/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.cursor/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.cursor/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.cursor/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.cursor/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.cursor/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.cursor/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.cursor/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.cursor/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.cursor/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/.gemini/skills/citable/SKILL.md +11 -2
- package/dist/universal/.gemini/skills/citable/VERSION +1 -1
- package/dist/universal/.gemini/skills/citable/manifest.json +15 -12
- package/dist/universal/.gemini/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.gemini/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.gemini/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.gemini/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.gemini/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.gemini/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.gemini/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.gemini/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.gemini/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.gemini/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/.github/skills/citable/SKILL.md +11 -2
- package/dist/universal/.github/skills/citable/VERSION +1 -1
- package/dist/universal/.github/skills/citable/manifest.json +15 -12
- package/dist/universal/.github/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.github/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.github/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.github/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.github/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.github/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.github/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.github/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.github/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.github/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/.kiro/skills/citable/SKILL.md +11 -2
- package/dist/universal/.kiro/skills/citable/VERSION +1 -1
- package/dist/universal/.kiro/skills/citable/manifest.json +15 -12
- package/dist/universal/.kiro/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.kiro/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.kiro/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.kiro/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.kiro/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.kiro/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.kiro/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.kiro/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.kiro/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.kiro/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/.opencode/skills/citable/SKILL.md +11 -2
- package/dist/universal/.opencode/skills/citable/VERSION +1 -1
- package/dist/universal/.opencode/skills/citable/manifest.json +15 -12
- package/dist/universal/.opencode/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.opencode/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.opencode/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.opencode/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.opencode/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.opencode/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.opencode/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.opencode/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.opencode/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.opencode/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/.pi/agent/skills/citable/SKILL.md +11 -2
- package/dist/universal/.pi/agent/skills/citable/VERSION +1 -1
- package/dist/universal/.pi/agent/skills/citable/manifest.json +15 -12
- package/dist/universal/.pi/agent/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.pi/agent/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.pi/agent/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.pi/agent/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.pi/agent/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.pi/agent/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.pi/agent/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/.qoder/skills/citable/SKILL.md +11 -2
- package/dist/universal/.qoder/skills/citable/VERSION +1 -1
- package/dist/universal/.qoder/skills/citable/manifest.json +15 -12
- package/dist/universal/.qoder/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.qoder/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.qoder/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.qoder/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.qoder/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.qoder/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.qoder/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.qoder/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.qoder/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.qoder/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/.rovodev/skills/citable/SKILL.md +11 -2
- package/dist/universal/.rovodev/skills/citable/VERSION +1 -1
- package/dist/universal/.rovodev/skills/citable/manifest.json +15 -12
- package/dist/universal/.rovodev/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.rovodev/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.rovodev/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.rovodev/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.rovodev/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.rovodev/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.rovodev/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.rovodev/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.rovodev/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.rovodev/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/.trae/skills/citable/SKILL.md +11 -2
- package/dist/universal/.trae/skills/citable/VERSION +1 -1
- package/dist/universal/.trae/skills/citable/manifest.json +15 -12
- package/dist/universal/.trae/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.trae/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.trae/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.trae/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.trae/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.trae/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.trae/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.trae/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.trae/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.trae/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/.trae-cn/skills/citable/SKILL.md +11 -2
- package/dist/universal/.trae-cn/skills/citable/VERSION +1 -1
- package/dist/universal/.trae-cn/skills/citable/manifest.json +15 -12
- package/dist/universal/.trae-cn/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.trae-cn/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.trae-cn/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.trae-cn/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.trae-cn/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.trae-cn/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.trae-cn/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/manifest.json +184 -148
- package/package.json +1 -1
- package/schemas/action-plan.schema.json +38 -1
- package/schemas/audit-coverage.schema.json +118 -0
- package/schemas/collection-result.schema.json +35 -0
- package/schemas/config.schema.json +2 -0
- package/schemas/cro-report.schema.json +8 -1
- package/schemas/finding.schema.json +105 -2
- package/schemas/remediation-verification.schema.json +12 -1
- package/schemas/run.schema.json +15 -2
- package/schemas/search-report.schema.json +8 -1
- package/schemas/summary.schema.json +41 -0
- package/skill/SKILL.md +11 -2
- package/src/acceptance/reproducibility.js +5 -25
- package/src/analysis/croRoadmap.js +3 -3
- package/src/analysis/strategicRoadmap.js +3 -3
- package/src/artifacts/interchange.js +2 -6
- package/src/cli/index.js +15 -3
- package/src/commands/actionPlan.js +46 -5
- package/src/commands/applyRemediation.js +5 -5
- package/src/commands/audit.js +75 -8
- package/src/commands/ciWorkflow.js +7 -2
- package/src/commands/compareSnapshots.js +155 -47
- package/src/commands/connect.js +24 -4
- package/src/commands/context.js +41 -2
- package/src/commands/croSuite.js +51 -13
- package/src/commands/delivery.js +7 -7
- package/src/commands/governance.js +9 -10
- package/src/commands/implementationKit.js +2 -1
- package/src/commands/init.js +8 -1
- package/src/commands/previewCro.js +1 -1
- package/src/commands/prioritize.js +3 -2
- package/src/commands/reviews.js +31 -5
- package/src/commands/roadmapCmd.js +7 -13
- package/src/commands/sweep.js +15 -2
- package/src/commands/verifyRemediation.js +94 -19
- package/src/connectors/collectionResult.js +80 -0
- package/src/connectors/ga4.js +46 -11
- package/src/connectors/gsc.js +41 -6
- package/src/connectors/index.js +1 -1
- package/src/connectors/webflow.js +140 -10
- package/src/connectors/wordpress.js +92 -8
- package/src/crawler/fetch.js +220 -24
- package/src/crawler/resourceValidity.js +119 -0
- package/src/crawler/sitemap.js +435 -16
- package/src/crawler/sitemapCollector.js +364 -0
- package/src/crawler/urlIdentity.js +97 -0
- package/src/detectors/agent.js +11 -0
- package/src/detectors/ans.js +16 -1
- package/src/detectors/arch.js +6 -0
- package/src/detectors/claim.js +9 -0
- package/src/detectors/crawl.js +7 -0
- package/src/detectors/cro.js +21 -0
- package/src/detectors/cwv.js +4 -0
- package/src/detectors/entity.js +7 -0
- package/src/detectors/evd.js +9 -0
- package/src/detectors/framework.js +28 -2
- package/src/detectors/geoReco.js +16 -0
- package/src/detectors/hreflang.js +3 -0
- package/src/detectors/lifeMeas.js +9 -0
- package/src/detectors/link.js +6 -1
- package/src/detectors/page.js +11 -0
- package/src/detectors/schemaData.js +16 -1
- package/src/detectors/tech.js +34 -3
- package/src/evidence/coverage.js +448 -0
- package/src/evidence/determination.js +157 -0
- package/src/evidence/downstream.js +110 -0
- package/src/evidence/hashes.js +109 -0
- package/src/evidence/run.js +68 -8
- package/src/extractor/page.js +29 -2
- package/src/extractor/site.js +298 -50
- package/src/reporting/executiveCroReport.js +36 -13
- package/src/reporting/executiveExport.js +45 -16
- package/src/reporting/executiveSearchReport.js +56 -22
- package/src/reporting/report.js +56 -9
- package/src/shared/epistemicLanguage.js +86 -0
- package/src/shared/evidenceSourceResolver.js +48 -17
- package/src/shared/htmlEscape.js +8 -4
- package/src/shared/runPackageVerifier.js +99 -8
- package/src/shared/verifiedRunLoader.js +129 -0
package/src/detectors/arch.js
CHANGED
|
@@ -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'],
|
package/src/detectors/claim.js
CHANGED
|
@@ -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'],
|
package/src/detectors/crawl.js
CHANGED
|
@@ -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'],
|
package/src/detectors/cro.js
CHANGED
|
@@ -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.',
|
package/src/detectors/cwv.js
CHANGED
|
@@ -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',
|
package/src/detectors/entity.js
CHANGED
|
@@ -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'],
|
package/src/detectors/evd.js
CHANGED
|
@@ -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:
|
|
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
|
}
|