@refract-org/evidence-graph 0.2.1

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 (36) hide show
  1. package/LICENSE +11 -0
  2. package/README.md +29 -0
  3. package/dist/src/schemas/claim.d.ts +34 -0
  4. package/dist/src/schemas/claim.d.ts.map +1 -0
  5. package/dist/src/schemas/claim.js +3 -0
  6. package/dist/src/schemas/claim.js.map +1 -0
  7. package/dist/src/schemas/evidence.d.ts +34 -0
  8. package/dist/src/schemas/evidence.d.ts.map +1 -0
  9. package/dist/src/schemas/evidence.js +3 -0
  10. package/dist/src/schemas/evidence.js.map +1 -0
  11. package/dist/src/schemas/report.d.ts +51 -0
  12. package/dist/src/schemas/report.d.ts.map +1 -0
  13. package/dist/src/schemas/report.js +3 -0
  14. package/dist/src/schemas/report.js.map +1 -0
  15. package/dist/src/schemas/revision.d.ts +36 -0
  16. package/dist/src/schemas/revision.d.ts.map +1 -0
  17. package/dist/src/schemas/revision.js +3 -0
  18. package/dist/src/schemas/revision.js.map +1 -0
  19. package/dist/src/schemas/source.d.ts +24 -0
  20. package/dist/src/schemas/source.d.ts.map +1 -0
  21. package/dist/src/schemas/source.js +3 -0
  22. package/dist/src/schemas/source.js.map +1 -0
  23. package/dist/tsconfig 2.tsbuildinfo +1 -0
  24. package/dist/tsconfig.tsbuildinfo +1 -0
  25. package/package.json +26 -0
  26. package/src/__tests__/hash-identity.test.ts +111 -0
  27. package/src/__tests__/replay-manifest.test.ts +87 -0
  28. package/src/hash-identity.ts +25 -0
  29. package/src/index.ts +43 -0
  30. package/src/interpretation-prompt.ts +209 -0
  31. package/src/replay-manifest.ts +110 -0
  32. package/src/schemas/claim.ts +58 -0
  33. package/src/schemas/evidence.ts +83 -0
  34. package/src/schemas/report.ts +54 -0
  35. package/src/schemas/revision.ts +41 -0
  36. package/src/schemas/source.ts +37 -0
@@ -0,0 +1,111 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { createClaimIdentity, createEventIdentity } from "../hash-identity.js";
3
+ import type { EvidenceEvent } from "../schemas/evidence.js";
4
+
5
+ describe("Claim identity hash", () => {
6
+ it("produces deterministic hash from same inputs", () => {
7
+ const a = createClaimIdentity({
8
+ text: "The sky is blue",
9
+ section: "Introduction",
10
+ pageTitle: "Color",
11
+ pageId: 12345,
12
+ });
13
+ const b = createClaimIdentity({
14
+ text: "The sky is blue",
15
+ section: "Introduction",
16
+ pageTitle: "Color",
17
+ pageId: 12345,
18
+ });
19
+ expect(a.claimId).toBe(b.claimId);
20
+ expect(a.identityKey).toBe(b.identityKey);
21
+ });
22
+
23
+ it("produces different hash for different text", () => {
24
+ const a = createClaimIdentity({
25
+ text: "The sky is blue",
26
+ section: "Introduction",
27
+ pageTitle: "Color",
28
+ pageId: 12345,
29
+ });
30
+ const b = createClaimIdentity({
31
+ text: "The sky is green",
32
+ section: "Introduction",
33
+ pageTitle: "Color",
34
+ pageId: 12345,
35
+ });
36
+ expect(a.claimId).not.toBe(b.claimId);
37
+ });
38
+
39
+ it("produces different hash for different sections", () => {
40
+ const a = createClaimIdentity({
41
+ text: "The sky is blue",
42
+ section: "Introduction",
43
+ pageTitle: "Color",
44
+ pageId: 12345,
45
+ });
46
+ const b = createClaimIdentity({
47
+ text: "The sky is blue",
48
+ section: "Discussion",
49
+ pageTitle: "Color",
50
+ pageId: 12345,
51
+ });
52
+ expect(a.claimId).not.toBe(b.claimId);
53
+ });
54
+
55
+ it("normalizes whitespace and case", () => {
56
+ const a = createClaimIdentity({
57
+ text: " The Sky Is Blue ",
58
+ section: "Introduction",
59
+ pageTitle: "Color",
60
+ pageId: 12345,
61
+ });
62
+ const b = createClaimIdentity({
63
+ text: "the sky is blue",
64
+ section: "Introduction",
65
+ pageTitle: "Color",
66
+ pageId: 12345,
67
+ });
68
+ expect(a.claimId).toBe(b.claimId);
69
+ });
70
+
71
+ it("claimId is 16 hex characters", () => {
72
+ const id = createClaimIdentity({
73
+ text: "test",
74
+ section: "test",
75
+ pageTitle: "Test",
76
+ pageId: 1,
77
+ });
78
+ expect(id.claimId).toMatch(/^[0-9a-f]{16}$/);
79
+ });
80
+ });
81
+
82
+ describe("createEventIdentity", () => {
83
+ const base: Omit<EvidenceEvent, "eventId" | "modelInterpretation"> = {
84
+ eventType: "revert_detected",
85
+ fromRevisionId: 1,
86
+ toRevisionId: 2,
87
+ section: "body",
88
+ before: "",
89
+ after: "reverted",
90
+ deterministicFacts: [{ fact: "test" }],
91
+ layer: "observed",
92
+ timestamp: "2026-01-01T00:00:00Z",
93
+ };
94
+
95
+ it("produces a deterministic 16-char hex hash", () => {
96
+ const id = createEventIdentity(base);
97
+ expect(id).toMatch(/^[0-9a-f]{16}$/);
98
+ });
99
+
100
+ it("is deterministic for same input", () => {
101
+ const a = createEventIdentity(base);
102
+ const b = createEventIdentity(base);
103
+ expect(a).toBe(b);
104
+ });
105
+
106
+ it("differs for different event types", () => {
107
+ const a = createEventIdentity({ ...base, eventType: "sentence_removed" });
108
+ const b = createEventIdentity(base);
109
+ expect(a).not.toBe(b);
110
+ });
111
+ });
@@ -0,0 +1,87 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { EvidenceEvent, Revision } from "../index.js";
3
+ import { createReplayManifest } from "../replay-manifest.js";
4
+
5
+ const rev: Revision = {
6
+ revId: 1,
7
+ pageId: 100,
8
+ pageTitle: "Test",
9
+ timestamp: "2026-01-01T00:00:00Z",
10
+ comment: "first edit",
11
+ content: "Hello world",
12
+ size: 11,
13
+ minor: false,
14
+ };
15
+
16
+ const event: EvidenceEvent = {
17
+ eventType: "revert_detected",
18
+ fromRevisionId: 1,
19
+ toRevisionId: 2,
20
+ section: "",
21
+ before: "",
22
+ after: "reverted",
23
+ deterministicFacts: [{ fact: "revert" }],
24
+ layer: "observed",
25
+ timestamp: "2026-01-01T00:00:00Z",
26
+ };
27
+
28
+ describe("createReplayManifest", () => {
29
+ it("produces a manifest with expected format", () => {
30
+ const manifest = createReplayManifest({
31
+ pageTitle: "Test",
32
+ analyzerVersions: { "revert-detector": "0.1.0" },
33
+ revisions: [rev],
34
+ events: [event],
35
+ generatedAt: "2026-01-01T00:00:00.000Z",
36
+ });
37
+
38
+ expect(manifest.format).toBe("refract-replay-manifest/v1");
39
+ expect(manifest.pageTitle).toBe("Test");
40
+ expect(manifest.analyzerVersions["revert-detector"]).toBe("0.1.0");
41
+ });
42
+
43
+ it("generates a manifest hash", () => {
44
+ const manifest = createReplayManifest({
45
+ pageTitle: "Test",
46
+ analyzerVersions: { "revert-detector": "0.1.0" },
47
+ revisions: [rev],
48
+ events: [event],
49
+ generatedAt: "2026-01-01T00:00:00.000Z",
50
+ });
51
+
52
+ expect(manifest.manifestHash).toMatch(/^[0-9a-f]{64}$/);
53
+ });
54
+
55
+ it("includes input and output hashes", () => {
56
+ const manifest = createReplayManifest({
57
+ pageTitle: "Test",
58
+ analyzerVersions: { "revert-detector": "0.1.0" },
59
+ revisions: [rev],
60
+ events: [event],
61
+ generatedAt: "2026-01-01T00:00:00.000Z",
62
+ });
63
+
64
+ expect(manifest.inputRevisionHashes).toHaveLength(1);
65
+ expect(manifest.outputEventHashes).toHaveLength(1);
66
+ });
67
+
68
+ it("is deterministic for same inputs", () => {
69
+ const a = createReplayManifest({
70
+ pageTitle: "Test",
71
+ analyzerVersions: { "revert-detector": "0.1.0" },
72
+ revisions: [rev],
73
+ events: [event],
74
+ generatedAt: "2026-01-01T00:00:00.000Z",
75
+ });
76
+
77
+ const b = createReplayManifest({
78
+ pageTitle: "Test",
79
+ analyzerVersions: { "revert-detector": "0.1.0" },
80
+ revisions: [rev],
81
+ events: [event],
82
+ generatedAt: "2026-01-01T00:00:00.000Z",
83
+ });
84
+
85
+ expect(a.manifestHash).toBe(b.manifestHash);
86
+ });
87
+ });
@@ -0,0 +1,25 @@
1
+ import { createHash } from "node:crypto";
2
+ import type { ClaimIdentity } from "./schemas/claim.js";
3
+ import type { EvidenceEvent } from "./schemas/evidence.js";
4
+
5
+ export function createClaimIdentity(params: {
6
+ text: string;
7
+ section: string;
8
+ pageTitle: string;
9
+ pageId: number;
10
+ }): ClaimIdentity {
11
+ const identityKey = `${params.pageTitle}|${params.pageId}|${params.section}|${params.text.toLowerCase().trim()}`;
12
+ const claimId = createHash("sha256").update(identityKey).digest("hex").slice(0, 16);
13
+ return {
14
+ claimId,
15
+ identityKey,
16
+ pageTitle: params.pageTitle,
17
+ pageId: params.pageId,
18
+ };
19
+ }
20
+
21
+ export function createEventIdentity(event: Omit<EvidenceEvent, "eventId" | "modelInterpretation">): string {
22
+ const factsStr = event.deterministicFacts.map((f) => `${f.fact}:${f.detail ?? ""}`).join("|");
23
+ const identityKey = `${event.eventType}|${event.fromRevisionId}|${event.toRevisionId}|${event.section}|${event.before}|${event.after}|${event.timestamp}|${factsStr}`;
24
+ return createHash("sha256").update(identityKey).digest("hex").slice(0, 16);
25
+ }
package/src/index.ts ADDED
@@ -0,0 +1,43 @@
1
+ export { createClaimIdentity, createEventIdentity } from "./hash-identity.js";
2
+ export {
3
+ buildInterpretationPrompt,
4
+ ModelInterpretationSchema,
5
+ parseInterpretationResponse,
6
+ } from "./interpretation-prompt.js";
7
+ export type { MerkleProof, ReplayManifest } from "./replay-manifest.js";
8
+ export {
9
+ buildMerkleTree,
10
+ createReplayManifest,
11
+ getMerkleProof,
12
+ singleEventProof,
13
+ verifyMerkleProof,
14
+ } from "./replay-manifest.js";
15
+ export type {
16
+ ClaimIdentity,
17
+ ClaimLineage,
18
+ ClaimObject,
19
+ ClaimState,
20
+ ClaimVariant,
21
+ PropositionType,
22
+ } from "./schemas/claim.js";
23
+ export type {
24
+ DeterministicFact,
25
+ EventType,
26
+ EvidenceEvent,
27
+ EvidenceLayer,
28
+ FactProvenance,
29
+ ModelInterpretation,
30
+ PolicyDimension,
31
+ } from "./schemas/evidence.js";
32
+ export type {
33
+ Depth,
34
+ ExportFormat,
35
+ PageTimeline,
36
+ PolicySignal,
37
+ Report,
38
+ ReportLayer,
39
+ ReportLayerLabel,
40
+ TimelineEvent,
41
+ } from "./schemas/report.js";
42
+ export type { DiffLine, DiffResult, Revision, Section, SectionChange } from "./schemas/revision.js";
43
+ export type { SourceAuthority, SourceLineage, SourceRecord, SourceReplacement, SourceType } from "./schemas/source.js";
@@ -0,0 +1,209 @@
1
+ import type { EventType, EvidenceEvent, ModelInterpretation } from "./schemas/evidence.js";
2
+
3
+ const _ALLOWED_EVENT_TYPES: EventType[] = [
4
+ "sentence_first_seen",
5
+ "sentence_removed",
6
+ "sentence_reintroduced",
7
+ "citation_added",
8
+ "citation_removed",
9
+ "citation_replaced",
10
+ "template_added",
11
+ "template_removed",
12
+ "revert_detected",
13
+ "section_reorganized",
14
+ "lead_promotion",
15
+ "lead_demotion",
16
+ "page_moved",
17
+ "wikilink_added",
18
+ "wikilink_removed",
19
+ "category_added",
20
+ "category_removed",
21
+ "protection_changed",
22
+ "talk_page_correlated",
23
+ "talk_thread_opened",
24
+ "talk_thread_archived",
25
+ "talk_reply_added",
26
+ "template_parameter_changed",
27
+ "edit_cluster_detected",
28
+ "talk_activity_spike",
29
+ ];
30
+
31
+ export const ModelInterpretationSchema = {
32
+ type: "object" as const,
33
+ properties: {
34
+ semanticChange: { type: "string" as const, description: "Description of what changed and why" },
35
+ confidence: {
36
+ type: "number" as const,
37
+ minimum: 0,
38
+ maximum: 1,
39
+ description: "Confidence in this interpretation (0.0–1.0)",
40
+ },
41
+ policyDimension: {
42
+ type: "string" as const,
43
+ enum: [
44
+ "verifiability",
45
+ "npov",
46
+ "blp",
47
+ "due_weight",
48
+ "protection",
49
+ "edit_warring",
50
+ "notability",
51
+ "copyright",
52
+ "civility",
53
+ ],
54
+ description: "Wikipedia policy dimension this event relates to",
55
+ },
56
+ discussionType: {
57
+ type: "string" as const,
58
+ enum: [
59
+ "notability_challenge",
60
+ "sourcing_dispute",
61
+ "neutrality_concern",
62
+ "content_deletion",
63
+ "content_addition",
64
+ "naming_dispute",
65
+ "procedural",
66
+ "other",
67
+ ],
68
+ description: "Type of talk page discussion this event correlates with",
69
+ },
70
+ },
71
+ required: ["semanticChange", "confidence"],
72
+ } as const;
73
+
74
+ const EVENT_DESCRIPTIONS: Partial<Record<EventType, string>> = {
75
+ sentence_first_seen: "a sentence appeared for the first time",
76
+ sentence_removed: "a sentence was deleted entirely",
77
+ sentence_reintroduced: "a previously removed sentence was restored",
78
+ citation_added: "a new reference or citation was added",
79
+ citation_removed: "an existing reference was removed",
80
+ citation_replaced: "one citation was replaced by another",
81
+ template_added: "a maintenance or policy template was added",
82
+ template_removed: "a template was removed",
83
+ revert_detected: "an edit summary indicates a revert",
84
+ section_reorganized: "sections were added, removed, or reordered",
85
+ lead_promotion: "content moved from the body into the lead section",
86
+ lead_demotion: "content moved from the lead into the body",
87
+ page_moved: "the page was renamed",
88
+ wikilink_added: "a new internal link was added",
89
+ wikilink_removed: "an internal link was removed",
90
+ category_added: "a category was added",
91
+ category_removed: "a category was removed",
92
+ protection_changed: "page protection level changed",
93
+ talk_page_correlated: "a talk page discussion exists near this revision",
94
+ talk_thread_opened: "a new talk page thread was created",
95
+ talk_thread_archived: "a talk page thread was archived",
96
+ talk_reply_added: "a reply was posted in an existing talk thread",
97
+ template_parameter_changed: "a template parameter was modified",
98
+ edit_cluster_detected: "multiple edits within a short time window",
99
+ talk_activity_spike: "talk page activity exceeds normal levels",
100
+ };
101
+
102
+ const POLICY_DIMENSION_DESCRIPTIONS: Record<string, string> = {
103
+ verifiability: "claims and sourcing — citations added/removed, citation needed tags",
104
+ npov: "neutral point of view — bias, balancing claims, fair representation",
105
+ blp: "biographies of living persons — potentially defamatory or unsourced claims about living people",
106
+ due_weight: "proportionate coverage — undue emphasis on fringe views",
107
+ protection: "page protection — edit restrictions, dispute mitigation",
108
+ edit_warring: "revert cycles and content disputes",
109
+ notability: "whether the topic meets inclusion criteria",
110
+ copyright: "potential copyright violations",
111
+ civility: "editor conduct and dispute tone",
112
+ };
113
+
114
+ export function buildInterpretationPrompt(events: EvidenceEvent[], pageTitle: string): string {
115
+ const summary = summarizeEvents(events);
116
+
117
+ const lines: string[] = [
118
+ `You are analyzing edit history events from the Wikipedia page "${pageTitle}".`,
119
+ "For each event below, provide a structured interpretation: what changed semantically,",
120
+ "your confidence level, which (if any) Wikipedia policy dimension applies, and whether",
121
+ "this event correlates with a talk page discussion type.",
122
+ "",
123
+ "These are mechanically observed events — factual descriptions of what appeared or disappeared",
124
+ "at revision boundaries. Your job is not to repeat the mechanical description but to",
125
+ "interpret what the change means in editorial context.",
126
+ "",
127
+ "Return a JSON array with one object per event, using this schema:",
128
+ JSON.stringify(ModelInterpretationSchema, null, 2),
129
+ "",
130
+ `Total events: ${events.length}`,
131
+ `Page: ${pageTitle}`,
132
+ `Event type breakdown:`,
133
+ ...Object.entries(summary.byType)
134
+ .sort(([, a], [, b]) => b - a)
135
+ .map(([type, count]) => ` ${type}: ${count} — ${EVENT_DESCRIPTIONS[type as EventType] ?? ""}`),
136
+ "",
137
+ "Policy dimensions:",
138
+ ...Object.entries(POLICY_DIMENSION_DESCRIPTIONS).map(([dim, desc]) => ` ${dim}: ${desc}`),
139
+ "",
140
+ "Discussion types:",
141
+ ...[
142
+ "notability_challenge",
143
+ "sourcing_dispute",
144
+ "neutrality_concern",
145
+ "content_deletion",
146
+ "content_addition",
147
+ "naming_dispute",
148
+ "procedural",
149
+ "other",
150
+ ].map((t) => ` ${t}`),
151
+ "",
152
+ "Events:",
153
+ ...events.map((e, i) => {
154
+ const section = e.section ? ` [${e.section}]` : "";
155
+ const facts = e.deterministicFacts.map((f) => `${f.fact}${f.detail ? `: ${f.detail}` : ""}`).join("; ");
156
+ return ` ${i + 1}. ${e.eventType}${section} (rev ${e.fromRevisionId}→${e.toRevisionId})${facts ? ` — ${facts}` : ""}`;
157
+ }),
158
+ ];
159
+
160
+ return lines.join("\n");
161
+ }
162
+
163
+ export function parseInterpretationResponse(text: string): ModelInterpretation[] {
164
+ const cleaned = text.trim();
165
+
166
+ let parsed: unknown;
167
+ try {
168
+ parsed = JSON.parse(cleaned);
169
+ } catch {
170
+ const jsonMatch = cleaned.match(/\[[\s\S]*\]/);
171
+ if (jsonMatch) {
172
+ try {
173
+ parsed = JSON.parse(jsonMatch[0]);
174
+ } catch {
175
+ return [];
176
+ }
177
+ } else {
178
+ return [];
179
+ }
180
+ }
181
+
182
+ if (!Array.isArray(parsed)) return [];
183
+
184
+ return parsed
185
+ .map((item: unknown) => {
186
+ if (typeof item !== "object" || item === null) return null;
187
+ const obj = item as Record<string, unknown>;
188
+ if (typeof obj.semanticChange !== "string" || typeof obj.confidence !== "number") return null;
189
+ return {
190
+ semanticChange: obj.semanticChange,
191
+ confidence: Math.max(0, Math.min(1, obj.confidence)),
192
+ ...(typeof obj.policyDimension === "string"
193
+ ? { policyDimension: obj.policyDimension as ModelInterpretation["policyDimension"] }
194
+ : {}),
195
+ ...(typeof obj.discussionType === "string"
196
+ ? { discussionType: obj.discussionType as ModelInterpretation["discussionType"] }
197
+ : {}),
198
+ } satisfies ModelInterpretation;
199
+ })
200
+ .filter((item): item is ModelInterpretation => item !== null);
201
+ }
202
+
203
+ function summarizeEvents(events: EvidenceEvent[]): { byType: Record<string, number> } {
204
+ const byType: Record<string, number> = {};
205
+ for (const e of events) {
206
+ byType[e.eventType] = (byType[e.eventType] ?? 0) + 1;
207
+ }
208
+ return { byType };
209
+ }
@@ -0,0 +1,110 @@
1
+ import { createHash } from "node:crypto";
2
+ import { createEventIdentity } from "./hash-identity.js";
3
+ import type { EvidenceEvent } from "./schemas/evidence.js";
4
+ import type { Revision } from "./schemas/revision.js";
5
+
6
+ export interface MerkleProof {
7
+ leafHash: string;
8
+ leafIndex: number;
9
+ siblings: string[];
10
+ rootHash: string;
11
+ }
12
+
13
+ export interface ReplayManifest {
14
+ format: "refract-replay-manifest/v1";
15
+ generatedAt: string;
16
+ pageTitle: string;
17
+ analyzerVersions: Record<string, string>;
18
+ inputRevisionHashes: string[];
19
+ outputEventHashes: string[];
20
+ merkleRoot: string;
21
+ manifestHash: string;
22
+ }
23
+
24
+ function hashPair(a: string, b: string): string {
25
+ return createHash("sha256")
26
+ .update(a < b ? a + b : b + a)
27
+ .digest("hex");
28
+ }
29
+
30
+ export function buildMerkleTree(hashes: string[]): string[][] {
31
+ if (hashes.length === 0) return [[""]];
32
+ const levels: string[][] = [hashes];
33
+ let current = hashes;
34
+ while (current.length > 1) {
35
+ const next: string[] = [];
36
+ for (let i = 0; i < current.length; i += 2) {
37
+ if (i + 1 < current.length) {
38
+ next.push(hashPair(current[i], current[i + 1]));
39
+ } else {
40
+ next.push(current[i]);
41
+ }
42
+ }
43
+ levels.push(next);
44
+ current = next;
45
+ }
46
+ return levels;
47
+ }
48
+
49
+ export function getMerkleProof(levels: string[][], leafIndex: number): MerkleProof {
50
+ const leafHash = levels[0][leafIndex];
51
+ if (!leafHash) throw new Error(`Leaf index ${leafIndex} out of range`);
52
+ const siblings: string[] = [];
53
+ let idx = leafIndex;
54
+ for (let level = 0; level < levels.length - 1; level++) {
55
+ const isLeft = idx % 2 === 0;
56
+ const siblingIdx = isLeft ? idx + 1 : idx - 1;
57
+ if (siblingIdx < levels[level].length) {
58
+ siblings.push(levels[level][siblingIdx]);
59
+ }
60
+ idx = Math.floor(idx / 2);
61
+ }
62
+ const root = levels[levels.length - 1];
63
+ return {
64
+ leafHash,
65
+ leafIndex,
66
+ siblings,
67
+ rootHash: root[0] ?? "",
68
+ };
69
+ }
70
+
71
+ export function verifyMerkleProof(proof: MerkleProof): boolean {
72
+ let hash = proof.leafHash;
73
+ for (const sibling of proof.siblings) {
74
+ hash = hashPair(hash, sibling);
75
+ }
76
+ return hash === proof.rootHash;
77
+ }
78
+
79
+ export function createReplayManifest(params: {
80
+ pageTitle: string;
81
+ analyzerVersions: Record<string, string>;
82
+ revisions: Revision[];
83
+ events: EvidenceEvent[];
84
+ generatedAt?: string;
85
+ }): ReplayManifest {
86
+ const inputHashes = params.revisions.map((r) => createHash("sha256").update(r.content).digest("hex"));
87
+
88
+ const outputHashes = params.events.map((e) => e.eventId ?? createEventIdentity(e));
89
+
90
+ const merkleRoot = buildMerkleTree(outputHashes).at(-1)?.[0] ?? "";
91
+
92
+ const partial = {
93
+ format: "refract-replay-manifest/v1" as const,
94
+ generatedAt: params.generatedAt ?? new Date().toISOString(),
95
+ pageTitle: params.pageTitle,
96
+ analyzerVersions: params.analyzerVersions,
97
+ inputRevisionHashes: inputHashes,
98
+ outputEventHashes: outputHashes,
99
+ merkleRoot,
100
+ };
101
+
102
+ const manifestHash = createHash("sha256").update(JSON.stringify(partial)).digest("hex");
103
+
104
+ return { ...partial, manifestHash };
105
+ }
106
+
107
+ export function singleEventProof(manifest: ReplayManifest, eventIndex: number): MerkleProof {
108
+ const levels = buildMerkleTree(manifest.outputEventHashes);
109
+ return getMerkleProof(levels, eventIndex);
110
+ }
@@ -0,0 +1,58 @@
1
+ // Claim object — the core provenance unit
2
+
3
+ export type PropositionType =
4
+ | "factual_claim"
5
+ | "attributed_claim"
6
+ | "institutional_finding"
7
+ | "allegation"
8
+ | "counterclaim"
9
+ | "policy_statement"
10
+ | "editorial_note"
11
+ | "unknown";
12
+
13
+ export type ClaimState =
14
+ | "absent"
15
+ | "emerging"
16
+ | "contested"
17
+ | "softened"
18
+ | "strengthened"
19
+ | "stabilizing"
20
+ | "hardened"
21
+ | "receding"
22
+ | "deleted"
23
+ | "reintroduced";
24
+
25
+ export interface ClaimIdentity {
26
+ claimId: string; // Deterministic hash from identity key
27
+ identityKey: string; // Canonical claim text + section + page
28
+ pageTitle: string;
29
+ pageId: number;
30
+ }
31
+
32
+ export interface ClaimVariant {
33
+ revisionId: number;
34
+ text: string;
35
+ section: string;
36
+ observedAt: string; // ISO 8601
37
+ }
38
+
39
+ export interface ClaimLineage {
40
+ firstSeenRevisionId: number;
41
+ firstSeenAt: string; // ISO 8601
42
+ lastSeenRevisionId?: number;
43
+ lastSeenAt?: string;
44
+ variants: ClaimVariant[];
45
+ mergeSourceIds?: string[];
46
+ splitTargetIds?: string[];
47
+ deprecatedAt?: string;
48
+ deprecatedByClaimId?: string;
49
+ }
50
+
51
+ export interface ClaimObject {
52
+ identity: ClaimIdentity;
53
+ lineage: ClaimLineage;
54
+ currentState: ClaimState;
55
+ propositionType: PropositionType;
56
+ sourceLineage: string[]; // SourceRecord IDs
57
+ phase: string; // Phase tag: Phase 0 | Phase 1b | Phase 2a | Phase 2b
58
+ }