Skip to content
This repository was archived by the owner on Oct 13, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "0.1.0",
"private": true,
"type": "module",
"description": "Standalone visualization UI for Refract \u2014 load JSONL, render timelines, diffs, citations",
"description": "Standalone visualization UI for Refract — load JSONL, render timelines, diffs, citations",
"license": "AGPL-3.0",
"repository": {
"type": "git",
Expand All @@ -26,6 +26,7 @@
},
"devDependencies": {
"@biomejs/biome": "^2.5.8",
"happy-dom": "^20.14.5",
"husky": "^9.1.7",
"lint-staged": "^17.0.5",
"typescript": "^5.7.0",
Expand Down
99 changes: 99 additions & 0 deletions tests/citations.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { beforeEach, describe, expect, it } from "vitest";
import { CitationChurnChart } from "../src/citations/CitationChurnChart.js";
import { SourceChangeTable } from "../src/citations/SourceChangeTable.js";
import type { EvidenceEvent } from "../src/types.js";

describe("CitationChurnChart", () => {
let container: HTMLElement;

beforeEach(() => {
container = document.createElement("div");
});

it("renders empty state when no citation events exist", () => {
const chart = new CitationChurnChart(container);
chart.setData([]);

expect(container.textContent).toContain("No citation events found");
});

it("renders added/removed metrics and chart bars when citation events exist", () => {
const chart = new CitationChurnChart(container);
const events: EvidenceEvent[] = [
{
eventId: "c1",
eventType: "citation_added",
fromRevisionId: 10,
toRevisionId: 11,
timestamp: "2026-02-01T00:00:00Z",
section: "References",
before: "",
after: "<ref>Source A</ref>",
deterministicFacts: [],
layer: "observed",
},
{
eventId: "c2",
eventType: "citation_removed",
fromRevisionId: 11,
toRevisionId: 12,
timestamp: "2026-02-02T00:00:00Z",
section: "References",
before: "<ref>Source B</ref>",
after: "",
deterministicFacts: [],
layer: "observed",
},
];

chart.setData(events);
expect(container.textContent).toContain("Citation churn");
expect(container.textContent).toContain("Added");
expect(container.textContent).toContain("Removed");
expect(container.querySelectorAll(".bar-group").length).toBe(2);
expect(container.querySelectorAll(".bar").length).toBe(6);
});
});

describe("SourceChangeTable", () => {
let container: HTMLElement;

beforeEach(() => {
container = document.createElement("div");
});

it("renders empty state when no source events exist", () => {
const table = new SourceChangeTable(container);
table.setData([]);

expect(container.textContent).toContain("No source/citation changes found");
});

it("renders table rows for citation modifications", () => {
const table = new SourceChangeTable(container);
const events: EvidenceEvent[] = [
{
eventId: "s1",
eventType: "citation_replaced",
fromRevisionId: 30,
toRevisionId: 31,
timestamp: "2026-04-10T10:00:00Z",
section: "Outcomes",
before: "doi:10.1001/old",
after: "doi:10.1001/new",
deterministicFacts: [
{
fact: "Updated DOI reference",
},
],
layer: "observed",
},
];

table.setData(events);
const rows = container.querySelectorAll("tbody tr");
expect(rows.length).toBe(1);
expect(rows[0].textContent).toContain("Outcomes");
expect(rows[0].textContent).toContain("Updated DOI reference");
});
});
143 changes: 143 additions & 0 deletions tests/disputes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { beforeEach, describe, expect, it } from "vitest";
import { EditVelocityChart } from "../src/disputes/EditVelocityChart.js";
import { RevertClusterDisplay } from "../src/disputes/RevertClusterDisplay.js";
import { TalkPageTimeline } from "../src/disputes/TalkPageTimeline.js";
import type { EvidenceEvent } from "../src/types.js";

describe("EditVelocityChart", () => {
let container: HTMLElement;

beforeEach(() => {
container = document.createElement("div");
});

it("renders empty state when no events exist", () => {
const chart = new EditVelocityChart(container);
chart.setData([]);

expect(container.textContent).toContain("No events to chart");
});

it("groups events into daily velocity buckets and renders bars", () => {
const chart = new EditVelocityChart(container);
const events: EvidenceEvent[] = [
{
eventId: "e1",
eventType: "claim_first_seen",
fromRevisionId: 1,
toRevisionId: 2,
timestamp: "2026-05-10T10:00:00Z",
section: "Lead",
before: "",
after: "First statement",
deterministicFacts: [],
layer: "observed",
},
{
eventId: "e2",
eventType: "revert_detected",
fromRevisionId: 2,
toRevisionId: 3,
timestamp: "2026-05-10T11:00:00Z",
section: "Lead",
before: "First statement",
after: "",
deterministicFacts: [],
layer: "observed",
},
];

chart.setData(events);
expect(container.querySelectorAll(".bar-group").length).toBe(1);
expect(container.querySelectorAll(".bar").length).toBe(2);
});
});

describe("RevertClusterDisplay", () => {
let container: HTMLElement;

beforeEach(() => {
container = document.createElement("div");
});

it("renders empty state when no revert events exist", () => {
const display = new RevertClusterDisplay(container);
display.setData([]);

expect(container.textContent).toContain("No revert events detected");
});

it("renders revert cluster cards with count badge", () => {
const display = new RevertClusterDisplay(container);
const events: EvidenceEvent[] = [
{
eventId: "rev-1",
eventType: "revert_detected",
fromRevisionId: 100,
toRevisionId: 101,
timestamp: "2026-07-01T15:00:00Z",
section: "Safety",
before: "Claim with caveat",
after: "Claim without caveat",
deterministicFacts: [
{
fact: "Reverted 1 edit by user A",
},
],
layer: "observed",
},
];

display.setData(events);
const cards = container.querySelectorAll(".card");
expect(cards.length).toBe(1);
expect(cards[0].textContent).toContain("r101");
expect(cards[0].textContent).toContain("Safety");
expect(cards[0].textContent).toContain("Reverted 1 edit by user A");
});
});

describe("TalkPageTimeline", () => {
let container: HTMLElement;

beforeEach(() => {
container = document.createElement("div");
});

it("renders empty state when no talk page events exist", () => {
const timeline = new TalkPageTimeline(container);
timeline.setData([]);

expect(container.textContent).toContain("No talk page events found");
});

it("renders timeline items for talk page discussions", () => {
const timeline = new TalkPageTimeline(container);
const events: EvidenceEvent[] = [
{
eventId: "talk-1",
eventType: "talk_page_correlated",
fromRevisionId: 5,
toRevisionId: 6,
timestamp: "2026-08-01T12:00:00Z",
section: "Talk:Article",
before: "",
after: "Discussion started regarding trial validity",
deterministicFacts: [
{
fact: "Thread opened: 'Trial Validity Analysis'",
},
],
layer: "observed",
},
];

timeline.setData(events);
const items = container.querySelectorAll(".timeline-item");
expect(items.length).toBe(1);
expect(items[0].textContent).toContain("talk page correlated");
expect(items[0].textContent).toContain(
"Thread opened: 'Trial Validity Analysis'",
);
});
});
Loading
Loading