@nebulacomponents/citable 1.21.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 +25 -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 +9 -5
- package/dist/universal/.agents/skills/citable/schemas/citation-provenance.schema.json +59 -0
- package/dist/universal/.agents/skills/citable/schemas/competitor-representation.schema.json +36 -0
- package/dist/universal/.agents/skills/citable/schemas/response-span.schema.json +16 -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 +9 -5
- package/dist/universal/.claude/skills/citable/schemas/citation-provenance.schema.json +59 -0
- package/dist/universal/.claude/skills/citable/schemas/competitor-representation.schema.json +36 -0
- package/dist/universal/.claude/skills/citable/schemas/response-span.schema.json +16 -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 +9 -5
- package/dist/universal/.cursor/skills/citable/schemas/citation-provenance.schema.json +59 -0
- package/dist/universal/.cursor/skills/citable/schemas/competitor-representation.schema.json +36 -0
- package/dist/universal/.cursor/skills/citable/schemas/response-span.schema.json +16 -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 +9 -5
- package/dist/universal/.gemini/skills/citable/schemas/citation-provenance.schema.json +59 -0
- package/dist/universal/.gemini/skills/citable/schemas/competitor-representation.schema.json +36 -0
- package/dist/universal/.gemini/skills/citable/schemas/response-span.schema.json +16 -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 +9 -5
- package/dist/universal/.github/skills/citable/schemas/citation-provenance.schema.json +59 -0
- package/dist/universal/.github/skills/citable/schemas/competitor-representation.schema.json +36 -0
- package/dist/universal/.github/skills/citable/schemas/response-span.schema.json +16 -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 +9 -5
- package/dist/universal/.kiro/skills/citable/schemas/citation-provenance.schema.json +59 -0
- package/dist/universal/.kiro/skills/citable/schemas/competitor-representation.schema.json +36 -0
- package/dist/universal/.kiro/skills/citable/schemas/response-span.schema.json +16 -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 +9 -5
- package/dist/universal/.opencode/skills/citable/schemas/citation-provenance.schema.json +59 -0
- package/dist/universal/.opencode/skills/citable/schemas/competitor-representation.schema.json +36 -0
- package/dist/universal/.opencode/skills/citable/schemas/response-span.schema.json +16 -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 +9 -5
- package/dist/universal/.pi/agent/skills/citable/schemas/citation-provenance.schema.json +59 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/competitor-representation.schema.json +36 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/response-span.schema.json +16 -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 +9 -5
- package/dist/universal/.qoder/skills/citable/schemas/citation-provenance.schema.json +59 -0
- package/dist/universal/.qoder/skills/citable/schemas/competitor-representation.schema.json +36 -0
- package/dist/universal/.qoder/skills/citable/schemas/response-span.schema.json +16 -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 +9 -5
- package/dist/universal/.rovodev/skills/citable/schemas/citation-provenance.schema.json +59 -0
- package/dist/universal/.rovodev/skills/citable/schemas/competitor-representation.schema.json +36 -0
- package/dist/universal/.rovodev/skills/citable/schemas/response-span.schema.json +16 -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 +9 -5
- package/dist/universal/.trae/skills/citable/schemas/citation-provenance.schema.json +59 -0
- package/dist/universal/.trae/skills/citable/schemas/competitor-representation.schema.json +36 -0
- package/dist/universal/.trae/skills/citable/schemas/response-span.schema.json +16 -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 +9 -5
- package/dist/universal/.trae-cn/skills/citable/schemas/citation-provenance.schema.json +59 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/competitor-representation.schema.json +36 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/response-span.schema.json +16 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/source-retrieval.schema.json +41 -0
- package/dist/universal/manifest.json +112 -64
- package/package.json +1 -1
- package/schemas/citation-provenance.schema.json +59 -0
- package/schemas/competitor-representation.schema.json +36 -0
- package/schemas/response-span.schema.json +16 -0
- package/schemas/source-retrieval.schema.json +41 -0
- package/skill/SKILL.md +1 -1
- package/src/representation/competitors.js +113 -0
- package/src/representation/provenance.js +214 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nebulacomponents/citable",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.22.0",
|
|
4
4
|
"description": "Production-grade SEO, AEO, and GEO agent skill with auditable detection, claim governance, evidence validation, and multi-agent installation.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"seo",
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$id": "citable://schemas/citation-provenance.schema.json",
|
|
3
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
4
|
+
"title": "Provider citation provenance observation data",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["execution_reference", "response_reference", "provider", "span", "citation", "source", "retrieval", "proposition", "support"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"execution_reference": { "type": "string", "minLength": 1 },
|
|
10
|
+
"response_reference": { "type": "string", "minLength": 1 },
|
|
11
|
+
"provider": { "type": "string", "minLength": 1 },
|
|
12
|
+
"span": { "$ref": "citable://schemas/response-span.schema.json" },
|
|
13
|
+
"citation": {
|
|
14
|
+
"type": "object", "additionalProperties": false,
|
|
15
|
+
"required": ["citation_id", "ordinal", "raw_marker", "placement", "displayed_title", "displayed_domain", "displayed_url"],
|
|
16
|
+
"properties": {
|
|
17
|
+
"citation_id": { "type": "string", "minLength": 1 },
|
|
18
|
+
"ordinal": { "type": "integer", "minimum": 1 },
|
|
19
|
+
"raw_marker": { "type": ["string", "null"] },
|
|
20
|
+
"placement": { "type": "string", "enum": ["inline", "footnote", "card", "source_list", "unknown"] },
|
|
21
|
+
"displayed_title": { "type": ["string", "null"] },
|
|
22
|
+
"displayed_domain": { "type": ["string", "null"] },
|
|
23
|
+
"displayed_url": { "type": ["string", "null"] }
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"source": {
|
|
27
|
+
"type": "object", "additionalProperties": false,
|
|
28
|
+
"required": ["identity_status", "resolved_url", "redirect_chain", "canonical_url"],
|
|
29
|
+
"properties": {
|
|
30
|
+
"identity_status": { "type": "string", "enum": ["IDENTIFIED", "NOT_IDENTIFIED", "AMBIGUOUS"] },
|
|
31
|
+
"resolved_url": { "type": ["string", "null"], "format": "uri" },
|
|
32
|
+
"redirect_chain": { "type": "array", "items": { "type": "string", "format": "uri" } },
|
|
33
|
+
"canonical_url": { "type": ["string", "null"], "format": "uri" }
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"retrieval": {
|
|
37
|
+
"type": "object", "additionalProperties": false,
|
|
38
|
+
"required": ["status", "observation_reference"],
|
|
39
|
+
"properties": {
|
|
40
|
+
"status": { "type": "string", "enum": ["NOT_ATTEMPTED", "ATTEMPTED", "SUCCEEDED", "FAILED", "INDETERMINATE"] },
|
|
41
|
+
"observation_reference": { "type": ["string", "null"] }
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"proposition": {
|
|
45
|
+
"type": "object", "additionalProperties": false,
|
|
46
|
+
"required": ["proposition_id", "text"],
|
|
47
|
+
"properties": { "proposition_id": { "type": "string", "minLength": 1 }, "text": { "type": "string", "minLength": 1 } }
|
|
48
|
+
},
|
|
49
|
+
"support": {
|
|
50
|
+
"type": "object", "additionalProperties": false,
|
|
51
|
+
"required": ["relevance_status", "support_status", "evidence_references"],
|
|
52
|
+
"properties": {
|
|
53
|
+
"relevance_status": { "type": "string", "enum": ["NOT_ASSESSED", "RELEVANT", "IRRELEVANT", "INDETERMINATE"] },
|
|
54
|
+
"support_status": { "type": "string", "enum": ["NOT_ASSESSED", "SUPPORTED", "PARTIALLY_SUPPORTED", "UNSUPPORTED", "CONTRADICTED", "INDETERMINATE", "NOT_OBSERVED"] },
|
|
55
|
+
"evidence_references": { "type": "array", "items": { "type": "string" } }
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$id": "citable://schemas/competitor-representation.schema.json",
|
|
3
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
4
|
+
"title": "Competitor or peer representation observation data",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["execution_reference", "provider", "prompt_reference", "prompt_set_reference", "span", "entity", "representation", "citation_reference", "proposition_reference"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"execution_reference": { "type": "string", "minLength": 1 },
|
|
10
|
+
"provider": { "type": "string", "minLength": 1 },
|
|
11
|
+
"prompt_reference": { "type": "string", "minLength": 1 },
|
|
12
|
+
"prompt_set_reference": { "type": "string", "minLength": 1 },
|
|
13
|
+
"span": { "$ref": "citable://schemas/response-span.schema.json" },
|
|
14
|
+
"entity": {
|
|
15
|
+
"type": "object", "additionalProperties": false,
|
|
16
|
+
"required": ["entity_id", "observed_name", "domain", "resolution_status", "resolution_method"],
|
|
17
|
+
"properties": {
|
|
18
|
+
"entity_id": { "type": ["string", "null"] },
|
|
19
|
+
"observed_name": { "type": "string", "minLength": 1 },
|
|
20
|
+
"domain": { "type": ["string", "null"] },
|
|
21
|
+
"resolution_status": { "type": "string", "enum": ["RESOLVED", "AMBIGUOUS", "UNRESOLVED", "NOT_OBSERVED"] },
|
|
22
|
+
"resolution_method": { "type": "string", "minLength": 1 }
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"representation": {
|
|
26
|
+
"type": "object", "additionalProperties": false,
|
|
27
|
+
"required": ["type", "observed_text"],
|
|
28
|
+
"properties": {
|
|
29
|
+
"type": { "type": "string", "enum": ["MENTION", "COMPARISON", "ALTERNATIVE", "RECOMMENDATION", "CITED_SOURCE", "CATEGORY_PEER"] },
|
|
30
|
+
"observed_text": { "type": "string", "minLength": 1 }
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"citation_reference": { "type": ["string", "null"] },
|
|
34
|
+
"proposition_reference": { "type": ["string", "null"] }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$id": "citable://schemas/response-span.schema.json",
|
|
3
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
4
|
+
"title": "Immutable provider response span",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["span_id", "execution_reference", "start", "end", "text", "text_hash"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"span_id": { "type": "string", "minLength": 1 },
|
|
10
|
+
"execution_reference": { "type": "string", "minLength": 1 },
|
|
11
|
+
"start": { "type": "integer", "minimum": 0 },
|
|
12
|
+
"end": { "type": "integer", "minimum": 0 },
|
|
13
|
+
"text": { "type": "string" },
|
|
14
|
+
"text_hash": { "type": "string", "pattern": "^[a-f0-9]{64}$" }
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$id": "citable://schemas/source-retrieval.schema.json",
|
|
3
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
4
|
+
"title": "Retrieved citation source observation data",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["citation_reference", "url", "retrieval", "content", "relevance"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"citation_reference": { "type": "string", "minLength": 1 },
|
|
10
|
+
"url": { "type": "string", "format": "uri" },
|
|
11
|
+
"retrieval": {
|
|
12
|
+
"type": "object", "additionalProperties": false,
|
|
13
|
+
"required": ["status", "attempted_at", "final_url", "redirect_chain", "http_status"],
|
|
14
|
+
"properties": {
|
|
15
|
+
"status": { "type": "string", "enum": ["SUCCEEDED", "FAILED", "INDETERMINATE"] },
|
|
16
|
+
"attempted_at": { "type": "string", "format": "date-time" },
|
|
17
|
+
"final_url": { "type": ["string", "null"], "format": "uri" },
|
|
18
|
+
"redirect_chain": { "type": "array", "items": { "type": "string", "format": "uri" } },
|
|
19
|
+
"http_status": { "type": ["integer", "null"], "minimum": 100, "maximum": 599 }
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"content": {
|
|
23
|
+
"type": "object", "additionalProperties": false,
|
|
24
|
+
"required": ["observed", "content_hash", "raw_retained"],
|
|
25
|
+
"properties": {
|
|
26
|
+
"observed": { "type": "boolean" },
|
|
27
|
+
"content_hash": { "type": ["string", "null"], "pattern": "^[a-f0-9]{64}$" },
|
|
28
|
+
"raw_retained": { "type": "boolean" }
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"relevance": {
|
|
32
|
+
"type": "object", "additionalProperties": false,
|
|
33
|
+
"required": ["status", "support_status", "evidence_references"],
|
|
34
|
+
"properties": {
|
|
35
|
+
"status": { "type": "string", "enum": ["NOT_ASSESSED", "RELEVANT", "IRRELEVANT", "INDETERMINATE"] },
|
|
36
|
+
"support_status": { "type": "string", "enum": ["NOT_ASSESSED", "SUPPORTED", "PARTIALLY_SUPPORTED", "UNSUPPORTED", "CONTRADICTED", "INDETERMINATE", "NOT_OBSERVED"] },
|
|
37
|
+
"evidence_references": { "type": "array", "items": { "type": "string" } }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
package/skill/SKILL.md
CHANGED
|
@@ -7,7 +7,7 @@ description: >
|
|
|
7
7
|
generative engine optimization, structured data governance, claim substantiation,
|
|
8
8
|
crawler policy, entity consistency, content discoverability, or wants a site audited
|
|
9
9
|
for how search and AI systems will retrieve, understand, cite, or recommend it.
|
|
10
|
-
version: 1.
|
|
10
|
+
version: 1.22.0
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
# Citable — evidence and change control for search and AI citation readiness
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { createExternalObservation, createDerivation, verifyExternalObservation, verifyClaim } 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
|
+
|
|
6
|
+
const SPAN_SCHEMA = 'response-span.schema.json';
|
|
7
|
+
const CITATION_SCHEMA = 'citation-provenance.schema.json';
|
|
8
|
+
const SOURCE_SCHEMA = 'source-retrieval.schema.json';
|
|
9
|
+
const COLLECTOR = { id: 'citable-citation-provenance', version: '1.0.0' };
|
|
10
|
+
const PARSER = { id: 'provider-neutral-citation-parser', version: '1.0.0' };
|
|
11
|
+
|
|
12
|
+
function validate(schema, value, label) {
|
|
13
|
+
const result = validateAgainst(schema, value);
|
|
14
|
+
if (!result.valid) throw new TypeError(`${label} violates contract: ${result.errors.join('; ')}`);
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function validUrl(value) {
|
|
19
|
+
try { return new URL(value).toString(); } catch { return null; }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function executionReference(execution) {
|
|
23
|
+
if (!execution?.observation_id || !verifyExternalObservation(execution).valid) {
|
|
24
|
+
throw new TypeError('citation execution must be a verified external observation');
|
|
25
|
+
}
|
|
26
|
+
if (execution.data?.response?.status !== 'succeeded') throw new Error('citation requires a substantive provider response');
|
|
27
|
+
return execution.observation_id;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Create a stable, exact text span without normalizing provider prose. */
|
|
31
|
+
export function createResponseSpan({ execution, start, end, text, spanId = null } = {}) {
|
|
32
|
+
const executionId = execution?.observation_id || execution;
|
|
33
|
+
if (typeof executionId !== 'string' || !executionId) throw new TypeError('execution reference is required');
|
|
34
|
+
if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end < start) throw new TypeError('span offsets are invalid');
|
|
35
|
+
if (typeof text !== 'string') throw new TypeError('span text is required');
|
|
36
|
+
if (end - start !== text.length) throw new TypeError('span offsets must bound the exact response text');
|
|
37
|
+
const item = {
|
|
38
|
+
span_id: spanId || `SPAN-${sha256(canonicalEvidenceJson({ execution_reference: executionId, start, end, text })).slice(0, 24)}`,
|
|
39
|
+
execution_reference: executionId,
|
|
40
|
+
start,
|
|
41
|
+
end,
|
|
42
|
+
text,
|
|
43
|
+
text_hash: sha256(text),
|
|
44
|
+
};
|
|
45
|
+
return validate(SPAN_SCHEMA, item, 'response span');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function sourceShape(citation, source = {}) {
|
|
49
|
+
const displayed = citation.displayed_url ?? citation.url ?? null;
|
|
50
|
+
const resolved = validUrl(source.resolved_url ?? citation.resolved_url);
|
|
51
|
+
const status = source.identity_status || (resolved ? 'IDENTIFIED' : displayed ? 'AMBIGUOUS' : 'NOT_IDENTIFIED');
|
|
52
|
+
return {
|
|
53
|
+
identity_status: status,
|
|
54
|
+
resolved_url: resolved,
|
|
55
|
+
redirect_chain: (source.redirect_chain || []).map(validUrl).filter(Boolean),
|
|
56
|
+
canonical_url: validUrl(source.canonical_url) || null,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function citationData({ execution, span, citation, proposition, source, retrieval, support }) {
|
|
61
|
+
const data = {
|
|
62
|
+
execution_reference: execution.observation_id,
|
|
63
|
+
response_reference: execution.data.response.response_hash,
|
|
64
|
+
provider: execution.data.provider,
|
|
65
|
+
span,
|
|
66
|
+
citation: {
|
|
67
|
+
citation_id: citation.citation_id || `CIT-${sha256(canonicalEvidenceJson({ execution: execution.observation_id, span: span.span_id, ordinal: citation.ordinal || 1, marker: citation.raw_marker || null })).slice(0, 24)}`,
|
|
68
|
+
ordinal: citation.ordinal || 1,
|
|
69
|
+
raw_marker: citation.raw_marker ?? null,
|
|
70
|
+
placement: citation.placement || 'unknown',
|
|
71
|
+
displayed_title: citation.displayed_title ?? citation.title ?? null,
|
|
72
|
+
displayed_domain: citation.displayed_domain ?? citation.domain ?? null,
|
|
73
|
+
displayed_url: citation.displayed_url ?? citation.url ?? null,
|
|
74
|
+
},
|
|
75
|
+
source: sourceShape(citation, source),
|
|
76
|
+
retrieval: { status: retrieval?.status || 'NOT_ATTEMPTED', observation_reference: retrieval?.observation_reference || null },
|
|
77
|
+
proposition: { proposition_id: proposition?.proposition_id || `PROP-${sha256(proposition?.text || span.text).slice(0, 24)}`, text: proposition?.text || span.text },
|
|
78
|
+
support: {
|
|
79
|
+
relevance_status: support?.relevance_status || 'NOT_ASSESSED',
|
|
80
|
+
support_status: support?.support_status || 'NOT_ASSESSED',
|
|
81
|
+
evidence_references: [...new Set(support?.evidence_references || [])],
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
return validate(CITATION_SCHEMA, data, 'citation provenance');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Create an immutable citation linked to one exact response span. */
|
|
88
|
+
export function createCitationObservation({ execution, span, citation, proposition, source = {}, retrieval = null, support = null, evidenceOrigin = 'RECORDED_FIXTURE', observedAt = nowIso(), limitations = [] } = {}) {
|
|
89
|
+
executionReference(execution);
|
|
90
|
+
const responseSpan = span?.span_id ? validate(SPAN_SCHEMA, span, 'response span') : createResponseSpan({ execution, ...(span || {}) });
|
|
91
|
+
if (responseSpan.execution_reference !== execution.observation_id) throw new Error('span does not belong to execution');
|
|
92
|
+
const data = citationData({ execution, span: responseSpan, citation: citation || {}, proposition, source, retrieval, support });
|
|
93
|
+
if (!['LIVE_PROVIDER', 'RECORDED_FIXTURE', 'SYNTHETIC_TEST'].includes(evidenceOrigin)) throw new TypeError('evidenceOrigin is invalid');
|
|
94
|
+
return createExternalObservation({
|
|
95
|
+
observation_id: `CIT-${sha256(canonicalEvidenceJson({ execution: execution.observation_id, data })).slice(0, 24)}`,
|
|
96
|
+
observation_type: 'EXTERNAL_OBSERVATION',
|
|
97
|
+
subject: { type: 'citation', id: data.citation.citation_id },
|
|
98
|
+
source: { provider: execution.data.provider, method: 'citation_provenance', authority: 'external_provider' },
|
|
99
|
+
collector: COLLECTOR,
|
|
100
|
+
parser: PARSER,
|
|
101
|
+
configuration: execution.data.execution.configuration,
|
|
102
|
+
observed_at: observedAt,
|
|
103
|
+
retrieved_at: observedAt,
|
|
104
|
+
executed_at: execution.data.execution.executed_at,
|
|
105
|
+
population: { declared: execution.population?.declared || null, observed: 1, retrieved: 1, evaluated: 1, unavailable: 0, failed: 0 },
|
|
106
|
+
observation_status: 'observed',
|
|
107
|
+
retrieval_status: 'retrieved',
|
|
108
|
+
data: { ...data, evidence_origin: evidenceOrigin },
|
|
109
|
+
lineage: { source_evidence: [execution.observation_id], predecessor_references: [execution.observation_id], package_reference: null },
|
|
110
|
+
limitations: [...new Set(limitations)],
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Build a set while refusing duplicate citation ordinals or identifiers. */
|
|
115
|
+
export function createCitationObservations(input = {}) {
|
|
116
|
+
const citations = input.citations || [];
|
|
117
|
+
const ids = new Set(), ordinals = new Set();
|
|
118
|
+
return citations.map((citation) => {
|
|
119
|
+
const item = createCitationObservation({ ...input, citation });
|
|
120
|
+
const id = item.data.citation.citation_id;
|
|
121
|
+
const ordinal = item.data.citation.ordinal;
|
|
122
|
+
if (ids.has(id)) throw new Error(`duplicate citation_id: ${id}`);
|
|
123
|
+
if (ordinals.has(ordinal)) throw new Error(`duplicate citation ordinal: ${ordinal}`);
|
|
124
|
+
ids.add(id); ordinals.add(ordinal);
|
|
125
|
+
return item;
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function verifyCitationObservation(item) {
|
|
130
|
+
return verifyExternalObservation(item);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Preserve source retrieval as a separate observation; failed retrieval is never support failure. */
|
|
134
|
+
export function createSourceRetrievalObservation({ citation, url, status = 'SUCCEEDED', body = null, finalUrl = null, redirectChain = [], httpStatus = null, attemptedAt = nowIso(), relevanceStatus = 'NOT_ASSESSED', supportStatus = 'NOT_ASSESSED', evidenceReferences = [], rawRetained = false, evidenceOrigin = 'RECORDED_FIXTURE', limitations = [] } = {}) {
|
|
135
|
+
if (!citation?.observation_id || !verifyCitationObservation(citation).valid) throw new TypeError('citation must be a verified citation observation');
|
|
136
|
+
const sourceUrl = validUrl(url || citation.data.source.resolved_url);
|
|
137
|
+
if (!sourceUrl) throw new TypeError('retrieval URL must be a valid URL');
|
|
138
|
+
const contentHash = body == null ? null : sha256(body);
|
|
139
|
+
const data = validate(SOURCE_SCHEMA, {
|
|
140
|
+
citation_reference: citation.observation_id,
|
|
141
|
+
url: sourceUrl,
|
|
142
|
+
retrieval: { status, attempted_at: attemptedAt, final_url: validUrl(finalUrl) || (status === 'SUCCEEDED' ? sourceUrl : null), redirect_chain: redirectChain.map(validUrl).filter(Boolean), http_status: status === 'SUCCEEDED' ? (httpStatus || 200) : httpStatus },
|
|
143
|
+
content: { observed: status === 'SUCCEEDED' && body != null, content_hash: contentHash, raw_retained: Boolean(rawRetained) },
|
|
144
|
+
relevance: { status: relevanceStatus, support_status: supportStatus, evidence_references: [...new Set(evidenceReferences)] },
|
|
145
|
+
}, 'source retrieval');
|
|
146
|
+
return createExternalObservation({
|
|
147
|
+
observation_id: `SRC-${sha256(canonicalEvidenceJson({ citation: citation.observation_id, data })).slice(0, 24)}`,
|
|
148
|
+
observation_type: 'EXTERNAL_OBSERVATION',
|
|
149
|
+
subject: { type: 'citation_source', id: sourceUrl },
|
|
150
|
+
source: { provider: citation.data.provider, method: 'citation_source_retrieval', authority: 'third_party' },
|
|
151
|
+
collector: COLLECTOR,
|
|
152
|
+
parser: PARSER,
|
|
153
|
+
configuration: null,
|
|
154
|
+
observed_at: attemptedAt,
|
|
155
|
+
retrieved_at: attemptedAt,
|
|
156
|
+
executed_at: null,
|
|
157
|
+
population: { declared: null, observed: status === 'SUCCEEDED' ? 1 : 0, retrieved: status === 'SUCCEEDED' ? 1 : 0, evaluated: status === 'SUCCEEDED' ? 1 : 0, unavailable: 0, failed: status === 'FAILED' ? 1 : 0 },
|
|
158
|
+
observation_status: status === 'SUCCEEDED' ? 'observed' : status === 'FAILED' ? 'failed' : 'indeterminate',
|
|
159
|
+
retrieval_status: status === 'SUCCEEDED' ? 'retrieved' : status === 'FAILED' ? 'failed' : 'indeterminate',
|
|
160
|
+
data: { ...data, evidence_origin: evidenceOrigin },
|
|
161
|
+
lineage: { source_evidence: [citation.observation_id], predecessor_references: [citation.observation_id], package_reference: null },
|
|
162
|
+
limitations: [...new Set([...(status === 'FAILED' ? ['Source retrieval failed; proposition support is indeterminate.'] : []), ...limitations])],
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function verifySourceRetrievalObservation(item) {
|
|
167
|
+
return verifyExternalObservation(item);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function assessPropositionSupport({ retrieval, relevanceStatus, supportStatus, evidenceReferences = [] } = {}) {
|
|
171
|
+
if (!retrieval || !verifySourceRetrievalObservation(retrieval).valid) return { status: 'INDETERMINATE', reasons: ['source retrieval evidence is missing or unverifiable'], evidence_references: [] };
|
|
172
|
+
if (retrieval.data.retrieval.status !== 'SUCCEEDED') return { status: 'INDETERMINATE', reasons: ['source retrieval did not succeed'], evidence_references: [retrieval.observation_id] };
|
|
173
|
+
if (relevanceStatus === 'IRRELEVANT') return { status: 'UNSUPPORTED', reasons: ['retrieved source was assessed as irrelevant to the proposition'], evidence_references: [retrieval.observation_id, ...evidenceReferences] };
|
|
174
|
+
const supportedStates = new Set(['SUPPORTED', 'PARTIALLY_SUPPORTED', 'UNSUPPORTED', 'CONTRADICTED', 'INDETERMINATE', 'NOT_OBSERVED']);
|
|
175
|
+
if (relevanceStatus !== 'RELEVANT' || !supportStatus || supportStatus === 'NOT_ASSESSED' || !supportedStates.has(supportStatus)) return { status: 'INDETERMINATE', reasons: ['source relevance or proposition support was not established'], evidence_references: [retrieval.observation_id, ...evidenceReferences] };
|
|
176
|
+
return { status: supportStatus, reasons: [], evidence_references: [retrieval.observation_id, ...evidenceReferences] };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Derive prevalence over successful/evaluable executions only, retaining exclusions. */
|
|
180
|
+
export function deriveRepresentationPrevalence(observations, { predicatePath = 'data.representation.brand_mentioned', expected = true, derivationId = 'REPRESENTATION-PREVALENCE', derivationVersion = 1 } = {}) {
|
|
181
|
+
if (!Array.isArray(observations) || !observations.length) throw new TypeError('observations are required');
|
|
182
|
+
const valid = observations.filter((item) => verifyExternalObservation(item).valid);
|
|
183
|
+
const evaluable = valid.filter((item) => item.data?.response?.status === 'succeeded');
|
|
184
|
+
const excluded = observations.filter((item) => !verifyExternalObservation(item).valid || item.data?.response?.status !== 'succeeded');
|
|
185
|
+
const valueAt = (item) => predicatePath.split('.').reduce((v, key) => v == null ? undefined : v[key], item);
|
|
186
|
+
const numerator = evaluable.filter((item) => valueAt(item) === expected).length;
|
|
187
|
+
return createDerivation({
|
|
188
|
+
derivation_id: derivationId,
|
|
189
|
+
derivation_version: derivationVersion,
|
|
190
|
+
operation: 'representation_prevalence',
|
|
191
|
+
input_observation_references: observations.map((item) => item.observation_id),
|
|
192
|
+
predicate: { path: predicatePath, operator: 'equals', value: expected },
|
|
193
|
+
result: {
|
|
194
|
+
numerator,
|
|
195
|
+
denominator: evaluable.length,
|
|
196
|
+
rate: evaluable.length ? numerator / evaluable.length : null,
|
|
197
|
+
evaluable_observation_references: evaluable.map((item) => item.observation_id),
|
|
198
|
+
excluded_observation_references: excluded.map((item) => item.observation_id),
|
|
199
|
+
exclusions: excluded.map((item) => ({ observation_reference: item.observation_id, reason: verifyExternalObservation(item).valid ? (item.data?.response?.status || 'not_evaluable') : 'integrity_unverifiable' })),
|
|
200
|
+
population_scope: 'successful_representation_executions_in_supplied_observations',
|
|
201
|
+
},
|
|
202
|
+
implementation: { id: 'citable-representation-prevalence', version: '1.0.0' },
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function citationsForExecution(citations, executionId) { return (citations || []).filter((item) => item.data?.execution_reference === executionId); }
|
|
207
|
+
export function citationsForProposition(citations, propositionId) { return (citations || []).filter((item) => item.data?.proposition?.proposition_id === propositionId); }
|
|
208
|
+
export function sourceForCitation(retrievals, citationId) { return (retrievals || []).find((item) => item.data?.citation_reference === citationId) || null; }
|
|
209
|
+
export function evidenceForProposition(retrievals, propositionId, citations = []) {
|
|
210
|
+
const citationIds = new Set(citationsForProposition(citations, propositionId).map((item) => item.observation_id));
|
|
211
|
+
return (retrievals || []).filter((item) => citationIds.has(item.data?.citation_reference));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export { verifyClaim };
|