@nebulacomponents/citable 1.20.0 → 1.21.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.
Files changed (163) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/universal/.agents/skills/citable/SKILL.md +1 -1
  3. package/dist/universal/.agents/skills/citable/VERSION +1 -1
  4. package/dist/universal/.agents/skills/citable/manifest.json +14 -5
  5. package/dist/universal/.agents/skills/citable/schemas/claim-evidence.schema.json +21 -0
  6. package/dist/universal/.agents/skills/citable/schemas/claim-verification.schema.json +18 -0
  7. package/dist/universal/.agents/skills/citable/schemas/derivation.schema.json +18 -0
  8. package/dist/universal/.agents/skills/citable/schemas/external-comparison.schema.json +16 -0
  9. package/dist/universal/.agents/skills/citable/schemas/external-observation.schema.json +32 -0
  10. package/dist/universal/.agents/skills/citable/schemas/prompt-set.schema.json +47 -0
  11. package/dist/universal/.agents/skills/citable/schemas/representation-data.schema.json +21 -0
  12. package/dist/universal/.agents/skills/citable/schemas/research-dataset.schema.json +27 -0
  13. package/dist/universal/.agents/skills/citable/schemas/search-discovery-data.schema.json +20 -0
  14. package/dist/universal/.claude/agents/citable/manifest.json +2 -2
  15. package/dist/universal/.claude/skills/citable/SKILL.md +1 -1
  16. package/dist/universal/.claude/skills/citable/VERSION +1 -1
  17. package/dist/universal/.claude/skills/citable/manifest.json +14 -5
  18. package/dist/universal/.claude/skills/citable/schemas/claim-evidence.schema.json +21 -0
  19. package/dist/universal/.claude/skills/citable/schemas/claim-verification.schema.json +18 -0
  20. package/dist/universal/.claude/skills/citable/schemas/derivation.schema.json +18 -0
  21. package/dist/universal/.claude/skills/citable/schemas/external-comparison.schema.json +16 -0
  22. package/dist/universal/.claude/skills/citable/schemas/external-observation.schema.json +32 -0
  23. package/dist/universal/.claude/skills/citable/schemas/prompt-set.schema.json +47 -0
  24. package/dist/universal/.claude/skills/citable/schemas/representation-data.schema.json +21 -0
  25. package/dist/universal/.claude/skills/citable/schemas/research-dataset.schema.json +27 -0
  26. package/dist/universal/.claude/skills/citable/schemas/search-discovery-data.schema.json +20 -0
  27. package/dist/universal/.cursor/skills/citable/SKILL.md +1 -1
  28. package/dist/universal/.cursor/skills/citable/VERSION +1 -1
  29. package/dist/universal/.cursor/skills/citable/manifest.json +14 -5
  30. package/dist/universal/.cursor/skills/citable/schemas/claim-evidence.schema.json +21 -0
  31. package/dist/universal/.cursor/skills/citable/schemas/claim-verification.schema.json +18 -0
  32. package/dist/universal/.cursor/skills/citable/schemas/derivation.schema.json +18 -0
  33. package/dist/universal/.cursor/skills/citable/schemas/external-comparison.schema.json +16 -0
  34. package/dist/universal/.cursor/skills/citable/schemas/external-observation.schema.json +32 -0
  35. package/dist/universal/.cursor/skills/citable/schemas/prompt-set.schema.json +47 -0
  36. package/dist/universal/.cursor/skills/citable/schemas/representation-data.schema.json +21 -0
  37. package/dist/universal/.cursor/skills/citable/schemas/research-dataset.schema.json +27 -0
  38. package/dist/universal/.cursor/skills/citable/schemas/search-discovery-data.schema.json +20 -0
  39. package/dist/universal/.gemini/skills/citable/SKILL.md +1 -1
  40. package/dist/universal/.gemini/skills/citable/VERSION +1 -1
  41. package/dist/universal/.gemini/skills/citable/manifest.json +14 -5
  42. package/dist/universal/.gemini/skills/citable/schemas/claim-evidence.schema.json +21 -0
  43. package/dist/universal/.gemini/skills/citable/schemas/claim-verification.schema.json +18 -0
  44. package/dist/universal/.gemini/skills/citable/schemas/derivation.schema.json +18 -0
  45. package/dist/universal/.gemini/skills/citable/schemas/external-comparison.schema.json +16 -0
  46. package/dist/universal/.gemini/skills/citable/schemas/external-observation.schema.json +32 -0
  47. package/dist/universal/.gemini/skills/citable/schemas/prompt-set.schema.json +47 -0
  48. package/dist/universal/.gemini/skills/citable/schemas/representation-data.schema.json +21 -0
  49. package/dist/universal/.gemini/skills/citable/schemas/research-dataset.schema.json +27 -0
  50. package/dist/universal/.gemini/skills/citable/schemas/search-discovery-data.schema.json +20 -0
  51. package/dist/universal/.github/skills/citable/SKILL.md +1 -1
  52. package/dist/universal/.github/skills/citable/VERSION +1 -1
  53. package/dist/universal/.github/skills/citable/manifest.json +14 -5
  54. package/dist/universal/.github/skills/citable/schemas/claim-evidence.schema.json +21 -0
  55. package/dist/universal/.github/skills/citable/schemas/claim-verification.schema.json +18 -0
  56. package/dist/universal/.github/skills/citable/schemas/derivation.schema.json +18 -0
  57. package/dist/universal/.github/skills/citable/schemas/external-comparison.schema.json +16 -0
  58. package/dist/universal/.github/skills/citable/schemas/external-observation.schema.json +32 -0
  59. package/dist/universal/.github/skills/citable/schemas/prompt-set.schema.json +47 -0
  60. package/dist/universal/.github/skills/citable/schemas/representation-data.schema.json +21 -0
  61. package/dist/universal/.github/skills/citable/schemas/research-dataset.schema.json +27 -0
  62. package/dist/universal/.github/skills/citable/schemas/search-discovery-data.schema.json +20 -0
  63. package/dist/universal/.kiro/skills/citable/SKILL.md +1 -1
  64. package/dist/universal/.kiro/skills/citable/VERSION +1 -1
  65. package/dist/universal/.kiro/skills/citable/manifest.json +14 -5
  66. package/dist/universal/.kiro/skills/citable/schemas/claim-evidence.schema.json +21 -0
  67. package/dist/universal/.kiro/skills/citable/schemas/claim-verification.schema.json +18 -0
  68. package/dist/universal/.kiro/skills/citable/schemas/derivation.schema.json +18 -0
  69. package/dist/universal/.kiro/skills/citable/schemas/external-comparison.schema.json +16 -0
  70. package/dist/universal/.kiro/skills/citable/schemas/external-observation.schema.json +32 -0
  71. package/dist/universal/.kiro/skills/citable/schemas/prompt-set.schema.json +47 -0
  72. package/dist/universal/.kiro/skills/citable/schemas/representation-data.schema.json +21 -0
  73. package/dist/universal/.kiro/skills/citable/schemas/research-dataset.schema.json +27 -0
  74. package/dist/universal/.kiro/skills/citable/schemas/search-discovery-data.schema.json +20 -0
  75. package/dist/universal/.opencode/skills/citable/SKILL.md +1 -1
  76. package/dist/universal/.opencode/skills/citable/VERSION +1 -1
  77. package/dist/universal/.opencode/skills/citable/manifest.json +14 -5
  78. package/dist/universal/.opencode/skills/citable/schemas/claim-evidence.schema.json +21 -0
  79. package/dist/universal/.opencode/skills/citable/schemas/claim-verification.schema.json +18 -0
  80. package/dist/universal/.opencode/skills/citable/schemas/derivation.schema.json +18 -0
  81. package/dist/universal/.opencode/skills/citable/schemas/external-comparison.schema.json +16 -0
  82. package/dist/universal/.opencode/skills/citable/schemas/external-observation.schema.json +32 -0
  83. package/dist/universal/.opencode/skills/citable/schemas/prompt-set.schema.json +47 -0
  84. package/dist/universal/.opencode/skills/citable/schemas/representation-data.schema.json +21 -0
  85. package/dist/universal/.opencode/skills/citable/schemas/research-dataset.schema.json +27 -0
  86. package/dist/universal/.opencode/skills/citable/schemas/search-discovery-data.schema.json +20 -0
  87. package/dist/universal/.pi/agent/skills/citable/SKILL.md +1 -1
  88. package/dist/universal/.pi/agent/skills/citable/VERSION +1 -1
  89. package/dist/universal/.pi/agent/skills/citable/manifest.json +14 -5
  90. package/dist/universal/.pi/agent/skills/citable/schemas/claim-evidence.schema.json +21 -0
  91. package/dist/universal/.pi/agent/skills/citable/schemas/claim-verification.schema.json +18 -0
  92. package/dist/universal/.pi/agent/skills/citable/schemas/derivation.schema.json +18 -0
  93. package/dist/universal/.pi/agent/skills/citable/schemas/external-comparison.schema.json +16 -0
  94. package/dist/universal/.pi/agent/skills/citable/schemas/external-observation.schema.json +32 -0
  95. package/dist/universal/.pi/agent/skills/citable/schemas/prompt-set.schema.json +47 -0
  96. package/dist/universal/.pi/agent/skills/citable/schemas/representation-data.schema.json +21 -0
  97. package/dist/universal/.pi/agent/skills/citable/schemas/research-dataset.schema.json +27 -0
  98. package/dist/universal/.pi/agent/skills/citable/schemas/search-discovery-data.schema.json +20 -0
  99. package/dist/universal/.qoder/skills/citable/SKILL.md +1 -1
  100. package/dist/universal/.qoder/skills/citable/VERSION +1 -1
  101. package/dist/universal/.qoder/skills/citable/manifest.json +14 -5
  102. package/dist/universal/.qoder/skills/citable/schemas/claim-evidence.schema.json +21 -0
  103. package/dist/universal/.qoder/skills/citable/schemas/claim-verification.schema.json +18 -0
  104. package/dist/universal/.qoder/skills/citable/schemas/derivation.schema.json +18 -0
  105. package/dist/universal/.qoder/skills/citable/schemas/external-comparison.schema.json +16 -0
  106. package/dist/universal/.qoder/skills/citable/schemas/external-observation.schema.json +32 -0
  107. package/dist/universal/.qoder/skills/citable/schemas/prompt-set.schema.json +47 -0
  108. package/dist/universal/.qoder/skills/citable/schemas/representation-data.schema.json +21 -0
  109. package/dist/universal/.qoder/skills/citable/schemas/research-dataset.schema.json +27 -0
  110. package/dist/universal/.qoder/skills/citable/schemas/search-discovery-data.schema.json +20 -0
  111. package/dist/universal/.rovodev/skills/citable/SKILL.md +1 -1
  112. package/dist/universal/.rovodev/skills/citable/VERSION +1 -1
  113. package/dist/universal/.rovodev/skills/citable/manifest.json +14 -5
  114. package/dist/universal/.rovodev/skills/citable/schemas/claim-evidence.schema.json +21 -0
  115. package/dist/universal/.rovodev/skills/citable/schemas/claim-verification.schema.json +18 -0
  116. package/dist/universal/.rovodev/skills/citable/schemas/derivation.schema.json +18 -0
  117. package/dist/universal/.rovodev/skills/citable/schemas/external-comparison.schema.json +16 -0
  118. package/dist/universal/.rovodev/skills/citable/schemas/external-observation.schema.json +32 -0
  119. package/dist/universal/.rovodev/skills/citable/schemas/prompt-set.schema.json +47 -0
  120. package/dist/universal/.rovodev/skills/citable/schemas/representation-data.schema.json +21 -0
  121. package/dist/universal/.rovodev/skills/citable/schemas/research-dataset.schema.json +27 -0
  122. package/dist/universal/.rovodev/skills/citable/schemas/search-discovery-data.schema.json +20 -0
  123. package/dist/universal/.trae/skills/citable/SKILL.md +1 -1
  124. package/dist/universal/.trae/skills/citable/VERSION +1 -1
  125. package/dist/universal/.trae/skills/citable/manifest.json +14 -5
  126. package/dist/universal/.trae/skills/citable/schemas/claim-evidence.schema.json +21 -0
  127. package/dist/universal/.trae/skills/citable/schemas/claim-verification.schema.json +18 -0
  128. package/dist/universal/.trae/skills/citable/schemas/derivation.schema.json +18 -0
  129. package/dist/universal/.trae/skills/citable/schemas/external-comparison.schema.json +16 -0
  130. package/dist/universal/.trae/skills/citable/schemas/external-observation.schema.json +32 -0
  131. package/dist/universal/.trae/skills/citable/schemas/prompt-set.schema.json +47 -0
  132. package/dist/universal/.trae/skills/citable/schemas/representation-data.schema.json +21 -0
  133. package/dist/universal/.trae/skills/citable/schemas/research-dataset.schema.json +27 -0
  134. package/dist/universal/.trae/skills/citable/schemas/search-discovery-data.schema.json +20 -0
  135. package/dist/universal/.trae-cn/skills/citable/SKILL.md +1 -1
  136. package/dist/universal/.trae-cn/skills/citable/VERSION +1 -1
  137. package/dist/universal/.trae-cn/skills/citable/manifest.json +14 -5
  138. package/dist/universal/.trae-cn/skills/citable/schemas/claim-evidence.schema.json +21 -0
  139. package/dist/universal/.trae-cn/skills/citable/schemas/claim-verification.schema.json +18 -0
  140. package/dist/universal/.trae-cn/skills/citable/schemas/derivation.schema.json +18 -0
  141. package/dist/universal/.trae-cn/skills/citable/schemas/external-comparison.schema.json +16 -0
  142. package/dist/universal/.trae-cn/skills/citable/schemas/external-observation.schema.json +32 -0
  143. package/dist/universal/.trae-cn/skills/citable/schemas/prompt-set.schema.json +47 -0
  144. package/dist/universal/.trae-cn/skills/citable/schemas/representation-data.schema.json +21 -0
  145. package/dist/universal/.trae-cn/skills/citable/schemas/research-dataset.schema.json +27 -0
  146. package/dist/universal/.trae-cn/skills/citable/schemas/search-discovery-data.schema.json +20 -0
  147. package/dist/universal/manifest.json +172 -64
  148. package/package.json +1 -1
  149. package/schemas/claim-evidence.schema.json +21 -0
  150. package/schemas/claim-verification.schema.json +18 -0
  151. package/schemas/derivation.schema.json +18 -0
  152. package/schemas/external-comparison.schema.json +16 -0
  153. package/schemas/external-observation.schema.json +32 -0
  154. package/schemas/prompt-set.schema.json +47 -0
  155. package/schemas/representation-data.schema.json +21 -0
  156. package/schemas/research-dataset.schema.json +27 -0
  157. package/schemas/search-discovery-data.schema.json +20 -0
  158. package/skill/SKILL.md +1 -1
  159. package/src/connectors/gsc.js +4 -0
  160. package/src/discovery/searchConsole.js +186 -0
  161. package/src/evidence/external.js +251 -0
  162. package/src/representation/prompts.js +117 -0
  163. package/src/representation/representation.js +186 -0
@@ -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,117 @@
1
+ import { canonicalEvidenceJson } from '../evidence/hashes.js';
2
+ import { sha256 } from '../shared/io.js';
3
+ import { validateAgainst } from '../shared/schemaValidator.js';
4
+
5
+ const PROMPT_SCHEMA = 'prompt-set.schema.json';
6
+
7
+ function without(value, key) {
8
+ const copy = { ...value };
9
+ delete copy[key];
10
+ return copy;
11
+ }
12
+
13
+ function promptId({ prompt_text, context = null, locale = null }) {
14
+ return `PROMPT-${sha256(canonicalEvidenceJson({ prompt_text, context, locale })).slice(0, 24)}`;
15
+ }
16
+
17
+ export function createPrompt(input) {
18
+ if (!input || typeof input.prompt_text !== 'string' || !input.prompt_text.trim()) throw new TypeError('prompt_text is required');
19
+ const prompt = {
20
+ prompt_id: input.prompt_id || promptId(input),
21
+ prompt_text: input.prompt_text,
22
+ ...(input.context == null ? {} : { context: input.context }),
23
+ ...(input.locale == null ? {} : { locale: input.locale }),
24
+ };
25
+ return prompt;
26
+ }
27
+
28
+ function normalizeIncluded(prompts = []) {
29
+ if (!Array.isArray(prompts)) throw new TypeError('included_prompts must be an array');
30
+ const normalized = prompts.map(createPrompt);
31
+ const ids = new Set();
32
+ for (const item of normalized) {
33
+ if (ids.has(item.prompt_id)) throw new Error(`duplicate prompt_id: ${item.prompt_id}`);
34
+ ids.add(item.prompt_id);
35
+ }
36
+ return normalized.sort((left, right) => left.prompt_id.localeCompare(right.prompt_id));
37
+ }
38
+
39
+ function normalizeExcluded(prompts = []) {
40
+ if (!Array.isArray(prompts)) throw new TypeError('excluded_prompts must be an array');
41
+ return prompts.map((item) => ({
42
+ prompt_id: item.prompt_id || promptId(item),
43
+ ...(item.prompt_text == null ? {} : { prompt_text: item.prompt_text }),
44
+ exclusion_reason: item.exclusion_reason,
45
+ })).sort((left, right) => left.prompt_id.localeCompare(right.prompt_id));
46
+ }
47
+
48
+ function hashPromptSet(item) {
49
+ const value = without(item, 'prompt_set_hash');
50
+ return sha256(canonicalEvidenceJson({
51
+ ...value,
52
+ included_prompts: [...(value.included_prompts || [])].sort((left, right) => left.prompt_id.localeCompare(right.prompt_id)),
53
+ excluded_prompts: [...(value.excluded_prompts || [])].sort((left, right) => left.prompt_id.localeCompare(right.prompt_id)),
54
+ }));
55
+ }
56
+
57
+ export function createPromptSet(input, { previous = null } = {}) {
58
+ if (!input || typeof input.prompt_set_id !== 'string' || !input.prompt_set_id) throw new TypeError('prompt_set_id is required');
59
+ if (!Number.isInteger(input.prompt_set_version) || input.prompt_set_version < 1) throw new TypeError('prompt_set_version must be a positive integer');
60
+ const included = normalizeIncluded(input.included_prompts);
61
+ const excluded = normalizeExcluded(input.excluded_prompts || []);
62
+ const item = {
63
+ prompt_set_id: input.prompt_set_id,
64
+ prompt_set_version: input.prompt_set_version,
65
+ purpose: input.purpose,
66
+ generation_method: input.generation_method,
67
+ generation_method_version: input.generation_method_version ?? null,
68
+ generation_input: input.generation_input ?? null,
69
+ generated_at: input.generated_at,
70
+ population_size: included.length,
71
+ sampling_method: input.sampling_method,
72
+ included_prompts: included,
73
+ excluded_prompts: excluded,
74
+ intended_population: input.intended_population,
75
+ limitations: [...new Set(input.limitations || [])],
76
+ };
77
+ item.prompt_set_hash = hashPromptSet(item);
78
+ const check = validateAgainst(PROMPT_SCHEMA, item);
79
+ if (!check.valid) throw new TypeError(`prompt set violates contract: ${check.errors.join('; ')}`);
80
+ if (previous && previous.prompt_set_id === item.prompt_set_id && previous.prompt_set_version === item.prompt_set_version && previous.prompt_set_hash !== item.prompt_set_hash) {
81
+ throw new Error('prompt set version is immutable; create a new prompt_set_version');
82
+ }
83
+ return item;
84
+ }
85
+
86
+ export function verifyPromptSet(item, { previous = null } = {}) {
87
+ const failures = [];
88
+ const check = validateAgainst(PROMPT_SCHEMA, item);
89
+ if (!check.valid) failures.push(...check.errors);
90
+ if (item?.prompt_set_hash !== hashPromptSet(item || {})) failures.push('prompt set hash is inconsistent');
91
+ if (previous && previous.prompt_set_id === item?.prompt_set_id && previous.prompt_set_version === item?.prompt_set_version && previous.prompt_set_hash !== item?.prompt_set_hash) failures.push('prompt set version is immutable');
92
+ return { valid: failures.length === 0, failures };
93
+ }
94
+
95
+ export function promptReference(promptSet, promptIdValue) {
96
+ if (!verifyPromptSet(promptSet).valid) throw new Error('cannot reference an unverifiable prompt set');
97
+ const prompt = promptSet.included_prompts.find((item) => item.prompt_id === promptIdValue);
98
+ if (!prompt) throw new Error(`prompt ${promptIdValue} is not included in prompt set ${promptSet.prompt_set_id}@${promptSet.prompt_set_version}`);
99
+ return `${promptSet.prompt_set_id}@${promptSet.prompt_set_version}:${prompt.prompt_id}`;
100
+ }
101
+
102
+ export function comparePromptSets(left, right) {
103
+ const validLeft = verifyPromptSet(left).valid;
104
+ const validRight = verifyPromptSet(right).valid;
105
+ if (!validLeft || !validRight) return { status: 'INDETERMINATE', dimensions: {}, limitations: ['Prompt-set integrity could not be verified.'] };
106
+ const dimensions = {
107
+ prompt_set_id: left.prompt_set_id === right.prompt_set_id ? 'match' : 'mismatch',
108
+ prompt_set_version: left.prompt_set_version === right.prompt_set_version ? 'match' : 'mismatch',
109
+ population: left.population_size === right.population_size ? 'match' : 'mismatch',
110
+ sampling_method: left.sampling_method === right.sampling_method ? 'match' : 'mismatch',
111
+ };
112
+ const status = dimensions.prompt_set_id === 'match' && dimensions.prompt_set_version === 'match' && dimensions.population === 'match' && dimensions.sampling_method === 'match'
113
+ ? 'COMPARABLE' : 'NOT_COMPARABLE';
114
+ return { status, left_reference: `${left.prompt_set_id}@${left.prompt_set_version}`, right_reference: `${right.prompt_set_id}@${right.prompt_set_version}`, dimensions, limitations: status === 'COMPARABLE' ? [] : ['Prompt population, sampling, or version changed; no denominator is reused silently.'] };
115
+ }
116
+
117
+ export { promptId };
@@ -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 { verifyPromptSet, promptReference } from './prompts.js';
6
+
7
+ const REPRESENTATION_SCHEMA = 'representation-data.schema.json';
8
+ const COLLECTOR = { id: 'citable-representation-observations', version: '1.0.0' };
9
+ const PARSER = { id: 'provider-neutral-representation-parser', version: '1.0.0' };
10
+
11
+ function responseHash(rawResponse, response) {
12
+ return sha256(typeof rawResponse === 'string' ? rawResponse : canonicalEvidenceJson(response ?? null));
13
+ }
14
+
15
+ function modelIdentity(model) {
16
+ if (model && typeof model === 'string') return { value: model, status: 'known' };
17
+ if (model && typeof model === 'object' && ['known', 'UNKNOWN', 'UNAVAILABLE', 'NOT_DISCLOSED'].includes(model.status)) return { value: model.value ?? null, status: model.status };
18
+ return { value: null, status: 'UNKNOWN' };
19
+ }
20
+
21
+ function hostOf(value) {
22
+ try { return new URL(value).hostname.toLowerCase(); } catch { return null; }
23
+ }
24
+
25
+ function normalizeUrls(value) {
26
+ if (!Array.isArray(value)) throw new TypeError('provider citations must be an array');
27
+ const urls = value.map((item) => typeof item === 'string' ? item : item?.url);
28
+ if (urls.some((item) => typeof item !== 'string' || !hostOf(item))) throw new TypeError('provider citation URL is malformed');
29
+ return [...new Set(urls)];
30
+ }
31
+
32
+ const SENSITIVE_KEY = /(^|[-_])(authorization|cookie|set-cookie|access[-_]?token|refresh[-_]?token|api[-_]?key|password|secret)($|[-_])/i;
33
+
34
+ function safeMetadata(value, depth = 0) {
35
+ if (depth > 4 || value == null) return value == null ? null : '[bounded]';
36
+ if (Array.isArray(value)) return value.slice(0, 50).map((item) => safeMetadata(item, depth + 1));
37
+ if (typeof value !== 'object') return typeof value === 'string' ? value.slice(0, 500) : value;
38
+ return Object.fromEntries(Object.entries(value).slice(0, 50).filter(([key]) => !SENSITIVE_KEY.test(key)).map(([key, item]) => [key, safeMetadata(item, depth + 1)]));
39
+ }
40
+
41
+ function executionStatusFor(status) {
42
+ if (status === 'succeeded') return { observation_status: 'observed', retrieval_status: 'retrieved' };
43
+ if (status === 'malformed' || status === 'parser_failed') return { observation_status: 'indeterminate', retrieval_status: 'indeterminate' };
44
+ return { observation_status: 'failed', retrieval_status: 'failed' };
45
+ }
46
+
47
+ function parserErrorObservation({ provider, product, surface, prompt, promptSet, executionConfiguration, model, evidenceOrigin, executedAt, rawResponse, status, errorMessage }) {
48
+ const hash = responseHash(rawResponse, null);
49
+ const execution = executionConfiguration || { id: 'default', version: '1.0.0' };
50
+ const state = executionStatusFor(status);
51
+ return createRepresentationObservation({
52
+ provider, product, surface, prompt, promptSet, executionConfiguration: execution, model, evidenceOrigin, executedAt,
53
+ response: { status, response_hash: hash, parser: PARSER, raw_response_retained: false, provider_metadata: {} },
54
+ representation: { brand_mentioned: null, domain_mentioned: null, domain_cited: null, cited_urls: [], mentioned_entities: [], cited_domains: [] },
55
+ observationStatus: state.observation_status, retrievalStatus: state.retrieval_status,
56
+ limitations: [`Representation response could not be normalized: ${String(errorMessage || status).slice(0, 240)}`, 'Raw provider response is not retained; response_hash is retained for identity.'],
57
+ });
58
+ }
59
+
60
+ export function createRepresentationObservation({ provider, product, surface, prompt, promptSet, executionConfiguration, model, evidenceOrigin, executedAt = nowIso(), response, representation, observationStatus = 'observed', retrievalStatus = 'retrieved', limitations = [] }) {
61
+ if (!provider || !product || !surface) throw new TypeError('provider, product, and surface are required');
62
+ if (!prompt || !prompt.prompt_id || !prompt.prompt_text) throw new TypeError('prompt identity and text are required');
63
+ if (!promptSet || !verifyPromptSet(promptSet).valid) throw new TypeError('prompt set is missing or unverifiable');
64
+ if (!promptSet.included_prompts.some((item) => item.prompt_id === prompt.prompt_id)) throw new Error(`prompt ${prompt.prompt_id} is not included in the prompt set`);
65
+ if (!['LIVE_PROVIDER', 'RECORDED_FIXTURE', 'SYNTHETIC_TEST'].includes(evidenceOrigin)) throw new TypeError('evidenceOrigin is invalid');
66
+ const config = executionConfiguration || { id: 'default', version: '1.0.0' };
67
+ const normalizedModel = modelIdentity(model);
68
+ const data = {
69
+ provider,
70
+ product,
71
+ surface,
72
+ model: normalizedModel,
73
+ prompt: { prompt_id: prompt.prompt_id, prompt_version: promptSet.prompt_set_version, prompt_text: prompt.prompt_text },
74
+ prompt_set: { prompt_set_id: promptSet.prompt_set_id, prompt_set_version: promptSet.prompt_set_version },
75
+ execution: { executed_at: executedAt, configuration: config },
76
+ response,
77
+ representation,
78
+ evidence_origin: evidenceOrigin,
79
+ limitations: [...new Set(limitations)],
80
+ };
81
+ const check = validateAgainst(REPRESENTATION_SCHEMA, data);
82
+ if (!check.valid) throw new TypeError(`representation data violates contract: ${check.errors.join('; ')}`);
83
+ const state = executionStatusFor(response.status);
84
+ const observation = createExternalObservation({
85
+ observation_id: `REP-${sha256(canonicalEvidenceJson({ provider, product, surface, prompt, promptSet: promptSet.prompt_set_hash, executedAt, response })).slice(0, 24)}`,
86
+ observation_type: 'EXTERNAL_OBSERVATION',
87
+ subject: { type: 'prompt_execution', id: promptReference(promptSet, prompt.prompt_id) },
88
+ source: { provider, method: 'representation_adapter', authority: 'external_provider' },
89
+ collector: COLLECTOR,
90
+ parser: PARSER,
91
+ configuration: config,
92
+ observed_at: executedAt,
93
+ retrieved_at: executedAt,
94
+ executed_at: executedAt,
95
+ population: { declared: { id: `${promptSet.prompt_set_id}@${promptSet.prompt_set_version}`, size: promptSet.population_size }, observed: 1, retrieved: response.status === 'succeeded' ? 1 : 0, evaluated: response.status === 'succeeded' ? 1 : 0, unavailable: response.status === 'unavailable' ? 1 : 0, failed: response.status === 'succeeded' ? 0 : 1 },
96
+ observation_status: observationStatus || state.observation_status,
97
+ retrieval_status: retrievalStatus || state.retrieval_status,
98
+ data,
99
+ lineage: { source_evidence: response.response_hash ? [response.response_hash] : [] },
100
+ limitations: data.limitations,
101
+ });
102
+ return observation;
103
+ }
104
+
105
+ export function normalizeRepresentationResponse({ provider, product, surface, prompt, promptSet, executionConfiguration, model, response, rawResponse = null, evidenceOrigin = 'RECORDED_FIXTURE', executedAt = nowIso(), brand = null, firstPartyDomains = [], retainRawResponse = false }) {
106
+ const base = { provider, product, surface, prompt, promptSet, executionConfiguration, model, evidenceOrigin, executedAt };
107
+ const rawHash = responseHash(rawResponse, response);
108
+ try {
109
+ if (!response || typeof response !== 'object' || Array.isArray(response)) throw new TypeError('provider response must be an object');
110
+ const answerText = response.answer_text ?? response.answer ?? response.text;
111
+ if (typeof answerText !== 'string') throw new TypeError('provider response requires answer_text');
112
+ const citations = normalizeUrls(response.citations || []);
113
+ const mentionedEntities = Array.isArray(response.mentions) ? response.mentions.filter((item) => typeof item === 'string') : [];
114
+ const firstParty = new Set(firstPartyDomains.map((item) => String(item).toLowerCase()));
115
+ const citedDomains = [...new Set(citations.map(hostOf).filter(Boolean))];
116
+ const domainMentioned = firstParty.size ? [...firstParty].some((domain) => answerText.toLowerCase().includes(domain)) : null;
117
+ const domainCited = firstParty.size ? citedDomains.some((domain) => firstParty.has(domain)) : null;
118
+ const brandMentioned = typeof brand === 'string' ? answerText.toLowerCase().includes(brand.toLowerCase()) : null;
119
+ return createRepresentationObservation({
120
+ ...base,
121
+ response: { status: 'succeeded', response_hash: rawHash, parser: PARSER, raw_response_retained: retainRawResponse, provider_metadata: { ...safeMetadata(response.provider_metadata || {}), ...(retainRawResponse ? { raw_response: rawResponse ?? response } : {}) } },
122
+ representation: { brand_mentioned: brandMentioned, domain_mentioned: domainMentioned, domain_cited: domainCited, cited_urls: citations, mentioned_entities: mentionedEntities, cited_domains: citedDomains },
123
+ limitations: retainRawResponse ? [] : ['Raw provider response is not retained; response_hash is retained for identity.'],
124
+ });
125
+ } catch (error) {
126
+ return parserErrorObservation({ ...base, rawResponse, status: 'parser_failed', errorMessage: error.message });
127
+ }
128
+ }
129
+
130
+ export async function collectRepresentationObservations({ promptSet, adapter, provider, product = 'unknown', surface = 'unknown', model = null, executionConfiguration = { id: 'default', version: '1.0.0' }, evidenceOrigin = 'LIVE_PROVIDER', brand = null, firstPartyDomains = [] } = {}) {
131
+ if (!promptSet || !verifyPromptSet(promptSet).valid) throw new TypeError('prompt set is missing or unverifiable');
132
+ if (typeof adapter !== 'function') throw new TypeError('representation adapter is required');
133
+ const observations = [];
134
+ for (const prompt of promptSet.included_prompts) {
135
+ const executedAt = nowIso();
136
+ try {
137
+ const result = await adapter(prompt, { provider, product, surface, model, promptSet, executionConfiguration });
138
+ if (!result || typeof result !== 'object') throw new Error('adapter returned an invalid result');
139
+ const status = result.status || 'succeeded';
140
+ if (status !== 'succeeded') {
141
+ observations.push(parserErrorObservation({ provider, product, surface, prompt, promptSet, executionConfiguration, model, evidenceOrigin, executedAt, rawResponse: result.raw ?? result.body ?? null, status, errorMessage: result.error || status }));
142
+ } else {
143
+ observations.push(normalizeRepresentationResponse({ provider, product, surface, prompt, promptSet, executionConfiguration, model, response: result.body ?? result.response ?? result, rawResponse: result.raw ?? null, evidenceOrigin, executedAt, brand, firstPartyDomains }));
144
+ }
145
+ } catch (error) {
146
+ observations.push(parserErrorObservation({ provider, product, surface, prompt, promptSet, executionConfiguration, model, evidenceOrigin, executedAt, rawResponse: null, status: error.connectorState === 'quota_limited' ? 'rate_limited' : error.name === 'AbortError' ? 'timeout' : 'error', errorMessage: error.message }));
147
+ }
148
+ }
149
+ return { observations, coverage: { declared: promptSet.population_size, executed: observations.filter((item) => item.data.response.status === 'succeeded').length, failed: observations.filter((item) => item.data.response.status !== 'succeeded').length, coverage_status: observations.every((item) => item.data.response.status === 'succeeded') ? 'complete_for_declared_prompt_set' : 'incomplete' }, prompt_set_reference: `${promptSet.prompt_set_id}@${promptSet.prompt_set_version}` };
150
+ }
151
+
152
+ export function createHttpRepresentationAdapter({ endpoint, fetchImpl = globalThis.fetch, accessToken = null } = {}) {
153
+ if (typeof endpoint !== 'string' || !/^https:\/\//.test(endpoint)) throw new TypeError('representation adapter endpoint must use HTTPS');
154
+ return async (prompt) => {
155
+ const response = await fetchImpl(endpoint, { method: 'POST', redirect: 'error', headers: { accept: 'application/json', 'content-type': 'application/json', ...(accessToken ? { authorization: `Bearer ${accessToken}` } : {}) }, body: JSON.stringify({ prompt_id: prompt.prompt_id, prompt_text: prompt.prompt_text }) });
156
+ const raw = await response.text();
157
+ if (!response.ok) {
158
+ const status = response.status === 429 ? 'rate_limited' : response.status === 408 ? 'timeout' : response.status >= 500 ? 'unavailable' : 'refused';
159
+ return { status, raw, error: `provider returned ${response.status}` };
160
+ }
161
+ let body;
162
+ try { body = JSON.parse(raw); } catch { return { status: 'malformed', raw, error: 'provider response was not valid JSON' }; }
163
+ return { status: 'succeeded', body, raw };
164
+ };
165
+ }
166
+
167
+ export function verifyRepresentationObservation(item) {
168
+ return verifyExternalObservation(item);
169
+ }
170
+
171
+ export function compareRepresentationObservations(left, right) {
172
+ if (!verifyExternalObservation(left).valid || !verifyExternalObservation(right).valid) return { status: 'INDETERMINATE', dimensions: {}, limitations: ['Representation observation integrity could not be verified.'] };
173
+ const dimensions = {
174
+ provider: left.data.provider === right.data.provider ? 'match' : 'mismatch',
175
+ product: left.data.product === right.data.product ? 'match' : 'mismatch',
176
+ surface: left.data.surface === right.data.surface ? 'match' : 'mismatch',
177
+ model: left.data.model.status !== 'known' || right.data.model.status !== 'known' ? 'missing' : left.data.model.value === right.data.model.value ? 'match' : 'mismatch',
178
+ prompt: left.data.prompt.prompt_id === right.data.prompt.prompt_id && left.data.prompt.prompt_text === right.data.prompt.prompt_text ? 'match' : 'mismatch',
179
+ prompt_set: left.data.prompt_set.prompt_set_id === right.data.prompt_set.prompt_set_id && left.data.prompt_set.prompt_set_version === right.data.prompt_set.prompt_set_version ? 'match' : 'mismatch',
180
+ configuration: left.data.execution.configuration.id === right.data.execution.configuration.id && left.data.execution.configuration.version === right.data.execution.configuration.version ? 'match' : 'mismatch',
181
+ parser: left.data.response.parser.id === right.data.response.parser.id && left.data.response.parser.version === right.data.response.parser.version ? 'match' : 'mismatch',
182
+ };
183
+ const hardMismatch = ['provider', 'product', 'surface', 'prompt', 'prompt_set', 'configuration', 'parser'].some((key) => dimensions[key] === 'mismatch');
184
+ const status = hardMismatch ? 'NOT_COMPARABLE' : dimensions.model === 'mismatch' ? 'NOT_COMPARABLE' : dimensions.model === 'missing' ? 'PARTIALLY_COMPARABLE' : 'COMPARABLE';
185
+ return { comparison_id: `REP-CMP-${sha256(canonicalEvidenceJson({ left: left.observation_id, right: right.observation_id, dimensions })).slice(0, 16)}`, left_reference: left.observation_id, right_reference: right.observation_id, status, dimensions, limitations: status === 'COMPARABLE' ? [] : ['Provider, model, prompt, execution, and parser identity remain explicit; no visibility delta is inferred from non-comparable observations.'] };
186
+ }