@nebulacomponents/citable 1.20.0 → 1.22.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 +42 -0
- package/dist/universal/.agents/skills/citable/SKILL.md +1 -1
- package/dist/universal/.agents/skills/citable/VERSION +1 -1
- package/dist/universal/.agents/skills/citable/manifest.json +18 -5
- package/dist/universal/.agents/skills/citable/schemas/citation-provenance.schema.json +59 -0
- package/dist/universal/.agents/skills/citable/schemas/claim-evidence.schema.json +21 -0
- package/dist/universal/.agents/skills/citable/schemas/claim-verification.schema.json +18 -0
- package/dist/universal/.agents/skills/citable/schemas/competitor-representation.schema.json +36 -0
- package/dist/universal/.agents/skills/citable/schemas/derivation.schema.json +18 -0
- package/dist/universal/.agents/skills/citable/schemas/external-comparison.schema.json +16 -0
- package/dist/universal/.agents/skills/citable/schemas/external-observation.schema.json +32 -0
- package/dist/universal/.agents/skills/citable/schemas/prompt-set.schema.json +47 -0
- package/dist/universal/.agents/skills/citable/schemas/representation-data.schema.json +21 -0
- package/dist/universal/.agents/skills/citable/schemas/research-dataset.schema.json +27 -0
- package/dist/universal/.agents/skills/citable/schemas/response-span.schema.json +16 -0
- package/dist/universal/.agents/skills/citable/schemas/search-discovery-data.schema.json +20 -0
- package/dist/universal/.agents/skills/citable/schemas/source-retrieval.schema.json +41 -0
- package/dist/universal/.claude/agents/citable/manifest.json +2 -2
- package/dist/universal/.claude/skills/citable/SKILL.md +1 -1
- package/dist/universal/.claude/skills/citable/VERSION +1 -1
- package/dist/universal/.claude/skills/citable/manifest.json +18 -5
- package/dist/universal/.claude/skills/citable/schemas/citation-provenance.schema.json +59 -0
- package/dist/universal/.claude/skills/citable/schemas/claim-evidence.schema.json +21 -0
- package/dist/universal/.claude/skills/citable/schemas/claim-verification.schema.json +18 -0
- package/dist/universal/.claude/skills/citable/schemas/competitor-representation.schema.json +36 -0
- package/dist/universal/.claude/skills/citable/schemas/derivation.schema.json +18 -0
- package/dist/universal/.claude/skills/citable/schemas/external-comparison.schema.json +16 -0
- package/dist/universal/.claude/skills/citable/schemas/external-observation.schema.json +32 -0
- package/dist/universal/.claude/skills/citable/schemas/prompt-set.schema.json +47 -0
- package/dist/universal/.claude/skills/citable/schemas/representation-data.schema.json +21 -0
- package/dist/universal/.claude/skills/citable/schemas/research-dataset.schema.json +27 -0
- package/dist/universal/.claude/skills/citable/schemas/response-span.schema.json +16 -0
- package/dist/universal/.claude/skills/citable/schemas/search-discovery-data.schema.json +20 -0
- package/dist/universal/.claude/skills/citable/schemas/source-retrieval.schema.json +41 -0
- package/dist/universal/.cursor/skills/citable/SKILL.md +1 -1
- package/dist/universal/.cursor/skills/citable/VERSION +1 -1
- package/dist/universal/.cursor/skills/citable/manifest.json +18 -5
- package/dist/universal/.cursor/skills/citable/schemas/citation-provenance.schema.json +59 -0
- package/dist/universal/.cursor/skills/citable/schemas/claim-evidence.schema.json +21 -0
- package/dist/universal/.cursor/skills/citable/schemas/claim-verification.schema.json +18 -0
- package/dist/universal/.cursor/skills/citable/schemas/competitor-representation.schema.json +36 -0
- package/dist/universal/.cursor/skills/citable/schemas/derivation.schema.json +18 -0
- package/dist/universal/.cursor/skills/citable/schemas/external-comparison.schema.json +16 -0
- package/dist/universal/.cursor/skills/citable/schemas/external-observation.schema.json +32 -0
- package/dist/universal/.cursor/skills/citable/schemas/prompt-set.schema.json +47 -0
- package/dist/universal/.cursor/skills/citable/schemas/representation-data.schema.json +21 -0
- package/dist/universal/.cursor/skills/citable/schemas/research-dataset.schema.json +27 -0
- package/dist/universal/.cursor/skills/citable/schemas/response-span.schema.json +16 -0
- package/dist/universal/.cursor/skills/citable/schemas/search-discovery-data.schema.json +20 -0
- package/dist/universal/.cursor/skills/citable/schemas/source-retrieval.schema.json +41 -0
- package/dist/universal/.gemini/skills/citable/SKILL.md +1 -1
- package/dist/universal/.gemini/skills/citable/VERSION +1 -1
- package/dist/universal/.gemini/skills/citable/manifest.json +18 -5
- package/dist/universal/.gemini/skills/citable/schemas/citation-provenance.schema.json +59 -0
- package/dist/universal/.gemini/skills/citable/schemas/claim-evidence.schema.json +21 -0
- package/dist/universal/.gemini/skills/citable/schemas/claim-verification.schema.json +18 -0
- package/dist/universal/.gemini/skills/citable/schemas/competitor-representation.schema.json +36 -0
- package/dist/universal/.gemini/skills/citable/schemas/derivation.schema.json +18 -0
- package/dist/universal/.gemini/skills/citable/schemas/external-comparison.schema.json +16 -0
- package/dist/universal/.gemini/skills/citable/schemas/external-observation.schema.json +32 -0
- package/dist/universal/.gemini/skills/citable/schemas/prompt-set.schema.json +47 -0
- package/dist/universal/.gemini/skills/citable/schemas/representation-data.schema.json +21 -0
- package/dist/universal/.gemini/skills/citable/schemas/research-dataset.schema.json +27 -0
- package/dist/universal/.gemini/skills/citable/schemas/response-span.schema.json +16 -0
- package/dist/universal/.gemini/skills/citable/schemas/search-discovery-data.schema.json +20 -0
- package/dist/universal/.gemini/skills/citable/schemas/source-retrieval.schema.json +41 -0
- package/dist/universal/.github/skills/citable/SKILL.md +1 -1
- package/dist/universal/.github/skills/citable/VERSION +1 -1
- package/dist/universal/.github/skills/citable/manifest.json +18 -5
- package/dist/universal/.github/skills/citable/schemas/citation-provenance.schema.json +59 -0
- package/dist/universal/.github/skills/citable/schemas/claim-evidence.schema.json +21 -0
- package/dist/universal/.github/skills/citable/schemas/claim-verification.schema.json +18 -0
- package/dist/universal/.github/skills/citable/schemas/competitor-representation.schema.json +36 -0
- package/dist/universal/.github/skills/citable/schemas/derivation.schema.json +18 -0
- package/dist/universal/.github/skills/citable/schemas/external-comparison.schema.json +16 -0
- package/dist/universal/.github/skills/citable/schemas/external-observation.schema.json +32 -0
- package/dist/universal/.github/skills/citable/schemas/prompt-set.schema.json +47 -0
- package/dist/universal/.github/skills/citable/schemas/representation-data.schema.json +21 -0
- package/dist/universal/.github/skills/citable/schemas/research-dataset.schema.json +27 -0
- package/dist/universal/.github/skills/citable/schemas/response-span.schema.json +16 -0
- package/dist/universal/.github/skills/citable/schemas/search-discovery-data.schema.json +20 -0
- package/dist/universal/.github/skills/citable/schemas/source-retrieval.schema.json +41 -0
- package/dist/universal/.kiro/skills/citable/SKILL.md +1 -1
- package/dist/universal/.kiro/skills/citable/VERSION +1 -1
- package/dist/universal/.kiro/skills/citable/manifest.json +18 -5
- package/dist/universal/.kiro/skills/citable/schemas/citation-provenance.schema.json +59 -0
- package/dist/universal/.kiro/skills/citable/schemas/claim-evidence.schema.json +21 -0
- package/dist/universal/.kiro/skills/citable/schemas/claim-verification.schema.json +18 -0
- package/dist/universal/.kiro/skills/citable/schemas/competitor-representation.schema.json +36 -0
- package/dist/universal/.kiro/skills/citable/schemas/derivation.schema.json +18 -0
- package/dist/universal/.kiro/skills/citable/schemas/external-comparison.schema.json +16 -0
- package/dist/universal/.kiro/skills/citable/schemas/external-observation.schema.json +32 -0
- package/dist/universal/.kiro/skills/citable/schemas/prompt-set.schema.json +47 -0
- package/dist/universal/.kiro/skills/citable/schemas/representation-data.schema.json +21 -0
- package/dist/universal/.kiro/skills/citable/schemas/research-dataset.schema.json +27 -0
- package/dist/universal/.kiro/skills/citable/schemas/response-span.schema.json +16 -0
- package/dist/universal/.kiro/skills/citable/schemas/search-discovery-data.schema.json +20 -0
- package/dist/universal/.kiro/skills/citable/schemas/source-retrieval.schema.json +41 -0
- package/dist/universal/.opencode/skills/citable/SKILL.md +1 -1
- package/dist/universal/.opencode/skills/citable/VERSION +1 -1
- package/dist/universal/.opencode/skills/citable/manifest.json +18 -5
- package/dist/universal/.opencode/skills/citable/schemas/citation-provenance.schema.json +59 -0
- package/dist/universal/.opencode/skills/citable/schemas/claim-evidence.schema.json +21 -0
- package/dist/universal/.opencode/skills/citable/schemas/claim-verification.schema.json +18 -0
- package/dist/universal/.opencode/skills/citable/schemas/competitor-representation.schema.json +36 -0
- package/dist/universal/.opencode/skills/citable/schemas/derivation.schema.json +18 -0
- package/dist/universal/.opencode/skills/citable/schemas/external-comparison.schema.json +16 -0
- package/dist/universal/.opencode/skills/citable/schemas/external-observation.schema.json +32 -0
- package/dist/universal/.opencode/skills/citable/schemas/prompt-set.schema.json +47 -0
- package/dist/universal/.opencode/skills/citable/schemas/representation-data.schema.json +21 -0
- package/dist/universal/.opencode/skills/citable/schemas/research-dataset.schema.json +27 -0
- package/dist/universal/.opencode/skills/citable/schemas/response-span.schema.json +16 -0
- package/dist/universal/.opencode/skills/citable/schemas/search-discovery-data.schema.json +20 -0
- package/dist/universal/.opencode/skills/citable/schemas/source-retrieval.schema.json +41 -0
- package/dist/universal/.pi/agent/skills/citable/SKILL.md +1 -1
- package/dist/universal/.pi/agent/skills/citable/VERSION +1 -1
- package/dist/universal/.pi/agent/skills/citable/manifest.json +18 -5
- package/dist/universal/.pi/agent/skills/citable/schemas/citation-provenance.schema.json +59 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/claim-evidence.schema.json +21 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/claim-verification.schema.json +18 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/competitor-representation.schema.json +36 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/derivation.schema.json +18 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/external-comparison.schema.json +16 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/external-observation.schema.json +32 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/prompt-set.schema.json +47 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/representation-data.schema.json +21 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/research-dataset.schema.json +27 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/response-span.schema.json +16 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/search-discovery-data.schema.json +20 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/source-retrieval.schema.json +41 -0
- package/dist/universal/.qoder/skills/citable/SKILL.md +1 -1
- package/dist/universal/.qoder/skills/citable/VERSION +1 -1
- package/dist/universal/.qoder/skills/citable/manifest.json +18 -5
- package/dist/universal/.qoder/skills/citable/schemas/citation-provenance.schema.json +59 -0
- package/dist/universal/.qoder/skills/citable/schemas/claim-evidence.schema.json +21 -0
- package/dist/universal/.qoder/skills/citable/schemas/claim-verification.schema.json +18 -0
- package/dist/universal/.qoder/skills/citable/schemas/competitor-representation.schema.json +36 -0
- package/dist/universal/.qoder/skills/citable/schemas/derivation.schema.json +18 -0
- package/dist/universal/.qoder/skills/citable/schemas/external-comparison.schema.json +16 -0
- package/dist/universal/.qoder/skills/citable/schemas/external-observation.schema.json +32 -0
- package/dist/universal/.qoder/skills/citable/schemas/prompt-set.schema.json +47 -0
- package/dist/universal/.qoder/skills/citable/schemas/representation-data.schema.json +21 -0
- package/dist/universal/.qoder/skills/citable/schemas/research-dataset.schema.json +27 -0
- package/dist/universal/.qoder/skills/citable/schemas/response-span.schema.json +16 -0
- package/dist/universal/.qoder/skills/citable/schemas/search-discovery-data.schema.json +20 -0
- package/dist/universal/.qoder/skills/citable/schemas/source-retrieval.schema.json +41 -0
- package/dist/universal/.rovodev/skills/citable/SKILL.md +1 -1
- package/dist/universal/.rovodev/skills/citable/VERSION +1 -1
- package/dist/universal/.rovodev/skills/citable/manifest.json +18 -5
- package/dist/universal/.rovodev/skills/citable/schemas/citation-provenance.schema.json +59 -0
- package/dist/universal/.rovodev/skills/citable/schemas/claim-evidence.schema.json +21 -0
- package/dist/universal/.rovodev/skills/citable/schemas/claim-verification.schema.json +18 -0
- package/dist/universal/.rovodev/skills/citable/schemas/competitor-representation.schema.json +36 -0
- package/dist/universal/.rovodev/skills/citable/schemas/derivation.schema.json +18 -0
- package/dist/universal/.rovodev/skills/citable/schemas/external-comparison.schema.json +16 -0
- package/dist/universal/.rovodev/skills/citable/schemas/external-observation.schema.json +32 -0
- package/dist/universal/.rovodev/skills/citable/schemas/prompt-set.schema.json +47 -0
- package/dist/universal/.rovodev/skills/citable/schemas/representation-data.schema.json +21 -0
- package/dist/universal/.rovodev/skills/citable/schemas/research-dataset.schema.json +27 -0
- package/dist/universal/.rovodev/skills/citable/schemas/response-span.schema.json +16 -0
- package/dist/universal/.rovodev/skills/citable/schemas/search-discovery-data.schema.json +20 -0
- package/dist/universal/.rovodev/skills/citable/schemas/source-retrieval.schema.json +41 -0
- package/dist/universal/.trae/skills/citable/SKILL.md +1 -1
- package/dist/universal/.trae/skills/citable/VERSION +1 -1
- package/dist/universal/.trae/skills/citable/manifest.json +18 -5
- package/dist/universal/.trae/skills/citable/schemas/citation-provenance.schema.json +59 -0
- package/dist/universal/.trae/skills/citable/schemas/claim-evidence.schema.json +21 -0
- package/dist/universal/.trae/skills/citable/schemas/claim-verification.schema.json +18 -0
- package/dist/universal/.trae/skills/citable/schemas/competitor-representation.schema.json +36 -0
- package/dist/universal/.trae/skills/citable/schemas/derivation.schema.json +18 -0
- package/dist/universal/.trae/skills/citable/schemas/external-comparison.schema.json +16 -0
- package/dist/universal/.trae/skills/citable/schemas/external-observation.schema.json +32 -0
- package/dist/universal/.trae/skills/citable/schemas/prompt-set.schema.json +47 -0
- package/dist/universal/.trae/skills/citable/schemas/representation-data.schema.json +21 -0
- package/dist/universal/.trae/skills/citable/schemas/research-dataset.schema.json +27 -0
- package/dist/universal/.trae/skills/citable/schemas/response-span.schema.json +16 -0
- package/dist/universal/.trae/skills/citable/schemas/search-discovery-data.schema.json +20 -0
- package/dist/universal/.trae/skills/citable/schemas/source-retrieval.schema.json +41 -0
- package/dist/universal/.trae-cn/skills/citable/SKILL.md +1 -1
- package/dist/universal/.trae-cn/skills/citable/VERSION +1 -1
- package/dist/universal/.trae-cn/skills/citable/manifest.json +18 -5
- package/dist/universal/.trae-cn/skills/citable/schemas/citation-provenance.schema.json +59 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/claim-evidence.schema.json +21 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/claim-verification.schema.json +18 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/competitor-representation.schema.json +36 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/derivation.schema.json +18 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/external-comparison.schema.json +16 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/external-observation.schema.json +32 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/prompt-set.schema.json +47 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/representation-data.schema.json +21 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/research-dataset.schema.json +27 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/response-span.schema.json +16 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/search-discovery-data.schema.json +20 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/source-retrieval.schema.json +41 -0
- package/dist/universal/manifest.json +220 -64
- package/package.json +1 -1
- package/schemas/citation-provenance.schema.json +59 -0
- package/schemas/claim-evidence.schema.json +21 -0
- package/schemas/claim-verification.schema.json +18 -0
- package/schemas/competitor-representation.schema.json +36 -0
- package/schemas/derivation.schema.json +18 -0
- package/schemas/external-comparison.schema.json +16 -0
- package/schemas/external-observation.schema.json +32 -0
- package/schemas/prompt-set.schema.json +47 -0
- package/schemas/representation-data.schema.json +21 -0
- package/schemas/research-dataset.schema.json +27 -0
- package/schemas/response-span.schema.json +16 -0
- package/schemas/search-discovery-data.schema.json +20 -0
- package/schemas/source-retrieval.schema.json +41 -0
- package/skill/SKILL.md +1 -1
- package/src/connectors/gsc.js +4 -0
- package/src/discovery/searchConsole.js +186 -0
- package/src/evidence/external.js +251 -0
- package/src/representation/competitors.js +113 -0
- package/src/representation/prompts.js +117 -0
- package/src/representation/provenance.js +214 -0
- package/src/representation/representation.js +186 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { createExternalObservation, verifyExternalObservation } from '../evidence/external.js';
|
|
2
|
+
import { canonicalEvidenceJson } from '../evidence/hashes.js';
|
|
3
|
+
import { sha256, nowIso } from '../shared/io.js';
|
|
4
|
+
import { validateAgainst } from '../shared/schemaValidator.js';
|
|
5
|
+
import { providerRequest } from '../connectors/http.js';
|
|
6
|
+
import { collectionResult, errorMessage } from '../connectors/collectionResult.js';
|
|
7
|
+
|
|
8
|
+
const SEARCH_DATA_SCHEMA = 'search-discovery-data.schema.json';
|
|
9
|
+
const BASE = 'https://www.googleapis.com/webmasters/v3';
|
|
10
|
+
const DIMENSIONS = new Set(['date', 'query', 'page', 'country', 'device', 'searchAppearance']);
|
|
11
|
+
const COLLECTOR = { id: 'citable-search-console-observations', version: '1.0.0' };
|
|
12
|
+
const PARSER = { id: 'google-search-analytics-response', version: '1.0.0' };
|
|
13
|
+
|
|
14
|
+
function assertDate(value, name) {
|
|
15
|
+
if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(value) || Number.isNaN(Date.parse(`${value}T00:00:00Z`))) {
|
|
16
|
+
throw new TypeError(`${name} must be an ISO date (YYYY-MM-DD)`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function validateRequest(request) {
|
|
21
|
+
if (!request || typeof request !== 'object' || Array.isArray(request)) throw new TypeError('Search Console request is required');
|
|
22
|
+
if (typeof request.property_id !== 'string' || !request.property_id) throw new TypeError('property_id is required');
|
|
23
|
+
assertDate(request.start_date, 'start_date');
|
|
24
|
+
assertDate(request.end_date, 'end_date');
|
|
25
|
+
if (request.start_date > request.end_date) throw new RangeError('start_date must not be after end_date');
|
|
26
|
+
const dimensions = request.dimensions ?? ['query', 'page'];
|
|
27
|
+
if (!Array.isArray(dimensions) || dimensions.length === 0 || dimensions.some((item) => !DIMENSIONS.has(item))) throw new TypeError('dimensions contain an unsupported Search Console dimension');
|
|
28
|
+
const filters = request.filters ?? {};
|
|
29
|
+
if (!filters || typeof filters !== 'object' || Array.isArray(filters)) throw new TypeError('filters must be an object');
|
|
30
|
+
const rowLimit = request.row_limit ?? 25000;
|
|
31
|
+
if (!Number.isInteger(rowLimit) || rowLimit < 1 || rowLimit > 25000) throw new RangeError('row_limit must be an integer from 1 to 25000');
|
|
32
|
+
return { property_id: request.property_id, start_date: request.start_date, end_date: request.end_date, dimensions: [...new Set(dimensions)], filters, row_limit: rowLimit };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function configurationFor(request) {
|
|
36
|
+
const configuration = { property_id: request.property_id, dimensions: request.dimensions, filters: request.filters, row_limit: request.row_limit };
|
|
37
|
+
return { id: `gsc-${sha256(canonicalEvidenceJson(configuration)).slice(0, 16)}`, version: '1.0.0' };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function numberOrNull(value) {
|
|
41
|
+
return value == null || value === '' || !Number.isFinite(Number(value)) ? null : Number(value);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function requestBody(request, startRow) {
|
|
45
|
+
const body = { startDate: request.start_date, endDate: request.end_date, dimensions: request.dimensions, rowLimit: request.row_limit, startRow, dataState: 'final' };
|
|
46
|
+
if (Object.keys(request.filters).length) body.dimensionFilterGroups = [{ groupType: 'and', filters: Object.entries(request.filters).map(([dimension, expression]) => ({ dimension, operator: 'equals', expression: String(expression) })) }];
|
|
47
|
+
return body;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function normalizeRow(row, request, dimensions, pageStart, coverageStatus, providerTotal, responseHash, origin) {
|
|
51
|
+
if (!row || !Array.isArray(row.keys)) throw new TypeError('Search Console row is malformed');
|
|
52
|
+
const values = Object.fromEntries(dimensions.map((name, index) => [name, row.keys[index] ?? null]));
|
|
53
|
+
const data = {
|
|
54
|
+
query: values.query ?? null,
|
|
55
|
+
page: values.page ?? null,
|
|
56
|
+
interval: { start_date: request.start_date, end_date: request.end_date },
|
|
57
|
+
metrics: { impressions: numberOrNull(row.impressions), clicks: numberOrNull(row.clicks), ctr: numberOrNull(row.ctr), average_position: numberOrNull(row.position) },
|
|
58
|
+
dimensions: { country: values.country ?? null, device: values.device ?? null, search_appearance: values.searchAppearance ?? null, date: values.date ?? null },
|
|
59
|
+
request: { property_id: request.property_id, dimensions: request.dimensions, filters: request.filters, row_limit: request.row_limit },
|
|
60
|
+
provider_reported: { row_count: providerTotal, page_start: pageStart },
|
|
61
|
+
coverage_status: coverageStatus,
|
|
62
|
+
response_hash: responseHash,
|
|
63
|
+
evidence_origin: origin,
|
|
64
|
+
};
|
|
65
|
+
const check = validateAgainst(SEARCH_DATA_SCHEMA, data);
|
|
66
|
+
if (!check.valid) throw new TypeError(`search discovery data violates contract: ${check.errors.join('; ')}`);
|
|
67
|
+
const subjectId = `query:${data.query ?? ''}|page:${data.page ?? ''}`;
|
|
68
|
+
return createExternalObservation({
|
|
69
|
+
observation_id: `GSC-${sha256(canonicalEvidenceJson({ request, values, row })).slice(0, 24)}`,
|
|
70
|
+
observation_type: 'EXTERNAL_OBSERVATION',
|
|
71
|
+
subject: { type: 'search_query_page', id: subjectId || `row:${pageStart}` },
|
|
72
|
+
source: { provider: 'google_search_console', method: 'search_analytics_api', authority: 'external_provider', account_or_property: request.property_id },
|
|
73
|
+
collector: COLLECTOR,
|
|
74
|
+
parser: PARSER,
|
|
75
|
+
configuration: configurationFor(request),
|
|
76
|
+
observed_at: nowIso(),
|
|
77
|
+
retrieved_at: nowIso(),
|
|
78
|
+
population: { declared: { id: `GSC-${sha256(canonicalEvidenceJson(request)).slice(0, 16)}`, size: providerTotal }, observed: 1, retrieved: 1, evaluated: 1, unavailable: 0, failed: 0 },
|
|
79
|
+
observation_status: coverageStatus === 'complete' ? 'observed' : 'incomplete',
|
|
80
|
+
retrieval_status: coverageStatus === 'complete' ? 'retrieved' : coverageStatus === 'indeterminate' ? 'indeterminate' : 'partial',
|
|
81
|
+
data,
|
|
82
|
+
lineage: { source_evidence: [responseHash] },
|
|
83
|
+
limitations: ['Search Console reports provider-returned search analytics rows, not universal search demand.', 'Provider privacy filtering, aggregation, and reporting revisions may limit representativeness.'],
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Collect normalized Search Console search-analytics observations. This is a
|
|
89
|
+
* provider adapter: credentials are supplied only to the transport and are
|
|
90
|
+
* never written into observations or continuation state.
|
|
91
|
+
*/
|
|
92
|
+
export async function collectSearchConsoleObservations({ connection, request, token, fetchImpl = globalThis.fetch, maxPages = 1000, evidenceOrigin = 'LIVE_PROVIDER' } = {}) {
|
|
93
|
+
const normalized = validateRequest(request);
|
|
94
|
+
if (!['LIVE_PROVIDER', 'RECORDED_FIXTURE', 'SYNTHETIC_TEST'].includes(evidenceOrigin)) throw new TypeError('evidenceOrigin is invalid');
|
|
95
|
+
if (!Number.isInteger(maxPages) || maxPages < 1 || maxPages > 10000) throw new RangeError('maxPages must be an integer from 1 to 10000');
|
|
96
|
+
if (connection?.property_id && connection.property_id !== normalized.property_id) throw new Error('connection property_id does not match request property_id');
|
|
97
|
+
const dimensions = normalized.dimensions;
|
|
98
|
+
const rows = [];
|
|
99
|
+
const observations = [];
|
|
100
|
+
const errors = [];
|
|
101
|
+
let startRow = 0;
|
|
102
|
+
let providerTotal = null;
|
|
103
|
+
let continuationState = null;
|
|
104
|
+
let pagesRequested = 0;
|
|
105
|
+
let pagesRetrieved = 0;
|
|
106
|
+
let lastResponseHash = null;
|
|
107
|
+
let response;
|
|
108
|
+
do {
|
|
109
|
+
pagesRequested += 1;
|
|
110
|
+
try {
|
|
111
|
+
response = await providerRequest(`${BASE}/sites/${encodeURIComponent(normalized.property_id)}/searchAnalytics/query`, {
|
|
112
|
+
token, fetchImpl, method: 'POST', body: requestBody(normalized, startRow),
|
|
113
|
+
});
|
|
114
|
+
if (!response || !Array.isArray(response.rows)) throw new Error('Search Console response rows must be an array');
|
|
115
|
+
} catch (error) {
|
|
116
|
+
errors.push(`start row ${startRow}: ${errorMessage(error)}`);
|
|
117
|
+
continuationState = { start_row: startRow };
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
pagesRetrieved += 1;
|
|
121
|
+
lastResponseHash = sha256(canonicalEvidenceJson(response));
|
|
122
|
+
if (Number.isInteger(response.rowCount) && response.rowCount >= 0) providerTotal = response.rowCount;
|
|
123
|
+
rows.push(...response.rows.map((row) => ({ row, pageStart: startRow, responseHash: lastResponseHash })));
|
|
124
|
+
const moreRows = providerTotal !== null && rows.length < providerTotal;
|
|
125
|
+
if (providerTotal !== null && !moreRows) break;
|
|
126
|
+
if (providerTotal === null && response.rows.length < normalized.row_limit) break;
|
|
127
|
+
startRow += normalized.row_limit;
|
|
128
|
+
if (pagesRequested >= maxPages) {
|
|
129
|
+
continuationState = { start_row: startRow };
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
} while (true);
|
|
133
|
+
|
|
134
|
+
const collection = collectionResult({
|
|
135
|
+
items: rows,
|
|
136
|
+
paginationState: { pages_requested: pagesRequested, pages_retrieved: pagesRetrieved, boundary: { max_pages: maxPages, row_limit: normalized.row_limit } },
|
|
137
|
+
providerReportedTotal: providerTotal,
|
|
138
|
+
continuationState,
|
|
139
|
+
providerCompleteness: providerTotal !== null ? 'declared' : 'unknown',
|
|
140
|
+
limitations: ['Search Console privacy filtering and aggregation apply.', 'A complete result is complete only for the declared API request; it is not a census of all search behavior.'],
|
|
141
|
+
errors,
|
|
142
|
+
});
|
|
143
|
+
const normalizedRows = rows.map(({ row, pageStart, responseHash }) => normalizeRow(row, normalized, dimensions, pageStart, collection.coverage_status, providerTotal, responseHash || sha256(canonicalEvidenceJson({ request: normalized, rows: [] })), evidenceOrigin));
|
|
144
|
+
observations.push(...normalizedRows);
|
|
145
|
+
return { observations, collection, request: normalized, provider: 'google_search_console', response_hash: lastResponseHash, errors };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function compareSearchDiscoveryObservations(left, right) {
|
|
149
|
+
if (!verifyExternalObservation(left).valid || !verifyExternalObservation(right).valid) return { status: 'INDETERMINATE', dimensions: {}, limitations: ['Observation integrity could not be verified.'] };
|
|
150
|
+
const dimensions = {
|
|
151
|
+
provider: left.source.provider === right.source.provider ? 'match' : 'mismatch',
|
|
152
|
+
property: left.source.account_or_property === right.source.account_or_property ? 'match' : 'mismatch',
|
|
153
|
+
query: left.data.query === right.data.query ? 'match' : 'mismatch',
|
|
154
|
+
page: left.data.page === right.data.page ? 'match' : 'mismatch',
|
|
155
|
+
configuration: left.configuration?.id === right.configuration?.id ? 'match' : 'mismatch',
|
|
156
|
+
interval: JSON.stringify(left.data.interval) === JSON.stringify(right.data.interval) ? 'match' : 'mismatch',
|
|
157
|
+
coverage: left.data.coverage_status === right.data.coverage_status ? 'match' : 'mismatch',
|
|
158
|
+
};
|
|
159
|
+
const hardMismatch = ['provider', 'property', 'query', 'page', 'configuration'].some((key) => dimensions[key] === 'mismatch');
|
|
160
|
+
const incomplete = [left.data.coverage_status, right.data.coverage_status].some((status) => !['complete'].includes(status));
|
|
161
|
+
const status = hardMismatch ? 'NOT_COMPARABLE' : incomplete ? 'INDETERMINATE' : dimensions.interval === 'mismatch' ? 'PARTIALLY_COMPARABLE' : 'COMPARABLE';
|
|
162
|
+
return {
|
|
163
|
+
comparison_id: `GSC-CMP-${sha256(canonicalEvidenceJson({ left: left.observation_id, right: right.observation_id, dimensions })).slice(0, 16)}`,
|
|
164
|
+
left_reference: left.observation_id,
|
|
165
|
+
right_reference: right.observation_id,
|
|
166
|
+
status,
|
|
167
|
+
dimensions,
|
|
168
|
+
limitations: status === 'COMPARABLE' ? [] : ['Search Console interval, configuration, and coverage limitations remain attached; no business opportunity or ranking conclusion is inferred.'],
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function validSearchObservations(observations) {
|
|
173
|
+
return (Array.isArray(observations) ? observations : []).filter((item) => verifyExternalObservation(item).valid && item.source?.provider === 'google_search_console');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function pagesForQuery(observations, query) {
|
|
177
|
+
return validSearchObservations(observations).filter((item) => item.data.query === query && item.data.page).map((item) => item);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function queriesForPage(observations, page) {
|
|
181
|
+
return validSearchObservations(observations).filter((item) => item.data.page === page && item.data.query).map((item) => item);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function relationshipObservations(observations, { query = null, page = null } = {}) {
|
|
185
|
+
return validSearchObservations(observations).filter((item) => (query == null || item.data.query === query) && (page == null || item.data.page === page));
|
|
186
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { canonicalEvidenceJson, createEvidenceHashes } from './hashes.js';
|
|
2
|
+
import { sha256 } from '../shared/io.js';
|
|
3
|
+
import { validateAgainst } from '../shared/schemaValidator.js';
|
|
4
|
+
|
|
5
|
+
const OBSERVATION_SCHEMA = 'external-observation.schema.json';
|
|
6
|
+
const DATASET_SCHEMA = 'research-dataset.schema.json';
|
|
7
|
+
const DERIVATION_SCHEMA = 'derivation.schema.json';
|
|
8
|
+
const CLAIM_SCHEMA = 'claim-evidence.schema.json';
|
|
9
|
+
const COMPARISON_SCHEMA = 'external-comparison.schema.json';
|
|
10
|
+
const VERIFICATION_SCHEMA = 'claim-verification.schema.json';
|
|
11
|
+
|
|
12
|
+
function without(value, key) {
|
|
13
|
+
const copy = { ...value };
|
|
14
|
+
delete copy[key];
|
|
15
|
+
return copy;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function integrity(value, key) {
|
|
19
|
+
return sha256(canonicalEvidenceJson(without(value, key)));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function validate(schema, value, label) {
|
|
23
|
+
const result = validateAgainst(schema, value);
|
|
24
|
+
if (!result.valid) throw new Error(`${label} violates contract: ${result.errors.join('; ')}`);
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function createExternalObservation(input) {
|
|
29
|
+
const item = { ...input };
|
|
30
|
+
item.evidence_hash = integrity(item, 'evidence_hash');
|
|
31
|
+
return validate(OBSERVATION_SCHEMA, item, 'external observation');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function verifyExternalObservation(item) {
|
|
35
|
+
const failures = [];
|
|
36
|
+
const check = validateAgainst(OBSERVATION_SCHEMA, item);
|
|
37
|
+
if (!check.valid) failures.push(...check.errors);
|
|
38
|
+
if (item?.evidence_hash !== integrity(item || {}, 'evidence_hash')) failures.push('evidence hash is inconsistent');
|
|
39
|
+
return { valid: failures.length === 0, failures };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function createResearchDataset(input, { previous = null } = {}) {
|
|
43
|
+
const item = { ...input };
|
|
44
|
+
item.dataset_hash = integrity(item, 'dataset_hash');
|
|
45
|
+
validate(DATASET_SCHEMA, item, 'research dataset');
|
|
46
|
+
if (previous && previous.dataset_id === item.dataset_id && previous.dataset_version === item.dataset_version
|
|
47
|
+
&& previous.dataset_hash !== item.dataset_hash) {
|
|
48
|
+
throw new Error('research dataset version is immutable; create a new dataset_version');
|
|
49
|
+
}
|
|
50
|
+
return item;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function verifyResearchDataset(item, { previous = null } = {}) {
|
|
54
|
+
const failures = [];
|
|
55
|
+
const check = validateAgainst(DATASET_SCHEMA, item);
|
|
56
|
+
if (!check.valid) failures.push(...check.errors);
|
|
57
|
+
if (item?.dataset_hash !== integrity(item || {}, 'dataset_hash')) failures.push('dataset hash is inconsistent');
|
|
58
|
+
if (previous && previous.dataset_id === item?.dataset_id && previous.dataset_version === item?.dataset_version
|
|
59
|
+
&& previous.dataset_hash !== item?.dataset_hash) failures.push('research dataset version is immutable');
|
|
60
|
+
if (failures.length) throw new Error(`research dataset verification failed: ${failures.join('; ')}`);
|
|
61
|
+
return { valid: true, failures: [] };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function createDerivation(input) {
|
|
65
|
+
const item = { ...input };
|
|
66
|
+
item.derivation_hash = integrity(item, 'derivation_hash');
|
|
67
|
+
return validate(DERIVATION_SCHEMA, item, 'derivation');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function verifyDerivation(item) {
|
|
71
|
+
const check = validateAgainst(DERIVATION_SCHEMA, item);
|
|
72
|
+
if (!check.valid || item.derivation_hash !== integrity(item, 'derivation_hash')) return false;
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function createEvidenceClaim(input) {
|
|
77
|
+
const item = { ...input };
|
|
78
|
+
item.claim_hash = integrity(item, 'claim_hash');
|
|
79
|
+
return validate(CLAIM_SCHEMA, item, 'claim evidence');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function verifyClaimIdentity(item) {
|
|
83
|
+
const check = validateAgainst(CLAIM_SCHEMA, item);
|
|
84
|
+
return check.valid && item.claim_hash === integrity(item, 'claim_hash');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function valueAt(object, path) {
|
|
88
|
+
return String(path).split('.').reduce((value, key) => value == null ? undefined : value[key], object);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function reason(code, message) {
|
|
92
|
+
return { code, message };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function evaluatePredicate(assertion, observations) {
|
|
96
|
+
if (!observations.length) return { status: 'NOT_OBSERVED', reasons: [reason('observation_missing', 'No referenced observation was available.')] };
|
|
97
|
+
const results = observations.map((item) => valueAt(item, assertion.path) === assertion.value);
|
|
98
|
+
if (results.every(Boolean)) return { status: 'SUPPORTED', reasons: [] };
|
|
99
|
+
if (results.some(Boolean)) return { status: 'PARTIALLY_SUPPORTED', reasons: [reason('predicate_partial', 'The predicate matched only part of the referenced population.')] };
|
|
100
|
+
return { status: 'CONTRADICTED', reasons: [reason('predicate_contradicted', 'In-scope evidence establishes an incompatible predicate value.')] };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function evaluateAssertion(assertion, observations, derivations) {
|
|
104
|
+
if (!assertion || typeof assertion !== 'object') return { status: 'INDETERMINATE', reasons: [reason('assertion_missing', 'The claim has no structured assertion.')] };
|
|
105
|
+
if (assertion.type === 'predicate' || assertion.type === 'all') return evaluatePredicate(assertion, observations);
|
|
106
|
+
if (assertion.type === 'count') {
|
|
107
|
+
const derivation = derivations[0];
|
|
108
|
+
if (!derivation || !verifyDerivation(derivation)) return { status: 'INDETERMINATE', reasons: [reason('derivation_unavailable', 'The referenced derivation is missing or unverifiable.')] };
|
|
109
|
+
if (derivation.result?.count === assertion.expected) return { status: 'SUPPORTED', reasons: [] };
|
|
110
|
+
return { status: 'CONTRADICTED', reasons: [reason('count_contradicted', 'The preserved derivation result differs from the claimed value.')] };
|
|
111
|
+
}
|
|
112
|
+
if (assertion.type === 'compound') {
|
|
113
|
+
const results = (assertion.assertions || []).map((part) => evaluateAssertion(part, observations, derivations));
|
|
114
|
+
if (!results.length) return { status: 'INDETERMINATE', reasons: [reason('compound_empty', 'The compound claim has no components.')] };
|
|
115
|
+
const reasons = results.flatMap((item) => item.reasons);
|
|
116
|
+
if (results.every((item) => item.status === 'SUPPORTED')) return { status: 'SUPPORTED', reasons };
|
|
117
|
+
if (results.some((item) => item.status === 'CONTRADICTED')) return { status: 'PARTIALLY_SUPPORTED', reasons: [...reasons, reason('compound_component_unsupported', 'At least one compound component is contradicted.')] };
|
|
118
|
+
if (results.some((item) => item.status === 'SUPPORTED')) return { status: 'PARTIALLY_SUPPORTED', reasons: [...reasons, reason('compound_component_unsupported', 'At least one compound component is unsupported.')] };
|
|
119
|
+
return { status: results.some((item) => item.status === 'NOT_OBSERVED') ? 'NOT_OBSERVED' : 'UNSUPPORTED', reasons };
|
|
120
|
+
}
|
|
121
|
+
return { status: 'INDETERMINATE', reasons: [reason('assertion_unsupported', `Assertion type ${assertion.type || 'unknown'} is not supported.`)] };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function scopeSupport(claim, datasets, observations) {
|
|
125
|
+
const scope = claim.declared_scope;
|
|
126
|
+
if (scope.generalization === 'universal') return { status: 'UNSUPPORTED', reasons: [reason('generalization_outside_population', 'The evidence population does not support a universal claim.')] };
|
|
127
|
+
if (scope.type === 'observation' && scope.observation_reference
|
|
128
|
+
&& !(claim.observation_references || []).includes(scope.observation_reference)) {
|
|
129
|
+
return { status: 'INDETERMINATE', reasons: [reason('scope_reference_mismatch', 'The declared observation scope is not among the claim evidence references.')] };
|
|
130
|
+
}
|
|
131
|
+
if (scope.type === 'dataset' && scope.dataset_reference
|
|
132
|
+
&& !(claim.dataset_references || []).includes(scope.dataset_reference)) {
|
|
133
|
+
return { status: 'INDETERMINATE', reasons: [reason('scope_reference_mismatch', 'The declared dataset scope is not among the claim evidence references.')] };
|
|
134
|
+
}
|
|
135
|
+
if (scope.type === 'dataset') {
|
|
136
|
+
const dataset = datasets[0];
|
|
137
|
+
if (!dataset || !verifyDatasetSafe(dataset)) return { status: 'INDETERMINATE', reasons: [reason('dataset_unavailable', 'The declared dataset is missing or unverifiable.')] };
|
|
138
|
+
if (dataset.coverage_status !== 'complete_for_declared_population') return { status: 'PARTIALLY_SUPPORTED', reasons: [reason('dataset_coverage_incomplete', `Dataset coverage is ${dataset.coverage_status}, not complete for the declared population.`)] };
|
|
139
|
+
const referenced = new Set(claim.observation_references || []);
|
|
140
|
+
const missingMembers = (dataset.observation_references || []).filter((id) => !referenced.has(id));
|
|
141
|
+
if (missingMembers.length) return { status: 'PARTIALLY_SUPPORTED', reasons: [reason('dataset_members_missing', `The claim does not reference every observation in the declared dataset: ${missingMembers.join(', ')}`)] };
|
|
142
|
+
}
|
|
143
|
+
if (!observations.length) return { status: 'NOT_OBSERVED', reasons: [reason('observation_missing', 'The declared scope has no observed members.')] };
|
|
144
|
+
return { status: 'SUPPORTED', reasons: [] };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function verifyDatasetSafe(dataset) {
|
|
148
|
+
try { return verifyResearchDataset(dataset).valid; } catch { return false; }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function datasetReference(dataset) {
|
|
152
|
+
return `${dataset.dataset_id}@${dataset.dataset_version}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function resolveDatasetReference(reference, datasets) {
|
|
156
|
+
const exact = datasets.filter((item) => datasetReference(item) === reference);
|
|
157
|
+
if (exact.length === 1) return exact[0];
|
|
158
|
+
// Legacy id-only references remain valid only while the id has one version.
|
|
159
|
+
// Once versions diverge, silently selecting one would rewrite historical
|
|
160
|
+
// claim meaning.
|
|
161
|
+
if (!String(reference).includes('@')) {
|
|
162
|
+
const byId = datasets.filter((item) => item.dataset_id === reference);
|
|
163
|
+
return byId.length === 1 ? byId[0] : null;
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function combine(proposition, scope) {
|
|
169
|
+
if (proposition.status === 'NOT_OBSERVED') return 'NOT_OBSERVED';
|
|
170
|
+
if (proposition.status === 'INDETERMINATE' || scope.status === 'INDETERMINATE') return 'INDETERMINATE';
|
|
171
|
+
if (proposition.status === 'CONTRADICTED') return 'CONTRADICTED';
|
|
172
|
+
if (proposition.status === 'PARTIALLY_SUPPORTED' || scope.status === 'PARTIALLY_SUPPORTED' || scope.status === 'UNSUPPORTED') return 'PARTIALLY_SUPPORTED';
|
|
173
|
+
if (proposition.status === 'UNSUPPORTED') return 'UNSUPPORTED';
|
|
174
|
+
return 'SUPPORTED';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function verifyClaim(claim, { observations = [], datasets = [], derivations = [] } = {}) {
|
|
178
|
+
if (!verifyClaimIdentity(claim)) {
|
|
179
|
+
return {
|
|
180
|
+
claim_id: claim?.claim_id || 'unknown', claim_version: claim?.claim_version || 1,
|
|
181
|
+
overall: 'INDETERMINATE', proposition_support: 'INDETERMINATE', scope_support: 'INDETERMINATE',
|
|
182
|
+
reasons: [reason('claim_unverifiable', 'Claim identity, schema, or integrity hash could not be verified.')],
|
|
183
|
+
lineage: { claim_reference: claim?.claim_id || 'unknown', observation_references: claim?.observation_references || [], dataset_references: claim?.dataset_references || [], derivation_references: claim?.derivation_references || [] }, limitations: ['Claim evidence could not be verified.'],
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
const byId = new Map(observations.map((item) => [item.observation_id, item]));
|
|
187
|
+
const referenced = (claim.observation_references || []).map((id) => byId.get(id)).filter(Boolean);
|
|
188
|
+
const missing = (claim.observation_references || []).filter((id) => !byId.has(id));
|
|
189
|
+
const integrityFailure = referenced.some((item) => !verifyExternalObservation(item).valid);
|
|
190
|
+
let proposition;
|
|
191
|
+
if (integrityFailure) proposition = { status: 'INDETERMINATE', reasons: [reason('evidence_unverifiable', 'Referenced observation integrity verification failed.')] };
|
|
192
|
+
else if (missing.length) proposition = { status: 'NOT_OBSERVED', reasons: [reason('observation_missing', `Missing referenced observations: ${missing.join(', ')}`)] };
|
|
193
|
+
else proposition = evaluateAssertion(claim.assertion, referenced, (claim.derivation_references || []).map((id) => derivations.find((item) => item.derivation_id === id)).filter(Boolean));
|
|
194
|
+
const selectedDatasets = (claim.dataset_references || []).map((reference) => resolveDatasetReference(reference, datasets)).filter(Boolean);
|
|
195
|
+
const missingDatasets = (claim.dataset_references || []).filter((reference) => !resolveDatasetReference(reference, datasets));
|
|
196
|
+
const scope = scopeSupport(claim, selectedDatasets, referenced);
|
|
197
|
+
if (missingDatasets.length && claim.declared_scope.type === 'dataset') {
|
|
198
|
+
scope.status = 'INDETERMINATE';
|
|
199
|
+
scope.reasons.push(reason('dataset_reference_ambiguous_or_missing', `Dataset references could not be resolved exactly: ${missingDatasets.join(', ')}`));
|
|
200
|
+
}
|
|
201
|
+
const result = {
|
|
202
|
+
claim_id: claim.claim_id, claim_version: claim.claim_version, overall: combine(proposition, scope),
|
|
203
|
+
proposition_support: proposition.status, scope_support: scope.status,
|
|
204
|
+
reasons: [...proposition.reasons, ...scope.reasons],
|
|
205
|
+
lineage: { claim_reference: claim.claim_id, observation_references: claim.observation_references || [], dataset_references: claim.dataset_references || [], derivation_references: claim.derivation_references || [] },
|
|
206
|
+
limitations: claim.limitations || [],
|
|
207
|
+
};
|
|
208
|
+
return validate(VERIFICATION_SCHEMA, result, 'claim verification');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const COMPARISON_DIMENSIONS = ['source', 'subject', 'collector', 'parser', 'configuration', 'population', 'normalization', 'dataset_version', 'derivation_version'];
|
|
212
|
+
|
|
213
|
+
function dimensionValue(item, dimension) {
|
|
214
|
+
if (dimension === 'source') return item.source?.provider;
|
|
215
|
+
if (dimension === 'subject') return item.subject?.id;
|
|
216
|
+
if (dimension === 'collector') return `${item.collector?.id || ''}@${item.collector?.version || ''}` || null;
|
|
217
|
+
if (dimension === 'parser') return item.parser ? `${item.parser.id}@${item.parser.version}` : null;
|
|
218
|
+
if (dimension === 'configuration') return item.configuration ? `${item.configuration.id}@${item.configuration.version}` : null;
|
|
219
|
+
if (dimension === 'population') return item.population?.declared?.id || item.declared_population?.id;
|
|
220
|
+
if (dimension === 'normalization') return item.normalization ? `${item.normalization.method}@${item.normalization.version}` : null;
|
|
221
|
+
if (dimension === 'dataset_version') return item.dataset_id ? `${item.dataset_id}@${item.dataset_version}` : null;
|
|
222
|
+
if (dimension === 'derivation_version') return item.derivation_id ? `${item.derivation_id}@${item.derivation_version}` : null;
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function compareItems(left, right, leftReference, rightReference, dimensionsToCompare = COMPARISON_DIMENSIONS) {
|
|
227
|
+
const dimensions = {};
|
|
228
|
+
for (const key of dimensionsToCompare) {
|
|
229
|
+
const a = dimensionValue(left, key); const b = dimensionValue(right, key);
|
|
230
|
+
dimensions[key] = a == null || b == null ? 'missing' : a === b ? 'match' : 'mismatch';
|
|
231
|
+
}
|
|
232
|
+
const mismatches = Object.values(dimensions).filter((x) => x === 'mismatch').length;
|
|
233
|
+
const missing = Object.values(dimensions).filter((x) => x === 'missing').length;
|
|
234
|
+
const status = mismatches ? 'NOT_COMPARABLE' : missing ? 'PARTIALLY_COMPARABLE' : 'COMPARABLE';
|
|
235
|
+
const result = { comparison_id: `CMP-${sha256(canonicalEvidenceJson({ leftReference, rightReference, dimensions })).slice(0, 16)}`, left_reference: leftReference, right_reference: rightReference, status, dimensions, limitations: status === 'COMPARABLE' ? [] : ['No delta should be generalized beyond the aligned dimensions.'] };
|
|
236
|
+
return validate(COMPARISON_SCHEMA, result, 'external comparison');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function compareExternalObservations(left, right) {
|
|
240
|
+
if (!verifyExternalObservation(left).valid || !verifyExternalObservation(right).valid) {
|
|
241
|
+
return { comparison_id: `CMP-${sha256(`${left?.observation_id || ''}:${right?.observation_id || ''}`).slice(0, 16)}`, left_reference: left?.observation_id || 'unknown', right_reference: right?.observation_id || 'unknown', status: 'INDETERMINATE', dimensions: {}, limitations: ['Observation integrity could not be verified.'] };
|
|
242
|
+
}
|
|
243
|
+
return compareItems(left, right, left.observation_id, right.observation_id,
|
|
244
|
+
['source', 'subject', 'collector', 'parser', 'configuration', 'population']);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function compareResearchDatasets(left, right) {
|
|
248
|
+
if (!verifyDatasetSafe(left) || !verifyDatasetSafe(right)) return { comparison_id: 'CMP-invalid', left_reference: left?.dataset_id || 'unknown', right_reference: right?.dataset_id || 'unknown', status: 'INDETERMINATE', dimensions: {}, limitations: ['Dataset integrity could not be verified.'] };
|
|
249
|
+
return compareItems(left, right, `${left.dataset_id}@${left.dataset_version}`, `${right.dataset_id}@${right.dataset_version}`,
|
|
250
|
+
['dataset_version', 'normalization', 'population']);
|
|
251
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { createExternalObservation, verifyExternalObservation, compareExternalObservations } from '../evidence/external.js';
|
|
2
|
+
import { canonicalEvidenceJson } from '../evidence/hashes.js';
|
|
3
|
+
import { sha256, nowIso } from '../shared/io.js';
|
|
4
|
+
import { validateAgainst } from '../shared/schemaValidator.js';
|
|
5
|
+
import { createResponseSpan } from './provenance.js';
|
|
6
|
+
|
|
7
|
+
const SCHEMA = 'competitor-representation.schema.json';
|
|
8
|
+
const COLLECTOR = { id: 'citable-competitor-representation', version: '1.0.0' };
|
|
9
|
+
const PARSER = { id: 'provider-neutral-competitor-parser', version: '1.0.0' };
|
|
10
|
+
const TYPES = new Set(['MENTION', 'COMPARISON', 'ALTERNATIVE', 'RECOMMENDATION', 'CITED_SOURCE', 'CATEGORY_PEER']);
|
|
11
|
+
|
|
12
|
+
function validate(value) {
|
|
13
|
+
const result = validateAgainst(SCHEMA, value);
|
|
14
|
+
if (!result.valid) throw new TypeError(`competitor representation violates contract: ${result.errors.join('; ')}`);
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function requireExecution(execution) {
|
|
19
|
+
if (!execution?.observation_id || !verifyExternalObservation(execution).valid) throw new TypeError('competitor representation requires a verified execution');
|
|
20
|
+
if (execution.data?.response?.status !== 'succeeded') throw new Error('competitor representation requires a substantive provider response');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Preserve observed entity identity; no name-only resolution is performed. */
|
|
24
|
+
export function resolveObservedEntity({ observedName, domain = null, entityId = null, resolutionMethod = 'not_resolved' } = {}) {
|
|
25
|
+
if (typeof observedName !== 'string' || !observedName.trim()) throw new TypeError('observedName is required');
|
|
26
|
+
const resolved = Boolean(entityId);
|
|
27
|
+
return {
|
|
28
|
+
entity_id: entityId || null,
|
|
29
|
+
observed_name: observedName,
|
|
30
|
+
domain: domain || null,
|
|
31
|
+
resolution_status: resolved ? 'RESOLVED' : 'AMBIGUOUS',
|
|
32
|
+
resolution_method: resolutionMethod,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function createCompetitorRepresentationObservation({ execution, span, entity, representation, citationReference = null, propositionReference = null, evidenceOrigin = 'RECORDED_FIXTURE', observedAt = nowIso(), limitations = [] } = {}) {
|
|
37
|
+
requireExecution(execution);
|
|
38
|
+
const responseSpan = span?.span_id ? span : createResponseSpan({ execution, ...(span || {}) });
|
|
39
|
+
if (responseSpan.execution_reference !== execution.observation_id) throw new Error('competitor span does not belong to execution');
|
|
40
|
+
if (!entity?.observed_name) throw new TypeError('competitor entity observation is required');
|
|
41
|
+
if (!representation?.type || !TYPES.has(representation.type) || !representation.observed_text) throw new TypeError('competitor representation type and observed_text are required');
|
|
42
|
+
if (!['LIVE_PROVIDER', 'RECORDED_FIXTURE', 'SYNTHETIC_TEST'].includes(evidenceOrigin)) throw new TypeError('evidenceOrigin is invalid');
|
|
43
|
+
const data = validate({
|
|
44
|
+
execution_reference: execution.observation_id,
|
|
45
|
+
provider: execution.data.provider,
|
|
46
|
+
prompt_reference: `${execution.data.prompt_set.prompt_set_id}@${execution.data.prompt_set.prompt_set_version}:${execution.data.prompt.prompt_id}`,
|
|
47
|
+
prompt_set_reference: `${execution.data.prompt_set.prompt_set_id}@${execution.data.prompt_set.prompt_set_version}`,
|
|
48
|
+
span: responseSpan,
|
|
49
|
+
entity: {
|
|
50
|
+
entity_id: entity.entity_id || null,
|
|
51
|
+
observed_name: entity.observed_name,
|
|
52
|
+
domain: entity.domain || null,
|
|
53
|
+
resolution_status: entity.resolution_status || (entity.entity_id ? 'RESOLVED' : 'AMBIGUOUS'),
|
|
54
|
+
resolution_method: entity.resolution_method || 'not_resolved',
|
|
55
|
+
},
|
|
56
|
+
representation: { type: representation.type, observed_text: representation.observed_text },
|
|
57
|
+
citation_reference: citationReference,
|
|
58
|
+
proposition_reference: propositionReference,
|
|
59
|
+
});
|
|
60
|
+
const identity = data.entity.entity_id || `AMBIGUOUS-${sha256(canonicalEvidenceJson({ execution: execution.observation_id, span: responseSpan.span_id, name: data.entity.observed_name, domain: data.entity.domain })).slice(0, 24)}`;
|
|
61
|
+
return createExternalObservation({
|
|
62
|
+
observation_id: `ENT-${sha256(canonicalEvidenceJson({ execution: execution.observation_id, data })).slice(0, 24)}`,
|
|
63
|
+
observation_type: 'EXTERNAL_OBSERVATION',
|
|
64
|
+
subject: { type: 'competitor_representation', id: identity },
|
|
65
|
+
source: { provider: execution.data.provider, method: 'competitor_representation_observation', authority: 'external_provider' },
|
|
66
|
+
collector: COLLECTOR,
|
|
67
|
+
parser: PARSER,
|
|
68
|
+
configuration: execution.data.execution.configuration,
|
|
69
|
+
observed_at: observedAt,
|
|
70
|
+
retrieved_at: observedAt,
|
|
71
|
+
executed_at: execution.data.execution.executed_at,
|
|
72
|
+
population: { declared: execution.population?.declared || null, observed: 1, retrieved: 1, evaluated: 1, unavailable: 0, failed: 0 },
|
|
73
|
+
observation_status: 'observed',
|
|
74
|
+
retrieval_status: 'retrieved',
|
|
75
|
+
data: { ...data, evidence_origin: evidenceOrigin },
|
|
76
|
+
lineage: { source_evidence: [execution.observation_id], predecessor_references: [execution.observation_id], package_reference: null },
|
|
77
|
+
limitations: [...new Set(limitations)],
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function verifyCompetitorRepresentationObservation(item) {
|
|
82
|
+
return verifyExternalObservation(item);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function representationsForEntity(observations, entityId) {
|
|
86
|
+
return (observations || []).filter((item) => item.data?.entity?.entity_id === entityId);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function representationsForExecution(observations, executionId) {
|
|
90
|
+
return (observations || []).filter((item) => item.data?.execution_reference === executionId);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Comparability is inherited from the common external evidence contract. */
|
|
94
|
+
export function compareCompetitorRepresentations(left, right) {
|
|
95
|
+
const base = compareExternalObservations(left, right);
|
|
96
|
+
if (!verifyCompetitorRepresentationObservation(left).valid || !verifyCompetitorRepresentationObservation(right).valid) return base;
|
|
97
|
+
const dimensions = {
|
|
98
|
+
...base.dimensions,
|
|
99
|
+
prompt: left.data.prompt_reference === right.data.prompt_reference ? 'match' : 'mismatch',
|
|
100
|
+
prompt_set: left.data.prompt_set_reference === right.data.prompt_set_reference ? 'match' : 'mismatch',
|
|
101
|
+
representation_type: left.data.representation.type === right.data.representation.type ? 'match' : 'mismatch',
|
|
102
|
+
entity_resolution: left.data.entity.resolution_method === right.data.entity.resolution_method && left.data.entity.resolution_status === right.data.entity.resolution_status ? 'match' : 'mismatch',
|
|
103
|
+
};
|
|
104
|
+
const mismatched = Object.values(dimensions).some((value) => value === 'mismatch');
|
|
105
|
+
const missing = Object.values(dimensions).some((value) => value === 'missing');
|
|
106
|
+
return {
|
|
107
|
+
...base,
|
|
108
|
+
comparison_id: `CMP-${sha256(canonicalEvidenceJson({ left: left.observation_id, right: right.observation_id, dimensions })).slice(0, 16)}`,
|
|
109
|
+
status: mismatched ? 'NOT_COMPARABLE' : missing ? 'PARTIALLY_COMPARABLE' : 'COMPARABLE',
|
|
110
|
+
dimensions,
|
|
111
|
+
limitations: mismatched || missing ? ['Representation type, entity resolution, prompt identity, and population must align before a delta is interpreted.'] : [],
|
|
112
|
+
};
|
|
113
|
+
}
|