@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
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import { gunzipSync } from 'node:zlib';
|
|
2
|
+
import { fetchUrl } from './fetch.js';
|
|
3
|
+
import { parseSitemap } from './sitemap.js';
|
|
4
|
+
|
|
5
|
+
const DEFAULT_MAX_DEPTH = 4;
|
|
6
|
+
const DEFAULT_MAX_DOCUMENTS = 1000;
|
|
7
|
+
const DEFAULT_MAX_UNCOMPRESSED_BYTES = 20 * 1024 * 1024;
|
|
8
|
+
const DEFAULT_MAX_TOTAL_UNCOMPRESSED_BYTES = 100 * 1024 * 1024;
|
|
9
|
+
const DEFAULT_MAX_DISCOVERED_URLS = 50_000;
|
|
10
|
+
const DEFAULT_MAX_QUEUED_DOCUMENTS = 1000;
|
|
11
|
+
const DEFAULT_MAX_RAW_ENTRIES = 100_000;
|
|
12
|
+
const DEFAULT_MAX_XML_TOKENS = 500_000;
|
|
13
|
+
const DEFAULT_SITEMAP_MAX_BYTES = 5 * 1024 * 1024;
|
|
14
|
+
|
|
15
|
+
/** Collect a same-origin sitemap graph with deterministic breadth-first traversal. */
|
|
16
|
+
export async function collectSitemapTopology(entryUrls, {
|
|
17
|
+
fetcher = fetchUrl,
|
|
18
|
+
maxDepth = DEFAULT_MAX_DEPTH,
|
|
19
|
+
maxDocuments = DEFAULT_MAX_DOCUMENTS,
|
|
20
|
+
maxUncompressedBytes = DEFAULT_MAX_UNCOMPRESSED_BYTES,
|
|
21
|
+
maxTotalUncompressedBytes = DEFAULT_MAX_TOTAL_UNCOMPRESSED_BYTES,
|
|
22
|
+
maxDiscoveredUrls = DEFAULT_MAX_DISCOVERED_URLS,
|
|
23
|
+
maxQueuedDocuments = DEFAULT_MAX_QUEUED_DOCUMENTS,
|
|
24
|
+
maxRawEntries = DEFAULT_MAX_RAW_ENTRIES,
|
|
25
|
+
maxXmlTokens = DEFAULT_MAX_XML_TOKENS,
|
|
26
|
+
sitemapMaxBytes = DEFAULT_SITEMAP_MAX_BYTES,
|
|
27
|
+
origin,
|
|
28
|
+
userAgent,
|
|
29
|
+
shouldStop = () => false,
|
|
30
|
+
} = {}) {
|
|
31
|
+
assertInteger(maxDepth, 'maxDepth', 0);
|
|
32
|
+
assertInteger(maxDocuments, 'maxDocuments', 1);
|
|
33
|
+
assertInteger(maxUncompressedBytes, 'maxUncompressedBytes', 1);
|
|
34
|
+
assertInteger(maxTotalUncompressedBytes, 'maxTotalUncompressedBytes', 1);
|
|
35
|
+
assertInteger(maxDiscoveredUrls, 'maxDiscoveredUrls', 1);
|
|
36
|
+
assertInteger(maxQueuedDocuments, 'maxQueuedDocuments', 1);
|
|
37
|
+
assertInteger(maxRawEntries, 'maxRawEntries', 1);
|
|
38
|
+
assertInteger(maxXmlTokens, 'maxXmlTokens', 1);
|
|
39
|
+
assertInteger(sitemapMaxBytes, 'sitemapMaxBytes', 1);
|
|
40
|
+
const entries = Array.isArray(entryUrls) ? entryUrls : [entryUrls];
|
|
41
|
+
const auditedOrigin = new URL(origin ?? entries.find(Boolean)).origin;
|
|
42
|
+
const queue = [];
|
|
43
|
+
const queued = new Set();
|
|
44
|
+
const documents = [];
|
|
45
|
+
const urls = [];
|
|
46
|
+
const seenUrls = new Set();
|
|
47
|
+
const exclusions = [];
|
|
48
|
+
const errors = [];
|
|
49
|
+
const limitations = [];
|
|
50
|
+
const stopReasons = [];
|
|
51
|
+
let totalUncompressedBytes = 0;
|
|
52
|
+
let queuedDocumentsPeak = 0;
|
|
53
|
+
|
|
54
|
+
const addStop = (reason, limitation) => {
|
|
55
|
+
if (!stopReasons.includes(reason)) stopReasons.push(reason);
|
|
56
|
+
if (limitation && !limitations.includes(limitation)) limitations.push(limitation);
|
|
57
|
+
};
|
|
58
|
+
const enqueue = (rawUrl, depth, parentUrl, kind) => {
|
|
59
|
+
let parsed;
|
|
60
|
+
try {
|
|
61
|
+
parsed = new URL(rawUrl, parentUrl ?? auditedOrigin);
|
|
62
|
+
parsed.hash = '';
|
|
63
|
+
} catch {
|
|
64
|
+
exclusions.push({ url: String(rawUrl ?? ''), kind, source_url: parentUrl, reason: 'malformed_url' });
|
|
65
|
+
errors.push({ url: String(rawUrl ?? ''), reason: 'malformed_url' });
|
|
66
|
+
return 'malformed_url';
|
|
67
|
+
}
|
|
68
|
+
if (parsed.origin !== auditedOrigin) {
|
|
69
|
+
exclusions.push({ url: parsed.href, kind, source_url: parentUrl, reason: 'out_of_origin' });
|
|
70
|
+
errors.push({ url: parsed.href, reason: 'out_of_origin' });
|
|
71
|
+
return 'out_of_origin';
|
|
72
|
+
}
|
|
73
|
+
if (queued.has(parsed.href)) return 'duplicate';
|
|
74
|
+
if (queue.length >= maxQueuedDocuments) {
|
|
75
|
+
addStop(
|
|
76
|
+
'max_queued_documents_exceeded',
|
|
77
|
+
`Sitemap topology collection reached the ${maxQueuedDocuments}-document queued-frontier limit.`,
|
|
78
|
+
);
|
|
79
|
+
return 'max_queued_documents_exceeded';
|
|
80
|
+
}
|
|
81
|
+
queued.add(parsed.href);
|
|
82
|
+
queue.push({ url: parsed.href, depth, parentUrl });
|
|
83
|
+
queuedDocumentsPeak = Math.max(queuedDocumentsPeak, queue.length);
|
|
84
|
+
return null;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
88
|
+
if (index >= maxRawEntries) {
|
|
89
|
+
addStop(
|
|
90
|
+
'max_raw_entries_exceeded',
|
|
91
|
+
`Sitemap topology collection reached the ${maxRawEntries}-entry raw-input safety limit.`,
|
|
92
|
+
);
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
enqueue(entries[index], 0, null, 'entry_sitemap');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
collection: while (queue.length) {
|
|
99
|
+
if (shouldStop()) {
|
|
100
|
+
addStop('time_budget_exhausted', 'Sitemap topology collection stopped when the whole-run time budget expired.');
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
if (documents.length >= maxDocuments) {
|
|
104
|
+
addStop('max_documents_exceeded', `Sitemap topology collection reached the ${maxDocuments}-document limit.`);
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
const item = queue.shift();
|
|
108
|
+
if (item.depth > maxDepth) {
|
|
109
|
+
addStop('max_depth_exceeded', `Sitemap topology collection reached the maximum depth of ${maxDepth}.`);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const record = {
|
|
114
|
+
requested_url: item.url,
|
|
115
|
+
effective_url: item.url,
|
|
116
|
+
depth: item.depth,
|
|
117
|
+
parent_url: item.parentUrl,
|
|
118
|
+
http_status: null,
|
|
119
|
+
compression: 'none',
|
|
120
|
+
compression_transport_decoded: false,
|
|
121
|
+
parse_errors: [],
|
|
122
|
+
url_count: 0,
|
|
123
|
+
child_count: 0,
|
|
124
|
+
raw_entry_count: 0,
|
|
125
|
+
status: 'failed',
|
|
126
|
+
failure_reason: null,
|
|
127
|
+
failure_stage: null,
|
|
128
|
+
parsed: null,
|
|
129
|
+
};
|
|
130
|
+
documents.push(record);
|
|
131
|
+
try {
|
|
132
|
+
const response = await fetcher(item.url, {
|
|
133
|
+
userAgent,
|
|
134
|
+
maxBodyBytes: sitemapMaxBytes,
|
|
135
|
+
responseType: 'buffer',
|
|
136
|
+
});
|
|
137
|
+
record.effective_url = response.url ?? item.url;
|
|
138
|
+
record.http_status = response.status ?? null;
|
|
139
|
+
if (new URL(record.effective_url).origin !== auditedOrigin) {
|
|
140
|
+
record.failure_reason = 'effective_url_out_of_origin';
|
|
141
|
+
record.failure_stage = 'policy';
|
|
142
|
+
errors.push({ url: item.url, reason: record.failure_reason });
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (response.status !== 200) {
|
|
146
|
+
record.failure_reason = `http_status_${response.status}`;
|
|
147
|
+
record.failure_stage = 'transport';
|
|
148
|
+
errors.push({ url: item.url, reason: record.failure_reason });
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
record.compression = detectCompression(response.body, response.headers, record.effective_url);
|
|
153
|
+
const decoded = decodeBody(
|
|
154
|
+
response.body, response.headers, record.effective_url, maxUncompressedBytes, record.compression,
|
|
155
|
+
);
|
|
156
|
+
record.compression_transport_decoded = decoded.transportDecoded;
|
|
157
|
+
record.uncompressed_bytes = decoded.uncompressedBytes;
|
|
158
|
+
if (totalUncompressedBytes + decoded.uncompressedBytes > maxTotalUncompressedBytes) {
|
|
159
|
+
markTruncated(record, 'max_total_uncompressed_bytes_exceeded');
|
|
160
|
+
addStop(
|
|
161
|
+
'max_total_uncompressed_bytes_exceeded',
|
|
162
|
+
`Sitemap topology collection exceeded the ${maxTotalUncompressedBytes}-byte cumulative uncompressed limit.`,
|
|
163
|
+
);
|
|
164
|
+
break collection;
|
|
165
|
+
}
|
|
166
|
+
totalUncompressedBytes += decoded.uncompressedBytes;
|
|
167
|
+
const parsed = parseSitemap(decoded.text, {
|
|
168
|
+
maxUrls: maxRawEntries,
|
|
169
|
+
maxChildren: maxRawEntries,
|
|
170
|
+
maxEntries: maxRawEntries,
|
|
171
|
+
maxTokens: maxXmlTokens,
|
|
172
|
+
});
|
|
173
|
+
record.parsed = parsed;
|
|
174
|
+
record.parse_errors = [...parsed.errors];
|
|
175
|
+
record.url_count = parsed.urlCount;
|
|
176
|
+
record.child_count = parsed.childCount;
|
|
177
|
+
record.raw_entry_count = parsed.rawEntryCount;
|
|
178
|
+
record.status = parsed.errors.length ? 'malformed' : 'fetched';
|
|
179
|
+
if (parsed.errors.length) {
|
|
180
|
+
record.failure_reason = 'sitemap_parse_error';
|
|
181
|
+
record.failure_stage = 'parse';
|
|
182
|
+
errors.push({ url: item.url, reason: record.failure_reason, details: [...parsed.errors] });
|
|
183
|
+
}
|
|
184
|
+
if (parsed.truncated) {
|
|
185
|
+
const [reason, limitation] = parsed.truncationReason === 'max_tokens_exceeded'
|
|
186
|
+
? ['max_xml_tokens_exceeded', `Sitemap parsing reached the ${maxXmlTokens}-token structural limit.`]
|
|
187
|
+
: ['max_raw_entries_exceeded', `Sitemap parsing reached the ${maxRawEntries}-entry raw-input safety limit.`];
|
|
188
|
+
markTruncated(record, reason);
|
|
189
|
+
addStop(reason, limitation);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
for (const child of parsed.rootValid ? parsed.children : []) {
|
|
193
|
+
const exclusionReason = enqueue(child.loc, item.depth + 1, record.effective_url, 'child_sitemap');
|
|
194
|
+
if (exclusionReason === 'malformed_url') markMalformedLoc(record, child.loc);
|
|
195
|
+
if (exclusionReason === 'max_queued_documents_exceeded') {
|
|
196
|
+
markTruncated(record, exclusionReason);
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
for (const entry of parsed.rootValid ? parsed.urls : []) {
|
|
201
|
+
let page;
|
|
202
|
+
try {
|
|
203
|
+
page = new URL(entry.loc, record.effective_url);
|
|
204
|
+
page.hash = '';
|
|
205
|
+
} catch {
|
|
206
|
+
markMalformedLoc(record, entry.loc);
|
|
207
|
+
exclusions.push({ url: entry.loc, kind: 'page_url', source_url: record.effective_url, reason: 'malformed_url' });
|
|
208
|
+
errors.push({ url: entry.loc, reason: 'malformed_url' });
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (page.origin !== auditedOrigin) {
|
|
212
|
+
exclusions.push({ url: page.href, kind: 'page_url', source_url: record.effective_url, reason: 'out_of_origin' });
|
|
213
|
+
errors.push({ url: page.href, reason: 'out_of_origin' });
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (seenUrls.has(page.href)) continue;
|
|
217
|
+
if (urls.length >= maxDiscoveredUrls) {
|
|
218
|
+
markTruncated(record, 'max_discovered_urls_exceeded');
|
|
219
|
+
addStop(
|
|
220
|
+
'max_discovered_urls_exceeded',
|
|
221
|
+
`Sitemap topology collection reached the ${maxDiscoveredUrls}-URL discovery limit.`,
|
|
222
|
+
);
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
seenUrls.add(page.href);
|
|
226
|
+
urls.push({ url: page.href, sitemap_url: record.effective_url, requested_sitemap_url: item.url, sitemap_depth: item.depth });
|
|
227
|
+
}
|
|
228
|
+
if (stopReasons.includes('max_discovered_urls_exceeded')) break collection;
|
|
229
|
+
} catch (error) {
|
|
230
|
+
record.failure_reason = classifyFailure(error);
|
|
231
|
+
record.failure_stage = failureStage(record.failure_reason);
|
|
232
|
+
errors.push({ url: item.url, reason: record.failure_reason, message: error.message });
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (shouldStop()) {
|
|
237
|
+
addStop('time_budget_exhausted', 'Sitemap topology collection stopped when the whole-run time budget expired.');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const truncated = stopReasons.length > 0;
|
|
241
|
+
const indeterminate = documents.some((document) => ['failed', 'malformed'].includes(document.status))
|
|
242
|
+
|| errors.some((error) => ['malformed_url', 'out_of_origin'].includes(error.reason));
|
|
243
|
+
return {
|
|
244
|
+
documents,
|
|
245
|
+
urls,
|
|
246
|
+
status: truncated ? 'truncated' : indeterminate ? 'indeterminate' : 'complete',
|
|
247
|
+
limitations,
|
|
248
|
+
errors,
|
|
249
|
+
exclusions,
|
|
250
|
+
stop_reasons: stopReasons,
|
|
251
|
+
bounds: {
|
|
252
|
+
max_depth: maxDepth,
|
|
253
|
+
max_documents: maxDocuments,
|
|
254
|
+
max_uncompressed_bytes: maxUncompressedBytes,
|
|
255
|
+
max_total_uncompressed_bytes: maxTotalUncompressedBytes,
|
|
256
|
+
max_discovered_urls: maxDiscoveredUrls,
|
|
257
|
+
max_queued_documents: maxQueuedDocuments,
|
|
258
|
+
max_raw_entries: maxRawEntries,
|
|
259
|
+
max_xml_tokens: maxXmlTokens,
|
|
260
|
+
sitemap_max_bytes: sitemapMaxBytes,
|
|
261
|
+
},
|
|
262
|
+
totals: {
|
|
263
|
+
documents: documents.length,
|
|
264
|
+
urls: urls.length,
|
|
265
|
+
uncompressed_bytes: totalUncompressedBytes,
|
|
266
|
+
queued_documents_peak: queuedDocumentsPeak,
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function detectCompression(body, headers = {}, url) {
|
|
272
|
+
const bytes = Buffer.isBuffer(body)
|
|
273
|
+
? body
|
|
274
|
+
: body instanceof Uint8Array
|
|
275
|
+
? Buffer.from(body.buffer, body.byteOffset, body.byteLength)
|
|
276
|
+
: Buffer.from(String(body ?? ''), 'utf8');
|
|
277
|
+
const contentEncoding = String(headerValue(headers, 'content-encoding') ?? '').toLowerCase();
|
|
278
|
+
const contentType = String(headerValue(headers, 'content-type') ?? '').toLowerCase();
|
|
279
|
+
const magicGzip = bytes.length >= 2 && bytes[0] === 0x1f && bytes[1] === 0x8b;
|
|
280
|
+
const hintedGzip = magicGzip || /(?:^|\W)gzip(?:\W|$)/.test(contentEncoding)
|
|
281
|
+
|| /(?:application|text)\/(?:x-)?gzip/.test(contentType) || new URL(url).pathname.toLowerCase().endsWith('.gz');
|
|
282
|
+
return hintedGzip ? 'gzip' : 'none';
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function decodeBody(body, headers, url, maxUncompressedBytes, compression = detectCompression(body, headers, url)) {
|
|
286
|
+
const bytes = Buffer.isBuffer(body)
|
|
287
|
+
? body
|
|
288
|
+
: body instanceof Uint8Array
|
|
289
|
+
? Buffer.from(body.buffer, body.byteOffset, body.byteLength)
|
|
290
|
+
: Buffer.from(String(body ?? ''), 'utf8');
|
|
291
|
+
const magicGzip = bytes.length >= 2 && bytes[0] === 0x1f && bytes[1] === 0x8b;
|
|
292
|
+
let decoded = bytes;
|
|
293
|
+
let transportDecoded = false;
|
|
294
|
+
if (compression === 'gzip') {
|
|
295
|
+
if (magicGzip) {
|
|
296
|
+
decoded = gunzipSync(bytes, { maxOutputLength: maxUncompressedBytes });
|
|
297
|
+
} else if (looksLikeDecodedXml(bytes)) {
|
|
298
|
+
transportDecoded = true;
|
|
299
|
+
} else {
|
|
300
|
+
const error = new Error('gzip-indicated sitemap body is neither gzip bytes nor transport-decoded XML');
|
|
301
|
+
error.code = 'SITEMAP_DECOMPRESSION_FAILED';
|
|
302
|
+
throw error;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (decoded.byteLength > maxUncompressedBytes) {
|
|
306
|
+
const error = new Error(`sitemap uncompressed body exceeds ${maxUncompressedBytes} bytes`);
|
|
307
|
+
error.code = 'SITEMAP_UNCOMPRESSED_LIMIT';
|
|
308
|
+
throw error;
|
|
309
|
+
}
|
|
310
|
+
return { text: decoded.toString('utf8'), compression, transportDecoded, uncompressedBytes: decoded.byteLength };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function looksLikeDecodedXml(bytes) {
|
|
314
|
+
return /^\s*</u.test(bytes.toString('utf8').replace(/^\uFEFF/u, ''));
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function headerValue(headers, name) {
|
|
318
|
+
if (headers && typeof headers.get === 'function') return headers.get(name);
|
|
319
|
+
const key = Object.keys(headers ?? {}).find((candidate) => candidate.toLowerCase() === name);
|
|
320
|
+
return key ? headers[key] : undefined;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function classifyFailure(error) {
|
|
324
|
+
if (error?.code === 'FETCH_BODY_LIMIT' || /^response body exceeds \d+ bytes$/i.test(error?.message ?? '')) {
|
|
325
|
+
return 'sitemap_transport_bytes_exceeded';
|
|
326
|
+
}
|
|
327
|
+
if (error?.code === 'SITEMAP_UNCOMPRESSED_LIMIT' || error?.code === 'ERR_BUFFER_TOO_LARGE'
|
|
328
|
+
|| /maxoutputlength|larger than/i.test(error?.message ?? '')) {
|
|
329
|
+
return 'max_uncompressed_bytes_exceeded';
|
|
330
|
+
}
|
|
331
|
+
if (error?.code === 'SITEMAP_DECOMPRESSION_FAILED'
|
|
332
|
+
|| /gzip|incorrect header|invalid distance|unexpected end/i.test(error?.message ?? '')) return 'decompression_failed';
|
|
333
|
+
return 'fetch_failed';
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function markMalformedLoc(record, loc) {
|
|
337
|
+
const message = `entry has malformed <loc>: ${loc}`;
|
|
338
|
+
if (!record.parse_errors.includes(message)) record.parse_errors.push(message);
|
|
339
|
+
record.status = 'malformed';
|
|
340
|
+
record.failure_reason = 'sitemap_parse_error';
|
|
341
|
+
record.failure_stage = 'parse';
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function markTruncated(record, reason) {
|
|
345
|
+
record.status = 'truncated';
|
|
346
|
+
record.failure_reason = reason;
|
|
347
|
+
record.failure_stage = 'collection';
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function failureStage(reason) {
|
|
351
|
+
if (reason === 'sitemap_transport_bytes_exceeded' || reason === 'fetch_failed' || reason.startsWith('http_status_')) {
|
|
352
|
+
return 'transport';
|
|
353
|
+
}
|
|
354
|
+
if (reason === 'max_uncompressed_bytes_exceeded' || reason === 'decompression_failed') return 'decompression';
|
|
355
|
+
if (reason === 'sitemap_parse_error') return 'parse';
|
|
356
|
+
if (reason.includes('out_of_origin')) return 'policy';
|
|
357
|
+
return 'collection';
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function assertInteger(value, name, minimum) {
|
|
361
|
+
if (!Number.isInteger(value) || value < minimum) {
|
|
362
|
+
throw new TypeError(`${name} must be an integer greater than or equal to ${minimum}`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { sha256 } from '../shared/io.js';
|
|
2
|
+
|
|
3
|
+
export const URL_IDENTITY_NORMALIZATION_VERSION = 'url-identity-v1';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* URL identity v1 deliberately performs only two transformations: remove the
|
|
7
|
+
* fragment and remove an explicit default HTTP(S) port. Query ordering,
|
|
8
|
+
* trailing slashes, casing, percent encoding, and host spelling are evidence
|
|
9
|
+
* and are therefore preserved.
|
|
10
|
+
*/
|
|
11
|
+
export function normalizeUrlIdentity(url) {
|
|
12
|
+
if (typeof url !== 'string' || !url) throw new TypeError('URL identity requires a non-empty URL string');
|
|
13
|
+
const fragmentAt = url.indexOf('#');
|
|
14
|
+
const withoutFragment = fragmentAt === -1 ? url : url.slice(0, fragmentAt);
|
|
15
|
+
const match = withoutFragment.match(/^([A-Za-z][A-Za-z\d+.-]*):\/\/([^/?#]*)([\s\S]*)$/);
|
|
16
|
+
if (!match) throw new TypeError(`URL identity requires an absolute URL: ${url}`);
|
|
17
|
+
const [, rawScheme, rawAuthority, suffix] = match;
|
|
18
|
+
const scheme = rawScheme.toLowerCase();
|
|
19
|
+
if (scheme !== 'http' && scheme !== 'https') throw new TypeError(`unsupported URL identity protocol: ${rawScheme}:`);
|
|
20
|
+
if (rawAuthority.includes('@')) throw new TypeError('URL credentials are not permitted');
|
|
21
|
+
|
|
22
|
+
// Parse for validity and policy comparisons, but never serialize with URL:
|
|
23
|
+
// WHATWG serialization would lowercase evidence-bearing scheme/host text.
|
|
24
|
+
const parsed = new URL(withoutFragment);
|
|
25
|
+
if (parsed.username || parsed.password) throw new TypeError('URL credentials are not permitted');
|
|
26
|
+
let authority = rawAuthority;
|
|
27
|
+
const defaultPort = scheme === 'https' ? '443' : '80';
|
|
28
|
+
const portPattern = new RegExp(`:${defaultPort}$`);
|
|
29
|
+
if (portPattern.test(authority)) authority = authority.replace(portPattern, '');
|
|
30
|
+
return `${rawScheme}://${authority}${suffix}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function assertSameOrigin(candidate, allowedOrigin, label) {
|
|
34
|
+
if (new URL(candidate).origin !== allowedOrigin) {
|
|
35
|
+
throw new Error(`${label} origin differs from requested URL origin`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function identityPart(url, baseUrl) {
|
|
40
|
+
if (url == null || url === '') return null;
|
|
41
|
+
const raw = String(url);
|
|
42
|
+
const absolute = baseUrl == null || /^[A-Za-z][A-Za-z\d+.-]*:\/\//.test(raw)
|
|
43
|
+
? raw
|
|
44
|
+
: new URL(raw, baseUrl).href;
|
|
45
|
+
return { url: absolute, normalized_url: normalizeUrlIdentity(absolute) };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function optionalIdentityPart(url, baseUrl) {
|
|
49
|
+
if (url == null || url === '') return null;
|
|
50
|
+
try {
|
|
51
|
+
return identityPart(url, baseUrl);
|
|
52
|
+
} catch {
|
|
53
|
+
// Preserve an invalid declaration for downstream detectors instead of
|
|
54
|
+
// turning URL metadata enrichment into a page extraction failure.
|
|
55
|
+
return { url: String(url), normalized_url: null };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Build a provenance-preserving identity without treating canonical as fetch identity. */
|
|
60
|
+
export function createUrlIdentity({
|
|
61
|
+
requestedUrl,
|
|
62
|
+
effectiveUrl,
|
|
63
|
+
redirectChain = [],
|
|
64
|
+
declaredCanonicalUrl = null,
|
|
65
|
+
} = {}) {
|
|
66
|
+
const requested = identityPart(requestedUrl);
|
|
67
|
+
const effective = identityPart(effectiveUrl);
|
|
68
|
+
if (!requested || !effective) throw new TypeError('requestedUrl and effectiveUrl are required');
|
|
69
|
+
const allowedOrigin = new URL(requested.url).origin;
|
|
70
|
+
assertSameOrigin(effective.url, allowedOrigin, 'effective URL');
|
|
71
|
+
|
|
72
|
+
const redirect = (redirectChain || []).map((step) => {
|
|
73
|
+
const source = identityPart(step.url);
|
|
74
|
+
if (!source) throw new TypeError('redirect URL is required');
|
|
75
|
+
assertSameOrigin(source.url, allowedOrigin, 'redirect URL');
|
|
76
|
+
const destination = step.location == null ? null : identityPart(step.location, source.url);
|
|
77
|
+
if (destination) assertSameOrigin(destination.url, allowedOrigin, 'redirect destination');
|
|
78
|
+
return {
|
|
79
|
+
url: source.url,
|
|
80
|
+
normalized_url: source.normalized_url,
|
|
81
|
+
status: step.status,
|
|
82
|
+
location: step.location ?? null,
|
|
83
|
+
destination_url: destination?.url ?? null,
|
|
84
|
+
normalized_destination_url: destination?.normalized_url ?? null,
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
const canonical = optionalIdentityPart(declaredCanonicalUrl, effective.url);
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
normalization_version: URL_IDENTITY_NORMALIZATION_VERSION,
|
|
91
|
+
requested,
|
|
92
|
+
effective,
|
|
93
|
+
redirect,
|
|
94
|
+
canonical,
|
|
95
|
+
resource_id: `RESOURCE-${sha256(effective.normalized_url).slice(0, 24).toUpperCase()}`,
|
|
96
|
+
};
|
|
97
|
+
}
|
package/src/detectors/agent.js
CHANGED
|
@@ -50,6 +50,7 @@ function siteUrl(ctx) {
|
|
|
50
50
|
// ---------------------------------------------------------------------------
|
|
51
51
|
|
|
52
52
|
export const AGENT_001 = defineDetector({
|
|
53
|
+
coverage_requirement: 'exhaustive_requested_scope',
|
|
53
54
|
id: 'AGENT-001',
|
|
54
55
|
name: 'AI bot rules absent from robots.txt',
|
|
55
56
|
namespace: 'AGENT',
|
|
@@ -94,6 +95,7 @@ export const AGENT_001 = defineDetector({
|
|
|
94
95
|
// ---------------------------------------------------------------------------
|
|
95
96
|
|
|
96
97
|
export const AGENT_002 = defineDetector({
|
|
98
|
+
coverage_requirement: 'evaluated_subset',
|
|
97
99
|
id: 'AGENT-002',
|
|
98
100
|
name: 'Link response headers absent',
|
|
99
101
|
namespace: 'AGENT',
|
|
@@ -131,6 +133,7 @@ export const AGENT_002 = defineDetector({
|
|
|
131
133
|
// ---------------------------------------------------------------------------
|
|
132
134
|
|
|
133
135
|
export const AGENT_003 = defineDetector({
|
|
136
|
+
coverage_requirement: 'exhaustive_requested_scope',
|
|
134
137
|
id: 'AGENT-003',
|
|
135
138
|
name: 'llms.txt missing',
|
|
136
139
|
namespace: 'AGENT',
|
|
@@ -169,6 +172,7 @@ export const AGENT_003 = defineDetector({
|
|
|
169
172
|
// ---------------------------------------------------------------------------
|
|
170
173
|
|
|
171
174
|
export const AGENT_004 = defineDetector({
|
|
175
|
+
coverage_requirement: 'exhaustive_requested_scope',
|
|
172
176
|
id: 'AGENT-004',
|
|
173
177
|
name: 'MCP Server Card not discoverable',
|
|
174
178
|
namespace: 'AGENT',
|
|
@@ -215,6 +219,7 @@ export const AGENT_004 = defineDetector({
|
|
|
215
219
|
// ---------------------------------------------------------------------------
|
|
216
220
|
|
|
217
221
|
export const AGENT_005 = defineDetector({
|
|
222
|
+
coverage_requirement: 'exhaustive_requested_scope',
|
|
218
223
|
id: 'AGENT-005',
|
|
219
224
|
name: 'A2A Agent Card not discoverable',
|
|
220
225
|
namespace: 'AGENT',
|
|
@@ -258,6 +263,7 @@ export const AGENT_005 = defineDetector({
|
|
|
258
263
|
// ---------------------------------------------------------------------------
|
|
259
264
|
|
|
260
265
|
export const AGENT_006 = defineDetector({
|
|
266
|
+
coverage_requirement: 'evaluated_subset',
|
|
261
267
|
id: 'AGENT-006',
|
|
262
268
|
name: 'Markdown content negotiation not supported',
|
|
263
269
|
namespace: 'AGENT',
|
|
@@ -302,6 +308,7 @@ export const AGENT_006 = defineDetector({
|
|
|
302
308
|
// ---------------------------------------------------------------------------
|
|
303
309
|
|
|
304
310
|
export const AGENT_007 = defineDetector({
|
|
311
|
+
coverage_requirement: 'evaluated_subset',
|
|
305
312
|
id: 'AGENT-007',
|
|
306
313
|
name: 'Web Bot Auth not declared',
|
|
307
314
|
namespace: 'AGENT',
|
|
@@ -345,6 +352,7 @@ export const AGENT_007 = defineDetector({
|
|
|
345
352
|
// ---------------------------------------------------------------------------
|
|
346
353
|
|
|
347
354
|
export const AGENT_008 = defineDetector({
|
|
355
|
+
coverage_requirement: 'evaluated_subset',
|
|
348
356
|
id: 'AGENT-008',
|
|
349
357
|
name: 'Content-Signals header absent',
|
|
350
358
|
namespace: 'AGENT',
|
|
@@ -386,6 +394,7 @@ export const AGENT_008 = defineDetector({
|
|
|
386
394
|
// ---------------------------------------------------------------------------
|
|
387
395
|
|
|
388
396
|
export const AGENT_009 = defineDetector({
|
|
397
|
+
coverage_requirement: 'evaluated_subset',
|
|
389
398
|
id: 'AGENT-009',
|
|
390
399
|
name: 'auth.md not present',
|
|
391
400
|
namespace: 'AGENT',
|
|
@@ -431,6 +440,7 @@ export const AGENT_009 = defineDetector({
|
|
|
431
440
|
// ---------------------------------------------------------------------------
|
|
432
441
|
|
|
433
442
|
export const AGENT_010 = defineDetector({
|
|
443
|
+
coverage_requirement: 'evaluated_subset',
|
|
434
444
|
id: 'AGENT-010',
|
|
435
445
|
name: 'Agentic commerce protocols not declared',
|
|
436
446
|
namespace: 'AGENT',
|
|
@@ -488,6 +498,7 @@ export const AGENT_010 = defineDetector({
|
|
|
488
498
|
// ---------------------------------------------------------------------------
|
|
489
499
|
|
|
490
500
|
export const AGENT_011 = defineDetector({
|
|
501
|
+
coverage_requirement: 'evaluated_subset',
|
|
491
502
|
id: 'AGENT-011',
|
|
492
503
|
name: 'llms.txt structure or link integrity broken',
|
|
493
504
|
namespace: 'AGENT',
|
package/src/detectors/ans.js
CHANGED
|
@@ -6,6 +6,7 @@ const D = [];
|
|
|
6
6
|
const PREAMBLE_RX = /\b(in today'?s (rapidly |ever[- ])?(evolving|changing)|in the (modern|current|digital) (era|age|landscape|world)|now more than ever|as (technology|ai) continues to (evolve|advance)|in an increasingly)\b/i;
|
|
7
7
|
|
|
8
8
|
D.push(defineDetector({
|
|
9
|
+
coverage_requirement: 'evaluated_subset',
|
|
9
10
|
id: 'ANS-001', name: 'Generic preamble before answer', namespace: 'ANS',
|
|
10
11
|
description: 'The opening paragraph uses generic scene-setting boilerplate instead of answering the page’s question.',
|
|
11
12
|
discipline: ['aeo', 'geo'], severity: 'medium', deterministic: false, requires: ['site'],
|
|
@@ -26,6 +27,7 @@ D.push(defineDetector({
|
|
|
26
27
|
}));
|
|
27
28
|
|
|
28
29
|
D.push(defineDetector({
|
|
30
|
+
coverage_requirement: 'evaluated_subset',
|
|
29
31
|
id: 'ANS-002', name: 'Question heading without direct answer', namespace: 'ANS',
|
|
30
32
|
description: 'A heading phrased as a question is not followed by prose before the next heading.',
|
|
31
33
|
discipline: ['aeo'], severity: 'medium', deterministic: true, requires: ['site'],
|
|
@@ -53,6 +55,7 @@ D.push(defineDetector({
|
|
|
53
55
|
}));
|
|
54
56
|
|
|
55
57
|
D.push(defineDetector({
|
|
58
|
+
coverage_requirement: 'evaluated_subset',
|
|
56
59
|
id: 'ANS-003', name: 'Circular definition', namespace: 'ANS',
|
|
57
60
|
description: 'A definitional sentence defines a term using the term itself as the definition head.',
|
|
58
61
|
discipline: ['aeo', 'geo'], severity: 'medium', deterministic: false, requires: ['site'],
|
|
@@ -85,6 +88,7 @@ D.push(defineDetector({
|
|
|
85
88
|
}));
|
|
86
89
|
|
|
87
90
|
D.push(defineDetector({
|
|
91
|
+
coverage_requirement: 'evaluated_subset',
|
|
88
92
|
id: 'ANS-004', name: 'Deictic dependency in answer text', namespace: 'ANS',
|
|
89
93
|
description: 'Answer-bearing prose depends on preceding visual context ("as shown above", "in the diagram below"), which breaks when extracted.',
|
|
90
94
|
discipline: ['aeo'], severity: 'low', deterministic: true, requires: ['site'],
|
|
@@ -108,6 +112,7 @@ D.push(defineDetector({
|
|
|
108
112
|
}));
|
|
109
113
|
|
|
110
114
|
D.push(defineDetector({
|
|
115
|
+
coverage_requirement: 'evaluated_subset',
|
|
111
116
|
id: 'ANS-005', name: 'Relative quantity without baseline or timeframe', namespace: 'ANS',
|
|
112
117
|
description: 'Text asserts a relative numeric improvement ("3x faster", "50% reduction") without a nearby baseline, unit, or timeframe.',
|
|
113
118
|
discipline: ['aeo', 'geo'], severity: 'medium', deterministic: false, requires: ['site'],
|
|
@@ -138,6 +143,7 @@ D.push(defineDetector({
|
|
|
138
143
|
}));
|
|
139
144
|
|
|
140
145
|
D.push(defineDetector({
|
|
146
|
+
coverage_requirement: 'evaluated_subset',
|
|
141
147
|
id: 'ANS-006', name: 'Procedure content without ordered steps', namespace: 'ANS',
|
|
142
148
|
description: 'A page registered as implementation/how-to content contains no ordered list or numbered steps.',
|
|
143
149
|
discipline: ['aeo'], severity: 'low', deterministic: true, requires: ['site', 'registries'],
|
|
@@ -164,6 +170,7 @@ D.push(defineDetector({
|
|
|
164
170
|
}));
|
|
165
171
|
|
|
166
172
|
D.push(defineDetector({
|
|
173
|
+
coverage_requirement: 'evaluated_subset',
|
|
167
174
|
id: 'ANS-007', name: 'Comparison without explicit basis', namespace: 'ANS',
|
|
168
175
|
description: 'A comparison page has neither a comparison table nor prose stating the comparison criteria.',
|
|
169
176
|
discipline: ['aeo', 'geo'], severity: 'medium', deterministic: true, requires: ['site', 'registries'],
|
|
@@ -190,6 +197,7 @@ D.push(defineDetector({
|
|
|
190
197
|
}));
|
|
191
198
|
|
|
192
199
|
D.push(defineDetector({
|
|
200
|
+
coverage_requirement: 'evaluated_subset',
|
|
193
201
|
id: 'ANS-008', name: 'Missing scope or limitations on answer page', namespace: 'ANS',
|
|
194
202
|
description: 'A definition/comparison/recommendation page never states scope boundaries, exclusions, or limitations.',
|
|
195
203
|
discipline: ['aeo', 'geo'], severity: 'medium', deterministic: false, requires: ['site', 'registries'],
|
|
@@ -218,6 +226,7 @@ D.push(defineDetector({
|
|
|
218
226
|
}));
|
|
219
227
|
|
|
220
228
|
D.push(defineDetector({
|
|
229
|
+
coverage_requirement: 'evaluated_subset',
|
|
221
230
|
id: 'ANS-009', name: 'Answer-target page has no prompt coverage', namespace: 'ANS',
|
|
222
231
|
description: 'An answer-bearing page is intended for discovery but is not mapped to any prompt in the governed prompt corpus.',
|
|
223
232
|
discipline: ['aeo', 'geo'], severity: 'medium', deterministic: true, requires: ['site', 'registries'],
|
|
@@ -240,6 +249,7 @@ D.push(defineDetector({
|
|
|
240
249
|
}));
|
|
241
250
|
|
|
242
251
|
D.push(defineDetector({
|
|
252
|
+
coverage_requirement: 'evaluated_subset',
|
|
243
253
|
id: 'ANS-010', name: 'Published claims lack page evidence mapping', namespace: 'ANS',
|
|
244
254
|
description: 'A page publishes governed claims but records no page-level evidence references, preventing evidence-adjacency review.',
|
|
245
255
|
discipline: ['aeo', 'geo'], severity: 'high', deterministic: true, requires: ['site', 'registries'],
|
|
@@ -261,6 +271,7 @@ D.push(defineDetector({
|
|
|
261
271
|
}));
|
|
262
272
|
|
|
263
273
|
D.push(defineDetector({
|
|
274
|
+
coverage_requirement: 'evaluated_subset',
|
|
264
275
|
id: 'ANS-011', name: 'Over-diluted answer passage (missing direct answer lead)', namespace: 'ANS',
|
|
265
276
|
description: 'A section targeting an informational query or question heading lacks a concise, direct answer passage in its opening sentences, burying key factual answers behind excessive preamble or rambling prose.',
|
|
266
277
|
discipline: ['aeo', 'geo'], severity: 'medium', deterministic: false, requires: ['site'],
|
|
@@ -320,6 +331,7 @@ D.push(defineDetector({
|
|
|
320
331
|
}));
|
|
321
332
|
|
|
322
333
|
D.push(defineDetector({
|
|
334
|
+
coverage_requirement: 'evaluated_subset',
|
|
323
335
|
id: 'ANS-012', name: 'Ungrounded quantitative metric in answer passage', namespace: 'ANS',
|
|
324
336
|
description: 'An answer-bearing passage asserts high-stakes quantitative metrics, percentages, or multiples without adjacent evidence citation, benchmark reference, or registered evidence link.',
|
|
325
337
|
discipline: ['aeo', 'geo'], severity: 'medium', deterministic: false, requires: ['site'],
|
|
@@ -359,6 +371,7 @@ D.push(defineDetector({
|
|
|
359
371
|
}));
|
|
360
372
|
|
|
361
373
|
D.push(defineDetector({
|
|
374
|
+
coverage_requirement: 'evaluated_subset',
|
|
362
375
|
id: 'ANS-013', name: 'Comparative or procedural section lacking structured table or ordered sequence', namespace: 'ANS',
|
|
363
376
|
description: 'A section covering comparative evaluation (vs, alternatives, comparison) or multi-step execution (how to, steps) lacks structured <table>, <ol>, or <ul> markup, diminishing answer engine extraction fidelity.',
|
|
364
377
|
discipline: ['aeo', 'seo'], severity: 'medium', deterministic: true, requires: ['site'],
|
|
@@ -420,6 +433,7 @@ D.push(defineDetector({
|
|
|
420
433
|
}));
|
|
421
434
|
|
|
422
435
|
D.push(defineDetector({
|
|
436
|
+
coverage_requirement: 'evaluated_subset',
|
|
423
437
|
id: 'ANS-014', name: 'Low information-gain fluff ratio in answer prose', namespace: 'ANS',
|
|
424
438
|
description: 'An answer passage contains excessive rhetorical filler, clichés, or conversational padding, reducing factual density for generative engine extraction.',
|
|
425
439
|
discipline: ['aeo', 'geo'], severity: 'low', deterministic: true, requires: ['site'],
|
|
@@ -467,6 +481,7 @@ D.push(defineDetector({
|
|
|
467
481
|
}));
|
|
468
482
|
|
|
469
483
|
D.push(defineDetector({
|
|
484
|
+
coverage_requirement: 'evaluated_subset',
|
|
470
485
|
id: 'ANS-015', name: 'Definitional page lacks direct copular definition', namespace: 'ANS',
|
|
471
486
|
description: 'A page with definitional intent (page_type=definition, /definition/, /glossary/, or "What is" heading) does not provide a direct copular definition ("X is a Y that Z") in the opening paragraph.',
|
|
472
487
|
discipline: ['aeo', 'geo'], severity: 'medium', deterministic: true, requires: ['site'],
|
|
@@ -506,6 +521,7 @@ D.push(defineDetector({
|
|
|
506
521
|
}));
|
|
507
522
|
|
|
508
523
|
D.push(defineDetector({
|
|
524
|
+
coverage_requirement: 'evaluated_subset',
|
|
509
525
|
id: 'ANS-016', name: 'Comprehensive long-form guide lacks executive summary or key takeaways', namespace: 'ANS',
|
|
510
526
|
description: 'An in-depth article or guide exceeding 1,200 words does not provide an executive summary, key takeaways, or TL;DR block, increasing RAG chunk fragmentation and extraction ambiguity.',
|
|
511
527
|
discipline: ['aeo', 'geo'], severity: 'low', deterministic: true, requires: ['site'],
|
|
@@ -541,4 +557,3 @@ D.push(defineDetector({
|
|
|
541
557
|
}));
|
|
542
558
|
|
|
543
559
|
export default D;
|
|
544
|
-
|