@nebulacomponents/citable 1.15.1 → 1.17.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 +50 -0
- package/README.md +18 -5
- package/dist/universal/.agents/skills/citable/SKILL.md +51 -1
- package/dist/universal/.agents/skills/citable/VERSION +1 -1
- package/dist/universal/.agents/skills/citable/commands/README.md +3 -1
- package/dist/universal/.agents/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.agents/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.agents/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.agents/skills/citable/manifest.json +17 -12
- package/dist/universal/.agents/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.agents/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.agents/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.agents/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.agents/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.agents/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.agents/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.agents/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/.claude/agents/citable/manifest.json +2 -2
- package/dist/universal/.claude/skills/citable/SKILL.md +51 -1
- package/dist/universal/.claude/skills/citable/VERSION +1 -1
- package/dist/universal/.claude/skills/citable/commands/README.md +3 -1
- package/dist/universal/.claude/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.claude/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.claude/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.claude/skills/citable/manifest.json +17 -12
- package/dist/universal/.claude/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.claude/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.claude/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.claude/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.claude/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.claude/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.claude/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.claude/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/.cursor/skills/citable/SKILL.md +51 -1
- package/dist/universal/.cursor/skills/citable/VERSION +1 -1
- package/dist/universal/.cursor/skills/citable/commands/README.md +3 -1
- package/dist/universal/.cursor/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.cursor/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.cursor/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.cursor/skills/citable/manifest.json +17 -12
- package/dist/universal/.cursor/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.cursor/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.cursor/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.cursor/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.cursor/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.cursor/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.cursor/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.cursor/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/.gemini/skills/citable/SKILL.md +51 -1
- package/dist/universal/.gemini/skills/citable/VERSION +1 -1
- package/dist/universal/.gemini/skills/citable/commands/README.md +3 -1
- package/dist/universal/.gemini/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.gemini/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.gemini/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.gemini/skills/citable/manifest.json +17 -12
- package/dist/universal/.gemini/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.gemini/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.gemini/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.gemini/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.gemini/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.gemini/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.gemini/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.gemini/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/.github/skills/citable/SKILL.md +51 -1
- package/dist/universal/.github/skills/citable/VERSION +1 -1
- package/dist/universal/.github/skills/citable/commands/README.md +3 -1
- package/dist/universal/.github/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.github/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.github/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.github/skills/citable/manifest.json +17 -12
- package/dist/universal/.github/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.github/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.github/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.github/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.github/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.github/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.github/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.github/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/.kiro/skills/citable/SKILL.md +51 -1
- package/dist/universal/.kiro/skills/citable/VERSION +1 -1
- package/dist/universal/.kiro/skills/citable/commands/README.md +3 -1
- package/dist/universal/.kiro/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.kiro/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.kiro/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.kiro/skills/citable/manifest.json +17 -12
- package/dist/universal/.kiro/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.kiro/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.kiro/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.kiro/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.kiro/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.kiro/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.kiro/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.kiro/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/.opencode/skills/citable/SKILL.md +51 -1
- package/dist/universal/.opencode/skills/citable/VERSION +1 -1
- package/dist/universal/.opencode/skills/citable/commands/README.md +3 -1
- package/dist/universal/.opencode/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.opencode/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.opencode/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.opencode/skills/citable/manifest.json +17 -12
- package/dist/universal/.opencode/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.opencode/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.opencode/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.opencode/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.opencode/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.opencode/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.opencode/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.opencode/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/.pi/agent/skills/citable/SKILL.md +51 -1
- package/dist/universal/.pi/agent/skills/citable/VERSION +1 -1
- package/dist/universal/.pi/agent/skills/citable/commands/README.md +3 -1
- package/dist/universal/.pi/agent/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.pi/agent/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.pi/agent/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.pi/agent/skills/citable/manifest.json +17 -12
- package/dist/universal/.pi/agent/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.pi/agent/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.pi/agent/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/.qoder/skills/citable/SKILL.md +51 -1
- package/dist/universal/.qoder/skills/citable/VERSION +1 -1
- package/dist/universal/.qoder/skills/citable/commands/README.md +3 -1
- package/dist/universal/.qoder/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.qoder/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.qoder/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.qoder/skills/citable/manifest.json +17 -12
- package/dist/universal/.qoder/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.qoder/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.qoder/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.qoder/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.qoder/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.qoder/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.qoder/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.qoder/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/.rovodev/skills/citable/SKILL.md +51 -1
- package/dist/universal/.rovodev/skills/citable/VERSION +1 -1
- package/dist/universal/.rovodev/skills/citable/commands/README.md +3 -1
- package/dist/universal/.rovodev/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.rovodev/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.rovodev/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.rovodev/skills/citable/manifest.json +17 -12
- package/dist/universal/.rovodev/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.rovodev/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.rovodev/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.rovodev/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.rovodev/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.rovodev/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.rovodev/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.rovodev/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/.trae/skills/citable/SKILL.md +51 -1
- package/dist/universal/.trae/skills/citable/VERSION +1 -1
- package/dist/universal/.trae/skills/citable/commands/README.md +3 -1
- package/dist/universal/.trae/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.trae/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.trae/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.trae/skills/citable/manifest.json +17 -12
- package/dist/universal/.trae/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.trae/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.trae/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.trae/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.trae/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.trae/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.trae/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.trae/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/.trae-cn/skills/citable/SKILL.md +51 -1
- package/dist/universal/.trae-cn/skills/citable/VERSION +1 -1
- package/dist/universal/.trae-cn/skills/citable/commands/README.md +3 -1
- package/dist/universal/.trae-cn/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.trae-cn/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.trae-cn/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.trae-cn/skills/citable/manifest.json +17 -12
- package/dist/universal/.trae-cn/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.trae-cn/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.trae-cn/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/manifest.json +208 -148
- package/package.json +1 -1
- package/schemas/cli-output-envelope.schema.json +30 -0
- package/schemas/experiment.schema.json +1 -1
- package/schemas/finding.schema.json +69 -0
- package/schemas/fleet.schema.json +93 -0
- package/schemas/funnel.schema.json +116 -0
- package/schemas/mcp-transport-envelope.schema.json +244 -0
- package/schemas/observation.schema.json +1 -1
- package/schemas/remediation-verification.schema.json +98 -0
- package/skill/SKILL.md +51 -1
- package/skill/commands/README.md +3 -1
- package/skill/commands/governance.md +12 -0
- package/skill/commands/measurement.md +20 -0
- package/skill/commands/reporting.md +16 -0
- package/src/analysis/saliency.js +119 -0
- package/src/cli/index.js +651 -13
- package/src/commands/attributeImpact.js +54 -0
- package/src/commands/audit.js +2 -2
- package/src/commands/ciWorkflow.js +63 -0
- package/src/commands/compatibility.js +205 -0
- package/src/commands/connect.js +3 -0
- package/src/commands/context.js +2 -1
- package/src/commands/edgeRules.js +196 -0
- package/src/commands/edgeSecurity.js +192 -0
- package/src/commands/exceptions.js +241 -0
- package/src/commands/experimentGuardrails.js +213 -0
- package/src/commands/fleet.js +104 -0
- package/src/commands/generateLlmsTxt.js +111 -0
- package/src/commands/goldenCorpus.js +155 -0
- package/src/commands/implementationKit.js +202 -0
- package/src/commands/inspectAeo.js +74 -0
- package/src/commands/inspectCro.js +142 -0
- package/src/commands/inspectGeo.js +52 -0
- package/src/commands/inspectSerp.js +317 -0
- package/src/commands/lintComponents.js +132 -0
- package/src/commands/monitor.js +138 -0
- package/src/commands/observe.js +6 -1
- package/src/commands/planExperiment.js +120 -0
- package/src/commands/previewCro.js +241 -0
- package/src/commands/probe.js +70 -0
- package/src/commands/remediate.js +461 -0
- package/src/commands/reportConsensus.js +48 -0
- package/src/commands/testFunnel.js +107 -0
- package/src/commands/verifyRemediation.js +204 -0
- package/src/commands/visualRegression.js +165 -0
- package/src/components/index.js +556 -0
- package/src/connectors/index.js +6 -1
- package/src/connectors/indexnow.js +431 -0
- package/src/connectors/mcp/allowlist.js +107 -0
- package/src/connectors/mcp/client.js +239 -0
- package/src/connectors/mcp/diagnostics.js +214 -0
- package/src/connectors/mcp/gscServer.js +155 -0
- package/src/connectors/mcp/http.js +130 -0
- package/src/connectors/mcp/index.js +7 -0
- package/src/connectors/mcp/pilot.js +138 -0
- package/src/connectors/mcp/pilotServer.js +80 -0
- package/src/connectors/mcp/sanitizer.js +38 -0
- package/src/connectors/mcp/stdio.js +178 -0
- package/src/crawler/ipRanges.js +161 -0
- package/src/detectors/agent.js +127 -0
- package/src/detectors/ans.js +282 -0
- package/src/detectors/crawl.js +46 -0
- package/src/detectors/cro.js +825 -0
- package/src/detectors/cwv.js +38 -1
- package/src/detectors/evd.js +101 -0
- package/src/detectors/framework.js +32 -2
- package/src/detectors/geoReco.js +408 -0
- package/src/detectors/index.js +4 -2
- package/src/detectors/link.js +112 -1
- package/src/detectors/page.js +101 -0
- package/src/detectors/schemaData.js +529 -0
- package/src/detectors/tech.js +306 -0
- package/src/extractor/page.js +186 -2
- package/src/extractor/site.js +9 -2
- package/src/installer/index.js +45 -2
- package/src/monitoring/alertDelivery.js +101 -7
- package/src/observations/attribution.js +217 -0
- package/src/registries/index.js +14 -0
- package/src/reporting/consensus.js +416 -0
- package/src/reporting/executiveExport.js +244 -0
package/src/detectors/cwv.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* - CLS (Cumulative Layout Shift): < 0.1 good, < 0.25 needs improvement
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { defineDetector } from './framework.js';
|
|
14
|
+
import { defineDetector, indexTargets, pageSubject } from './framework.js';
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* CWV-001: LCP element identified and optimized
|
|
@@ -164,8 +164,45 @@ export const CWV_003 = defineDetector({
|
|
|
164
164
|
},
|
|
165
165
|
});
|
|
166
166
|
|
|
167
|
+
export const CWV_004 = defineDetector({
|
|
168
|
+
id: 'CWV-004',
|
|
169
|
+
name: 'Excessive DOM size and depth',
|
|
170
|
+
namespace: 'CWV',
|
|
171
|
+
discipline: ['seo'],
|
|
172
|
+
severity: 'medium',
|
|
173
|
+
deterministic: true,
|
|
174
|
+
requires: ['site'],
|
|
175
|
+
description: 'Page DOM tree exceeds 1,500 nodes or maximum nesting depth exceeds 32 levels, causing mobile crawler execution delays and Interaction to Next Paint (INP) degradation.',
|
|
176
|
+
remediation: 'Simplify DOM structure, paginate or virtualize repeated elements, and reduce deep nesting of wrapper containers.',
|
|
177
|
+
verification: 'Re-audit page to verify total DOM node count is <= 1,500 and maximum depth is <= 32.',
|
|
178
|
+
check: (ctx) => {
|
|
179
|
+
const hits = [];
|
|
180
|
+
const targets = ctx.site ? indexTargets(ctx) : ctx.page ? [ctx.page] : [];
|
|
181
|
+
for (const p of targets) {
|
|
182
|
+
if (p.status !== 200) continue;
|
|
183
|
+
const nodeCount = p.domNodeCount || 0;
|
|
184
|
+
const maxDepth = p.maxDomDepth || 0;
|
|
185
|
+
if (nodeCount > 1500 || maxDepth > 32) {
|
|
186
|
+
hits.push({
|
|
187
|
+
subject: pageSubject(p),
|
|
188
|
+
summary: `Excessive DOM complexity (${nodeCount > 1500 ? `${nodeCount} nodes` : ''}${nodeCount > 1500 && maxDepth > 32 ? ', ' : ''}${maxDepth > 32 ? `depth ${maxDepth}` : ''}) risks crawler execution timeout and INP latency`,
|
|
189
|
+
evidence: [
|
|
190
|
+
`total DOM nodes: ${nodeCount}`,
|
|
191
|
+
`maximum DOM depth: ${maxDepth}`,
|
|
192
|
+
'Google Lighthouse and mobile crawler guidelines recommend <= 1,500 nodes and <= 32 nesting levels',
|
|
193
|
+
],
|
|
194
|
+
captured: { nodeCount, maxDepth },
|
|
195
|
+
expected: 'nodeCount <= 1500 and maxDepth <= 32',
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return hits;
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
|
|
167
203
|
export const cwvDetectors = [
|
|
168
204
|
CWV_001,
|
|
169
205
|
CWV_002,
|
|
170
206
|
CWV_003,
|
|
207
|
+
CWV_004,
|
|
171
208
|
];
|
package/src/detectors/evd.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
1
3
|
import { defineDetector, entrySubject } from './framework.js';
|
|
2
4
|
import { isPastDate } from '../shared/io.js';
|
|
3
5
|
|
|
@@ -161,4 +163,103 @@ D.push(defineDetector({
|
|
|
161
163
|
},
|
|
162
164
|
}));
|
|
163
165
|
|
|
166
|
+
D.push(defineDetector({
|
|
167
|
+
id: 'EVD-008', name: 'Dangling media reference in claim evidence', namespace: 'EVD',
|
|
168
|
+
description: 'Claim evidence references a media asset (image, chart, diagram, or video) whose referenced source location or file is missing, unresolved, or unreachable.',
|
|
169
|
+
discipline: ['aeo', 'geo'], severity: 'high', deterministic: true, requires: ['registries'],
|
|
170
|
+
impact: { legal: 'medium', citation: 'medium' },
|
|
171
|
+
applicable_requirement: 'Registry §6.5 evidence sources; premise 3.5: verified evidence must be grounded and inspectable',
|
|
172
|
+
remediation: 'Ensure referenced media artifacts exist at the specified target path or update the source location with an accessible asset.',
|
|
173
|
+
verification: 'All media evidence sources resolve to valid accessible files or active URLs.',
|
|
174
|
+
check(ctx) {
|
|
175
|
+
const isMedia = (e) => {
|
|
176
|
+
const st = (e.source_type || '').toLowerCase();
|
|
177
|
+
if (['media', 'image', 'video', 'diagram', 'chart', 'screenshot'].includes(st)) return true;
|
|
178
|
+
if (typeof e.source === 'string' && /\.(png|jpe?g|svg|webp|gif|mp4|webm)$/i.test(e.source.split('?')[0].split('#')[0])) return true;
|
|
179
|
+
return false;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const hits = [];
|
|
183
|
+
for (const e of (ctx.registries?.evidence?.entries || [])) {
|
|
184
|
+
if (!isMedia(e)) continue;
|
|
185
|
+
|
|
186
|
+
let dangling = false;
|
|
187
|
+
let reason = '';
|
|
188
|
+
|
|
189
|
+
if (!e.source || e.source.trim() === '') {
|
|
190
|
+
dangling = true;
|
|
191
|
+
reason = 'source field is empty or missing';
|
|
192
|
+
} else if (!/^https?:\/\//i.test(e.source)) {
|
|
193
|
+
// Local file reference
|
|
194
|
+
const cleanPath = e.source.replace(/^\/+/, '');
|
|
195
|
+
let fileFound = false;
|
|
196
|
+
|
|
197
|
+
if (ctx.site?.dir) {
|
|
198
|
+
const fullPath = path.resolve(ctx.site.dir, cleanPath);
|
|
199
|
+
if (fs.existsSync(fullPath)) fileFound = true;
|
|
200
|
+
}
|
|
201
|
+
if (!fileFound && ctx.root) {
|
|
202
|
+
const fullPath = path.resolve(ctx.root, cleanPath);
|
|
203
|
+
if (fs.existsSync(fullPath)) fileFound = true;
|
|
204
|
+
}
|
|
205
|
+
if (!fileFound && ctx.site?.pages) {
|
|
206
|
+
fileFound = ctx.site.pages.some((p) =>
|
|
207
|
+
p.images?.some((img) => img.src === e.source || img.src === `/${cleanPath}` || img.src?.endsWith(cleanPath))
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (!fileFound) {
|
|
212
|
+
dangling = true;
|
|
213
|
+
reason = `local media file not found: ${cleanPath}`;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (dangling) {
|
|
218
|
+
hits.push({
|
|
219
|
+
subject: entrySubject('evidence', e.evidence_id),
|
|
220
|
+
summary: `Evidence "${e.title}" references dangling or missing media (${reason})`,
|
|
221
|
+
evidence: [`source: ${e.source || '(missing)'}`, `source_type: ${e.source_type || 'unspecified'}`, `reason: ${reason}`],
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return hits;
|
|
226
|
+
},
|
|
227
|
+
}));
|
|
228
|
+
|
|
229
|
+
D.push(defineDetector({
|
|
230
|
+
id: 'EVD-009', name: 'Unanchored PDF claim citation', namespace: 'EVD',
|
|
231
|
+
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
|
+
discipline: ['aeo', 'geo'], severity: 'low', deterministic: true, requires: ['registries'],
|
|
233
|
+
impact: { citation: 'medium', legal: 'low' },
|
|
234
|
+
applicable_requirement: 'AEO §8 version evidence; premise 3.5 precise citation anchoring for defensible claims',
|
|
235
|
+
false_positive_conditions: ['single-page PDF documents where page anchor is redundant'],
|
|
236
|
+
remediation: 'Append #page=<n> or specify the page/section citation in methodology or test_conditions.',
|
|
237
|
+
verification: 'PDF evidence sources carry explicit page, section, or fragment anchors.',
|
|
238
|
+
check(ctx) {
|
|
239
|
+
const isPdf = (e) => (e.source_type || '').toLowerCase() === 'pdf' || (typeof e.source === 'string' && /\.pdf([?#]|$)/i.test(e.source));
|
|
240
|
+
const hits = [];
|
|
241
|
+
|
|
242
|
+
for (const e of (ctx.registries?.evidence?.entries || [])) {
|
|
243
|
+
if (!isPdf(e)) continue;
|
|
244
|
+
|
|
245
|
+
const hasUrlAnchor = typeof e.source === 'string' && (/#page=\d+|#section|#[a-zA-Z0-9_-]{2,}/i.test(e.source));
|
|
246
|
+
const text = `${e.methodology || ''} ${e.test_conditions || ''}`;
|
|
247
|
+
const hasTextAnchor = /\b(page|p\.|section|sec\.|paragraph|para\.)\s*\d+/i.test(text);
|
|
248
|
+
|
|
249
|
+
if (!hasUrlAnchor && !hasTextAnchor) {
|
|
250
|
+
hits.push({
|
|
251
|
+
subject: entrySubject('evidence', e.evidence_id),
|
|
252
|
+
summary: `PDF evidence "${e.title}" lacks a specific page or section anchor for citation grounding`,
|
|
253
|
+
evidence: [
|
|
254
|
+
`source: ${e.source || '(missing)'}`,
|
|
255
|
+
`methodology: ${e.methodology || 'missing'}`,
|
|
256
|
+
`test_conditions: ${e.test_conditions || 'missing'}`,
|
|
257
|
+
],
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return hits;
|
|
262
|
+
},
|
|
263
|
+
}));
|
|
264
|
+
|
|
164
265
|
export default D;
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import { sha256 } from '../shared/io.js';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { readJson } from '../shared/io.js';
|
|
4
|
+
|
|
5
|
+
const TOOL_VERSION = readJson(new URL('../../package.json', import.meta.url)).version;
|
|
2
6
|
|
|
3
7
|
const REQUIRED = ['id', 'name', 'namespace', 'description', 'discipline', 'severity', 'deterministic', 'remediation', 'verification', 'check'];
|
|
4
8
|
// RENDER (browser-rendered truth / source-render divergence) is reserved for the
|
|
5
9
|
// rendered-truth phase; no RENDER detectors ship until a real renderer backs them.
|
|
6
|
-
const NAMESPACES = ['TECH', 'CRAWL', 'ARCH', 'PAGE', 'ANS', 'ENTITY', 'CLAIM', 'EVD', 'SCHEMA', 'LINK', 'EXT', 'GEO', 'RECO', 'LIFE', 'MEAS', 'RENDER', 'HREFLANG', 'CWV', 'AGENT'];
|
|
10
|
+
const NAMESPACES = ['TECH', 'CRAWL', 'ARCH', 'PAGE', 'ANS', 'ENTITY', 'CLAIM', 'EVD', 'SCHEMA', 'LINK', 'EXT', 'GEO', 'RECO', 'LIFE', 'MEAS', 'RENDER', 'HREFLANG', 'CWV', 'AGENT', 'CRO'];
|
|
7
11
|
|
|
8
12
|
/**
|
|
9
13
|
* Detector definition contract. `check(ctx)` returns raw hits:
|
|
10
|
-
* { subject: {type, identifier, url?, source_file?, source_location?}, summary, evidence: [..],
|
|
14
|
+
* { subject: {type, identifier, url?, source_file?, source_location?, rendered_selector?}, summary, evidence: [..],
|
|
11
15
|
* captured?, expected?, severity?, confidence? }
|
|
16
|
+
* Bump `version` whenever detector logic, thresholds, or output semantics
|
|
17
|
+
* change; findings record it in provenance.detector_version.
|
|
12
18
|
*/
|
|
13
19
|
export function defineDetector(def) {
|
|
14
20
|
for (const k of REQUIRED) {
|
|
@@ -17,6 +23,7 @@ export function defineDetector(def) {
|
|
|
17
23
|
if (!NAMESPACES.includes(def.namespace)) throw new Error(`detector ${def.id}: unknown namespace ${def.namespace}`);
|
|
18
24
|
if (!def.id.startsWith(def.namespace + '-')) throw new Error(`detector id ${def.id} must be prefixed with namespace`);
|
|
19
25
|
return {
|
|
26
|
+
version: 1,
|
|
20
27
|
confidence_model: def.deterministic ? 'binary condition; confirmed when observed' : 'heuristic; confidence reported per finding',
|
|
21
28
|
confidence: def.deterministic ? 'confirmed' : 'medium',
|
|
22
29
|
finding_type: def.deterministic ? 'deterministic_observation' : 'probabilistic_inference',
|
|
@@ -28,6 +35,14 @@ export function defineDetector(def) {
|
|
|
28
35
|
};
|
|
29
36
|
}
|
|
30
37
|
|
|
38
|
+
function evidenceSourceFor(detector, ctx) {
|
|
39
|
+
if (ctx.observations?.length && detector.requires?.includes('observations')) return 'controlled_observation';
|
|
40
|
+
if (detector.requires?.includes('registries') && detector.requires?.includes('site')) return 'registry+dom_parse';
|
|
41
|
+
if (detector.requires?.includes('registries')) return 'registry';
|
|
42
|
+
if (detector.requires?.includes('site')) return 'dom_parse';
|
|
43
|
+
return 'configuration';
|
|
44
|
+
}
|
|
45
|
+
|
|
31
46
|
/** Run detectors over a context; returns { findings, detectorsRun, detectorsSkipped, errors }. */
|
|
32
47
|
export function runDetectors(detectors, ctx) {
|
|
33
48
|
const findings = [];
|
|
@@ -53,6 +68,7 @@ export function runDetectors(detectors, ctx) {
|
|
|
53
68
|
findings.push({
|
|
54
69
|
finding_id: `F-${sha256(idSeed).slice(0, 12)}`,
|
|
55
70
|
detector_id: d.id,
|
|
71
|
+
detector_name: d.name,
|
|
56
72
|
run_id: ctx.runId ?? 'adhoc',
|
|
57
73
|
timestamp: ts,
|
|
58
74
|
discipline: d.discipline,
|
|
@@ -88,6 +104,20 @@ export function runDetectors(detectors, ctx) {
|
|
|
88
104
|
expected_result: 'detector no longer reports this subject',
|
|
89
105
|
detector_to_rerun: d.id,
|
|
90
106
|
},
|
|
107
|
+
provenance: {
|
|
108
|
+
detector_version: d.version ?? 1,
|
|
109
|
+
tool_version: TOOL_VERSION,
|
|
110
|
+
source_url: hit.subject?.url ?? hit.subject?.identifier ?? null,
|
|
111
|
+
source_file: hit.subject?.source_file ?? null,
|
|
112
|
+
evidence_selector: hit.subject?.rendered_selector ?? hit.subject?.source_location ?? null,
|
|
113
|
+
evidence_source: evidenceSourceFor(d, ctx),
|
|
114
|
+
viewport: ctx.viewport ?? null,
|
|
115
|
+
viewport_note: ctx.viewport ? null : 'viewport not captured for this collection method; static source/built-output check',
|
|
116
|
+
methodology: d.deterministic
|
|
117
|
+
? 'deterministic condition check over captured evidence'
|
|
118
|
+
: 'heuristic judgment; human semantic review required before acting',
|
|
119
|
+
revalidation_required: d.deterministic ? 'on_next_audit' : 'human_review_before_action',
|
|
120
|
+
},
|
|
91
121
|
status: { state: 'open', first_seen: ts, last_seen: ts, resolved_at: null },
|
|
92
122
|
});
|
|
93
123
|
}
|
package/src/detectors/geoReco.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineDetector, indexTargets, registryPageFor, sitePageFor, pageSubject, entrySubject } from './framework.js';
|
|
2
2
|
import { classifyAnswerStance } from '../observations/stance.js';
|
|
3
|
+
import { evaluateAnswerAttribution } from '../observations/attribution.js';
|
|
3
4
|
|
|
4
5
|
const D = [];
|
|
5
6
|
|
|
@@ -324,6 +325,413 @@ D.push(defineDetector({
|
|
|
324
325
|
},
|
|
325
326
|
}));
|
|
326
327
|
|
|
328
|
+
D.push(defineDetector({
|
|
329
|
+
id: 'GEO-008', name: 'Generative citation attributes unverified or distorted claim to brand', namespace: 'GEO',
|
|
330
|
+
description: 'Recorded generative engine answers attribute contradicted, distorted, or unverified factual claims to a registered entity or brand.',
|
|
331
|
+
discipline: ['geo', 'aeo'], severity: 'high', deterministic: false, requires: ['registries'],
|
|
332
|
+
finding_type: 'evidence_backed_semantic_finding',
|
|
333
|
+
impact: { representation: 'high', reputational: 'high', conversion: 'medium' },
|
|
334
|
+
applicable_requirement: 'GEO §6: accurate synthesis and entity representation; Rubric: narrative accuracy (§4 & §5) and claim boundedness (§1)',
|
|
335
|
+
false_positive_conditions: ['Answer accurately reflects a verified change in product capability not yet updated in registries'],
|
|
336
|
+
remediation: 'Review the generative engine answer against the narrative-accuracy and claim-boundedness rubrics, identify the distorted or unverified claim attribution, and update public corroborating evidence or submit engine correction requests.',
|
|
337
|
+
unsafe_shortcuts: ['deleting legitimate claim records to hide discrepancies', 'crawler prompt injection (GEO-001)'],
|
|
338
|
+
verification: 'Follow-up generative engine observations demonstrate accurate claim attribution without distorted or hallucinated assertions.',
|
|
339
|
+
review_required: true,
|
|
340
|
+
check(ctx) {
|
|
341
|
+
const entities = (ctx.registries?.entities?.entries || []).filter((e) => e.status !== 'retired');
|
|
342
|
+
if (!entities.length) return [];
|
|
343
|
+
const claims = (ctx.registries?.claims?.entries || []).filter((c) => c.status !== 'retired');
|
|
344
|
+
const hits = [];
|
|
345
|
+
|
|
346
|
+
// 1. Check observations (attribution or citation)
|
|
347
|
+
for (const obs of ctx.observations || []) {
|
|
348
|
+
if (obs.kind === 'attribution' && obs.data?.has_distorted_claims) {
|
|
349
|
+
for (const evalItem of obs.data.evaluations || []) {
|
|
350
|
+
if (evalItem.attribution_status === 'distorted') {
|
|
351
|
+
hits.push({
|
|
352
|
+
subject: entrySubject('claims', evalItem.claim_id),
|
|
353
|
+
summary: `Generative engine ${obs.data.engine || 'unknown'} attributes distorted claim "${evalItem.claim_id}" to entity "${obs.data.canonical_name}"`,
|
|
354
|
+
evidence: [
|
|
355
|
+
`engine: ${obs.data.engine || 'unknown'}`,
|
|
356
|
+
`prompt: ${obs.data.prompt_text || obs.data.prompt_id || 'unknown'}`,
|
|
357
|
+
`claim_id: ${evalItem.claim_id}`,
|
|
358
|
+
`claim_text: "${evalItem.claim_text}"`,
|
|
359
|
+
`assertion_excerpt: "${evalItem.assertion_excerpt}"`,
|
|
360
|
+
`reasons: ${(evalItem.reasons || []).join('; ')}`,
|
|
361
|
+
`reviewer: ${obs.data.reviewer || 'unreviewed (semantic review required)'}`,
|
|
362
|
+
],
|
|
363
|
+
confidence: obs.data.reviewer ? 'confirmed' : 'medium',
|
|
364
|
+
finding_type: 'evidence_backed_semantic_finding',
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
} else if (obs.kind === 'citation' && obs.data?.answer_text) {
|
|
369
|
+
for (const e of entities) {
|
|
370
|
+
const evalRes = evaluateAnswerAttribution(obs.data.answer_text, e, { claims });
|
|
371
|
+
if (evalRes.mentioned && evalRes.has_distorted_claims) {
|
|
372
|
+
for (const evalItem of evalRes.evaluations) {
|
|
373
|
+
if (evalItem.attribution_status === 'distorted') {
|
|
374
|
+
hits.push({
|
|
375
|
+
subject: entrySubject('claims', evalItem.claim_id),
|
|
376
|
+
summary: `Generative engine ${obs.data.provider || 'unknown'} attributes distorted claim "${evalItem.claim_id}" to entity "${e.canonical_name}"`,
|
|
377
|
+
evidence: [
|
|
378
|
+
`engine: ${obs.data.provider || 'unknown'}`,
|
|
379
|
+
`prompt: ${obs.data.prompt_text || obs.data.prompt_id || 'unknown'}`,
|
|
380
|
+
`claim_id: ${evalItem.claim_id}`,
|
|
381
|
+
`assertion_excerpt: "${evalItem.assertion_excerpt}"`,
|
|
382
|
+
`reasons: ${(evalItem.reasons || []).join('; ')}`,
|
|
383
|
+
],
|
|
384
|
+
confidence: 'medium',
|
|
385
|
+
finding_type: 'evidence_backed_semantic_finding',
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// 2. Check promptResults
|
|
395
|
+
for (const pr of ctx.promptResults || []) {
|
|
396
|
+
if (!pr.answer_text) continue;
|
|
397
|
+
for (const e of entities) {
|
|
398
|
+
const evalRes = evaluateAnswerAttribution(pr.answer_text, e, { claims, reviewer: pr.evaluator });
|
|
399
|
+
if (evalRes.mentioned && evalRes.has_distorted_claims) {
|
|
400
|
+
for (const evalItem of evalRes.evaluations) {
|
|
401
|
+
if (evalItem.attribution_status === 'distorted') {
|
|
402
|
+
hits.push({
|
|
403
|
+
subject: entrySubject('claims', evalItem.claim_id),
|
|
404
|
+
summary: `Generative engine ${pr.engine || 'unknown'} attributes distorted claim "${evalItem.claim_id}" to entity "${e.canonical_name}"`,
|
|
405
|
+
evidence: [
|
|
406
|
+
`engine: ${pr.engine || 'unknown'}`,
|
|
407
|
+
`prompt: ${pr.prompt_text || pr.prompt_id || 'unknown'}`,
|
|
408
|
+
`claim_id: ${evalItem.claim_id}`,
|
|
409
|
+
`assertion_excerpt: "${evalItem.assertion_excerpt}"`,
|
|
410
|
+
`reasons: ${(evalItem.reasons || []).join('; ')}`,
|
|
411
|
+
`reviewer: ${pr.evaluator || 'unreviewed (semantic review required)'}`,
|
|
412
|
+
],
|
|
413
|
+
confidence: pr.evaluator ? 'confirmed' : 'medium',
|
|
414
|
+
finding_type: 'evidence_backed_semantic_finding',
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const seen = new Set();
|
|
423
|
+
return hits.filter((h) => {
|
|
424
|
+
const k = `${h.subject.identifier}|${h.summary}`;
|
|
425
|
+
if (seen.has(k)) return false;
|
|
426
|
+
seen.add(k);
|
|
427
|
+
return true;
|
|
428
|
+
});
|
|
429
|
+
},
|
|
430
|
+
}));
|
|
431
|
+
|
|
432
|
+
D.push(defineDetector({
|
|
433
|
+
id: 'GEO-009', name: 'Unstable citation presence across repeated probes', namespace: 'GEO',
|
|
434
|
+
description: 'Brand citation status exhibits high volatility or flapping (alternating between cited and unmentioned) across repeated probe runs for the same prompt and engine.',
|
|
435
|
+
discipline: ['geo', 'aeo'], severity: 'medium', deterministic: false, requires: ['registries'],
|
|
436
|
+
finding_type: 'evidence_backed_semantic_finding',
|
|
437
|
+
impact: { representation: 'high', citation: 'high' },
|
|
438
|
+
applicable_requirement: 'GEO §6 generative retrieval stability; longitudinal observation controls; answer-engine variance tracking',
|
|
439
|
+
false_positive_conditions: ['Deliberate query variation or divergent test prompts'],
|
|
440
|
+
remediation: 'Strengthen entity salience and disambiguation in target content, improve passage answer density, and establish consistent external corroboration for core claims.',
|
|
441
|
+
unsafe_shortcuts: ['over-optimizing phrasing to trigger keyword matching in a single model version'],
|
|
442
|
+
verification: 'Subsequent repeated probe runs demonstrate stable citation presence (presence rate >= 80% with low flapping).',
|
|
443
|
+
check(ctx) {
|
|
444
|
+
const hits = [];
|
|
445
|
+
const groups = new Map();
|
|
446
|
+
|
|
447
|
+
// 1. Group citation observations by prompt + provider
|
|
448
|
+
for (const obs of ctx.observations || []) {
|
|
449
|
+
if (obs.kind !== 'citation' || !obs.data) continue;
|
|
450
|
+
const promptKey = obs.data.prompt_id || obs.data.prompt_text;
|
|
451
|
+
if (!promptKey) continue;
|
|
452
|
+
const provider = obs.data.provider || 'unknown';
|
|
453
|
+
const key = `${promptKey}|${provider}`;
|
|
454
|
+
if (!groups.has(key)) groups.set(key, { prompt: promptKey, provider, samples: [] });
|
|
455
|
+
groups.get(key).samples.push(Boolean(obs.data.property_cited));
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// 2. Also group promptResults if present
|
|
459
|
+
for (const pr of ctx.promptResults || []) {
|
|
460
|
+
const promptKey = pr.prompt_id || pr.prompt_text;
|
|
461
|
+
if (!promptKey) continue;
|
|
462
|
+
const provider = pr.engine || 'unknown';
|
|
463
|
+
const key = `${promptKey}|${provider}`;
|
|
464
|
+
if (!groups.has(key)) groups.set(key, { prompt: promptKey, provider, samples: [] });
|
|
465
|
+
const cited = pr.cited !== undefined ? Boolean(pr.cited) : (pr.citations && pr.citations.length > 0);
|
|
466
|
+
groups.get(key).samples.push(Boolean(cited));
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Evaluate each group with at least 3 samples
|
|
470
|
+
for (const { prompt, provider, samples } of groups.values()) {
|
|
471
|
+
if (samples.length < 3) continue;
|
|
472
|
+
const total = samples.length;
|
|
473
|
+
const citedCount = samples.filter(Boolean).length;
|
|
474
|
+
const presenceRate = citedCount / total;
|
|
475
|
+
|
|
476
|
+
// Calculate transitions (flapping)
|
|
477
|
+
let flips = 0;
|
|
478
|
+
for (let i = 1; i < samples.length; i++) {
|
|
479
|
+
if (samples[i] !== samples[i - 1]) flips++;
|
|
480
|
+
}
|
|
481
|
+
const flappingRate = flips / (total - 1);
|
|
482
|
+
|
|
483
|
+
// Flapping threshold: presence rate between 20% and 80% with at least 1 flip, or flapping rate >= 0.4
|
|
484
|
+
if ((presenceRate >= 0.2 && presenceRate <= 0.8 && flips >= 1) || flappingRate >= 0.4) {
|
|
485
|
+
const volatility = (1 - Math.abs(presenceRate - 0.5) * 2).toFixed(2);
|
|
486
|
+
hits.push({
|
|
487
|
+
subject: entrySubject('prompts', prompt),
|
|
488
|
+
summary: `High citation volatility for "${prompt}" on ${provider}: cited in ${citedCount}/${total} runs (${Math.round(presenceRate * 100)}% presence rate)`,
|
|
489
|
+
evidence: [
|
|
490
|
+
`provider: ${provider}`,
|
|
491
|
+
`prompt: ${prompt}`,
|
|
492
|
+
`total_probes: ${total}`,
|
|
493
|
+
`cited_count: ${citedCount}`,
|
|
494
|
+
`presence_rate: ${(presenceRate * 100).toFixed(1)}%`,
|
|
495
|
+
`flapping_rate: ${(flappingRate * 100).toFixed(1)}% (${flips} state flips)`,
|
|
496
|
+
`volatility_score: ${volatility}`,
|
|
497
|
+
`sample_sequence: [${samples.map((s) => (s ? 'cited' : 'omitted')).join(', ')}]`,
|
|
498
|
+
],
|
|
499
|
+
confidence: total >= 5 ? 'confirmed' : 'medium',
|
|
500
|
+
captured: { presenceRate, flappingRate, volatility: Number(volatility), samples: total },
|
|
501
|
+
expected: 'stable citation presence (presence rate >= 80% with low flapping)',
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return hits;
|
|
507
|
+
},
|
|
508
|
+
}));
|
|
509
|
+
|
|
510
|
+
D.push(defineDetector({
|
|
511
|
+
id: 'GEO-010', name: 'Brand erasure in multi-competitor category prompt', namespace: 'GEO',
|
|
512
|
+
description: 'A generative engine observation for a relevant category or comparison prompt cites multiple registered competitors but completely omits the first-party entity or brand.',
|
|
513
|
+
discipline: ['geo', 'aeo'], severity: 'high', deterministic: false, requires: ['registries'],
|
|
514
|
+
impact: { representation: 'high', conversion: 'medium' },
|
|
515
|
+
applicable_requirement: 'GEO §6 entity representation in competitive category synthesis; GEO §1 share of model parity',
|
|
516
|
+
remediation: 'Publish comparative evidence, authoritative category definitions, and benchmark substantiation to establish entity co-occurrence in generative training and retrieval corpora.',
|
|
517
|
+
verification: 'Follow-up category engine observations cite the first-party brand alongside peers.',
|
|
518
|
+
check(ctx) {
|
|
519
|
+
const competitors = ctx.registries?.competitors?.entries || [];
|
|
520
|
+
if (competitors.length < 2) return [];
|
|
521
|
+
const entities = (ctx.registries?.entities?.entries || []).filter((e) => e.status !== 'retired');
|
|
522
|
+
if (!entities.length) return [];
|
|
523
|
+
|
|
524
|
+
const firstPartyNames = [];
|
|
525
|
+
for (const e of entities) {
|
|
526
|
+
if (e.canonical_name) firstPartyNames.push(e.canonical_name.toLowerCase());
|
|
527
|
+
for (const a of e.aliases || []) firstPartyNames.push(a.toLowerCase());
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const hits = [];
|
|
531
|
+
for (const obs of ctx.observations || []) {
|
|
532
|
+
const d = obs.data || {};
|
|
533
|
+
const answer = d.answer_text || '';
|
|
534
|
+
const lowerAnswer = answer.toLowerCase();
|
|
535
|
+
|
|
536
|
+
// Check if first-party brand is omitted
|
|
537
|
+
const firstPartyCited = d.property_cited === true || firstPartyNames.some((n) => lowerAnswer.includes(n));
|
|
538
|
+
if (firstPartyCited) continue;
|
|
539
|
+
|
|
540
|
+
let citedCompetitors = [];
|
|
541
|
+
if (Array.isArray(d.competitors_cited) && d.competitors_cited.length > 0) {
|
|
542
|
+
citedCompetitors = d.competitors_cited;
|
|
543
|
+
} else if (answer) {
|
|
544
|
+
citedCompetitors = competitors.filter((c) => {
|
|
545
|
+
const cNames = [c.name, ...(c.aliases || [])].filter(Boolean);
|
|
546
|
+
return cNames.some((cn) => lowerAnswer.includes(cn.toLowerCase()));
|
|
547
|
+
}).map((c) => c.name);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (citedCompetitors.length >= 2) {
|
|
551
|
+
const promptLabel = d.prompt_id || d.prompt_text || 'category prompt';
|
|
552
|
+
const engineLabel = d.provider || d.engine || 'generative engine';
|
|
553
|
+
hits.push({
|
|
554
|
+
subject: entrySubject('prompts', promptLabel),
|
|
555
|
+
summary: `Brand erased in category response by ${engineLabel}: ${citedCompetitors.length} competitors cited (${citedCompetitors.slice(0, 3).join(', ')}), 0 first-party mentions`,
|
|
556
|
+
evidence: [
|
|
557
|
+
`engine: ${engineLabel}`,
|
|
558
|
+
`prompt: ${promptLabel}`,
|
|
559
|
+
`competitors cited: ${citedCompetitors.join(', ')}`,
|
|
560
|
+
'first-party mentions: 0',
|
|
561
|
+
],
|
|
562
|
+
confidence: 'high',
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return hits;
|
|
567
|
+
},
|
|
568
|
+
}));
|
|
569
|
+
|
|
570
|
+
D.push(defineDetector({
|
|
571
|
+
id: 'GEO-011', name: 'RAG chunk fracture: claim separated from citation across oversized section', namespace: 'GEO',
|
|
572
|
+
description: 'A section under a single heading exceeds 450 words without structural sub-headings or lists, separating claims from qualifying citations and causing retrieval fracture in RAG chunking pipelines.',
|
|
573
|
+
discipline: ['geo', 'aeo'], severity: 'medium', deterministic: true, requires: ['site'],
|
|
574
|
+
impact: { retrieval: 'medium', representation: 'high' },
|
|
575
|
+
applicable_requirement: 'GEO §4 RAG semantic chunk boundary preservation; AEO §4 modular evidence proximity',
|
|
576
|
+
remediation: 'Subdivide sections exceeding 400 words with H3 sub-headings or bullet lists, and anchor citations in the same paragraph as the claim.',
|
|
577
|
+
verification: 'Re-audit section word counts and ensure paragraphs with factual claims contain adjacent citation anchors.',
|
|
578
|
+
check(ctx) {
|
|
579
|
+
const hits = [];
|
|
580
|
+
for (const p of indexTargets(ctx)) {
|
|
581
|
+
if (p.status !== 200 || !p.rawHtml) continue;
|
|
582
|
+
const paragraphs = p.paragraphs || [];
|
|
583
|
+
if (paragraphs.length < 4) continue;
|
|
584
|
+
|
|
585
|
+
let currentHeading = 'intro';
|
|
586
|
+
let currentWords = 0;
|
|
587
|
+
let hasClaimInChunk = false;
|
|
588
|
+
const CLAIM_RX = /\b(?:\d+%\s*(?:reduction|faster|increase|savings?)|SOC\s*2|GDPR|ISO\s*27001|compliant|verified|guaranteed)\b/i;
|
|
589
|
+
const CITE_RX = /\[\d+\]|href=|\(source:|\(see\s+|according\s+to|citation/i;
|
|
590
|
+
|
|
591
|
+
for (const para of paragraphs) {
|
|
592
|
+
const words = para.split(/\s+/).length;
|
|
593
|
+
currentWords += words;
|
|
594
|
+
if (CLAIM_RX.test(para) && !CITE_RX.test(para)) {
|
|
595
|
+
hasClaimInChunk = true;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (currentWords > 450 && hasClaimInChunk) {
|
|
599
|
+
hits.push({
|
|
600
|
+
subject: pageSubject(p),
|
|
601
|
+
summary: `Oversized section under "${currentHeading.slice(0, 50)}" (${currentWords} words) risks RAG chunk fracture for unanchored claims`,
|
|
602
|
+
evidence: [
|
|
603
|
+
`section heading: "${currentHeading}"`,
|
|
604
|
+
`accumulated section words: ${currentWords}`,
|
|
605
|
+
'generative search RAG pipelines chunk text at 300-500 tokens, severing unanchored claims from qualifying context',
|
|
606
|
+
],
|
|
607
|
+
captured: { section: currentHeading, wordCount: currentWords },
|
|
608
|
+
expected: '<= 400 words per section before sub-heading',
|
|
609
|
+
});
|
|
610
|
+
break;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
return hits;
|
|
615
|
+
},
|
|
616
|
+
}));
|
|
617
|
+
|
|
618
|
+
D.push(defineDetector({
|
|
619
|
+
id: 'GEO-012', name: 'Repetitive phrase stuffing in section headings degrading dense retrieval', namespace: 'GEO',
|
|
620
|
+
description: 'Multiple headings repeat the identical multi-word phrase or entity prefix, degrading dense vector discriminability and triggering keyword stuffing penalties in neural search rankers.',
|
|
621
|
+
discipline: ['geo', 'seo'], severity: 'low', deterministic: true, requires: ['site'],
|
|
622
|
+
impact: { retrieval: 'medium', ranking: 'low' },
|
|
623
|
+
applicable_requirement: 'GEO §4 semantic vector distinctiveness; SEO §4 heading diversity and keyword stuffing avoidance',
|
|
624
|
+
remediation: 'Diversify section headings to reflect specific sub-topics, user questions, or tasks rather than repeating the same keyword stem.',
|
|
625
|
+
verification: 'Ensure no multi-word phrase is repeated across more than 3 headings.',
|
|
626
|
+
check(ctx) {
|
|
627
|
+
const hits = [];
|
|
628
|
+
for (const p of indexTargets(ctx)) {
|
|
629
|
+
if (p.status !== 200 || !p.headings || p.headings.length < 5) continue;
|
|
630
|
+
const headingTexts = p.headings.map((h) => (h.text || '').toLowerCase().trim()).filter(Boolean);
|
|
631
|
+
|
|
632
|
+
const ngramCounts = new Map();
|
|
633
|
+
for (const text of headingTexts) {
|
|
634
|
+
const words = text.split(/\s+/).filter((w) => w.length > 2);
|
|
635
|
+
for (let i = 0; i <= words.length - 2; i++) {
|
|
636
|
+
const gram2 = words.slice(i, i + 2).join(' ');
|
|
637
|
+
ngramCounts.set(gram2, (ngramCounts.get(gram2) || 0) + 1);
|
|
638
|
+
if (i <= words.length - 3) {
|
|
639
|
+
const gram3 = words.slice(i, i + 3).join(' ');
|
|
640
|
+
ngramCounts.set(gram3, (ngramCounts.get(gram3) || 0) + 1);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
let repeatedGram = null;
|
|
646
|
+
let maxCount = 0;
|
|
647
|
+
for (const [gram, count] of ngramCounts) {
|
|
648
|
+
if (count >= 4 && count > maxCount) {
|
|
649
|
+
maxCount = count;
|
|
650
|
+
repeatedGram = gram;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (repeatedGram && maxCount >= 4 && (maxCount / headingTexts.length) >= 0.4) {
|
|
655
|
+
hits.push({
|
|
656
|
+
subject: pageSubject(p),
|
|
657
|
+
summary: `Repetitive phrase "${repeatedGram}" repeated in ${maxCount}/${headingTexts.length} headings`,
|
|
658
|
+
evidence: [
|
|
659
|
+
`repeated phrase: "${repeatedGram}"`,
|
|
660
|
+
`occurrence: ${maxCount} headings (${Math.round((maxCount / headingTexts.length) * 100)}%)`,
|
|
661
|
+
'dense retrieval and bi-encoder embeddings suffer loss of discriminability from repetitive heading stems',
|
|
662
|
+
],
|
|
663
|
+
captured: { phrase: repeatedGram, count: maxCount, totalHeadings: headingTexts.length },
|
|
664
|
+
expected: 'repeated phrase in <= 3 headings',
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return hits;
|
|
669
|
+
},
|
|
670
|
+
}));
|
|
671
|
+
|
|
672
|
+
D.push(defineDetector({
|
|
673
|
+
id: 'GEO-013', name: 'AI engine assertion contradicts verified publisher claim registry', namespace: 'GEO',
|
|
674
|
+
description: 'An external AI search engine or generative model assertion directly contradicts or negates a verified publisher claim registered in claims.yaml.',
|
|
675
|
+
discipline: ['geo', 'aeo'], severity: 'high', deterministic: true, requires: ['observations', 'registries'],
|
|
676
|
+
impact: { representation: 'high', citation: 'high' },
|
|
677
|
+
applicable_requirement: 'GEO §13 AI hallucination and claim contradiction governance; AEO §2 factual integrity',
|
|
678
|
+
remediation: 'Publish explicit corroborating evidence on high-authority pages and submit model feedback/correction citations addressing the contradicted fact.',
|
|
679
|
+
verification: 'All generative AI assertions corroborate verified publisher claims without factual contradiction.',
|
|
680
|
+
check(ctx) {
|
|
681
|
+
const hits = [];
|
|
682
|
+
const claims = ctx.registries?.claims?.entries || [];
|
|
683
|
+
const verifiedClaims = claims.filter((c) => c.status === 'verified');
|
|
684
|
+
if (verifiedClaims.length === 0) return hits;
|
|
685
|
+
|
|
686
|
+
const observations = ctx.observations || [];
|
|
687
|
+
for (const obs of observations) {
|
|
688
|
+
if (obs.data?.contradiction_detected) {
|
|
689
|
+
hits.push({
|
|
690
|
+
subject: { type: 'registry_entry', identifier: `claims/${obs.data.claim_id || 'unspecified'}` },
|
|
691
|
+
summary: `AI engine ${obs.data.provider || 'unknown'} assertion contradicts verified claim "${obs.data.claim_id || 'unspecified'}"`,
|
|
692
|
+
evidence: [
|
|
693
|
+
`provider: ${obs.data.provider || 'unknown'}`,
|
|
694
|
+
`prompt: "${obs.data.prompt_text || 'prompt'}"`,
|
|
695
|
+
`contradicted assertion: "${obs.data.contradicted_assertion || obs.data.raw_response || ''}"`,
|
|
696
|
+
`claim: "${obs.data.claim_text || ''}"`,
|
|
697
|
+
],
|
|
698
|
+
captured: { provider: obs.data.provider, claim_id: obs.data.claim_id, assertion: obs.data.contradicted_assertion },
|
|
699
|
+
expected: 'AI answer corroborates verified claim',
|
|
700
|
+
});
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
if (obs.data?.raw_response || obs.data?.assertion) {
|
|
705
|
+
const responseText = (obs.data.raw_response || obs.data.assertion || '').toLowerCase();
|
|
706
|
+
for (const vc of verifiedClaims) {
|
|
707
|
+
if (!vc.claim) continue;
|
|
708
|
+
const subjectMatch = vc.claim.match(/(?:supports|provides|features|certified|complies with)\s+([^,\.]+)/i);
|
|
709
|
+
if (subjectMatch) {
|
|
710
|
+
const feature = subjectMatch[1].toLowerCase().trim();
|
|
711
|
+
const negationRegex = new RegExp(`\\b(does not support|doesn't support|lacks|no support for|not certified|unsupported|fails to support)\\s+${feature.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')}`, 'i');
|
|
712
|
+
if (negationRegex.test(responseText)) {
|
|
713
|
+
hits.push({
|
|
714
|
+
subject: { type: 'registry_entry', identifier: `claims/${vc.claim_id}` },
|
|
715
|
+
summary: `AI engine ${obs.data.provider || 'unknown'} assertion contradicts verified claim "${vc.claim_id}" (${feature})`,
|
|
716
|
+
evidence: [
|
|
717
|
+
`provider: ${obs.data.provider || 'unknown'}`,
|
|
718
|
+
`prompt: "${obs.data.prompt_text || 'prompt'}"`,
|
|
719
|
+
`contradictory assertion: "${negationRegex.exec(responseText)?.[0] || 'negated assertion'}"`,
|
|
720
|
+
`verified claim: "${vc.claim}"`,
|
|
721
|
+
],
|
|
722
|
+
captured: { provider: obs.data.provider, claim_id: vc.claim_id, feature },
|
|
723
|
+
expected: `AI answer reflects verified claim: "${vc.claim}"`,
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
return hits;
|
|
732
|
+
},
|
|
733
|
+
}));
|
|
734
|
+
|
|
327
735
|
/* ---------------- RECO: recommendation eligibility ---------------- */
|
|
328
736
|
|
|
329
737
|
const RECO_CHECKS = [
|