From bf9ea3b75f06ab02139bab99dba06bdcced5e43f Mon Sep 17 00:00:00 2001 From: Kanav Jain Date: Thu, 14 May 2026 13:12:37 -0500 Subject: [PATCH 001/183] chore: initial scaffold for varia-docs --- AGENTS.md | 44 +++++++++++++++ README.md | 36 ++++++++++++ docs/boundary.md | 28 ++++++++++ docs/cli.md | 76 ++++++++++++++++++++++++++ docs/concepts.md | 50 +++++++++++++++++ docs/index.md | 27 +++++++++ docs/quickstart.md | 40 ++++++++++++++ docs/schema.md | 85 +++++++++++++++++++++++++++++ docs/sdk.md | 62 +++++++++++++++++++++ docs/tutorials/citation-churn.md | 34 ++++++++++++ docs/tutorials/dispute-timeline.md | 40 ++++++++++++++ docs/tutorials/fandom-canon.md | 41 ++++++++++++++ docs/tutorials/wikipedia-history.md | 50 +++++++++++++++++ 13 files changed, 613 insertions(+) create mode 100644 AGENTS.md create mode 100644 README.md create mode 100644 docs/boundary.md create mode 100644 docs/cli.md create mode 100644 docs/concepts.md create mode 100644 docs/index.md create mode 100644 docs/quickstart.md create mode 100644 docs/schema.md create mode 100644 docs/sdk.md create mode 100644 docs/tutorials/citation-churn.md create mode 100644 docs/tutorials/dispute-timeline.md create mode 100644 docs/tutorials/fandom-canon.md create mode 100644 docs/tutorials/wikipedia-history.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..30f3dba --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,44 @@ +# Agent instructions for varia-docs + +Varia-docs is the public documentation site for the Varia project — an open-source deterministic observation engine for public revision histories. + +## Repository structure + +``` +README.md # Landing page +AGENTS.md # This file +docs/ + index.md # Homepage + quickstart.md # Quick start guide + concepts.md # Three-Knowledge-Split architecture + schema.md # Event schema reference + cli.md # CLI command reference + sdk.md # SDK / package reference + boundary.md # What Varia is and is not + tutorials/ + wikipedia-history.md # Tutorial: Track Wikipedia changes + fandom-canon.md # Tutorial: Track Fandom canon changes + citation-churn.md # Tutorial: Monitor citation churn + dispute-timeline.md # Tutorial: Build a dispute timeline +``` + +## Documentation principles + +- **Tagline**: "Varia answers: 'What changed?'" +- **Boundary statement**: "Varia does not decide what is true. Varia makes visible how public knowledge changes." +- **Open source framing**: "What changed?" for what Varia does. "Not included: Domain-specific decision judgment." for what Varia excludes. +- **Tone**: technical, neutral, precise. No marketing language. +- **Tutorials**: markdown with TODO notes for incomplete sections. +- **CLI examples**: always use `wikihistory` as the command name. + +## Copy conventions + +- Use `Varia` (capital V) as the project name. +- Reference packages as `@var-ia/`. +- Reference the CLI as `wikihistory`. +- Use "Three-Knowledge-Split" (hyphenated, capitalized). +- L1/L2/L3 references should be deterministic/model/ground truth on first mention. + +## Build + +No build step for this repo — all files are plain markdown. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a5aa6c5 --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +# Varia + +**Varia answers: "What changed?"** + +An open-source, deterministic observation engine for public revision histories. Varia tracks what changes in collaborative knowledge spaces — Wikipedia, Fandom wikis, public registries — and makes those changes auditable, analyzable, and interpretable. + +--- + +## Quick start + +```bash +bun add @var-ia/cli +npx wikihistory analyze --page "Earth" +``` + +See [docs/quickstart.md](docs/quickstart.md) for a full walkthrough. + +## Packages + +| Package | Description | +|---|---| +| `@var-ia/evidence-graph` | Core types & event schemas | +| `@var-ia/ingestion` | Wikimedia API adapters | +| `@var-ia/analyzers` | Deterministic analyzers | +| `@var-ia/interpreter` | Model adapter interface | +| `@var-ia/cli` | CLI tool (`wikihistory`) | +| `@var-ia/persistence` | SQLite storage | +| `@var-ia/eval` | Evaluation harness | + +## Project status + +Varia is in active development by [NextConsensus](https://nextconsensus.com). The core L1 (deterministic) layer is usable. L2 (model-assisted) and L3 (ground truth) layers are in progress. + +## License + +MIT diff --git a/docs/boundary.md b/docs/boundary.md new file mode 100644 index 0000000..adb767c --- /dev/null +++ b/docs/boundary.md @@ -0,0 +1,28 @@ +# Boundary + +**Varia does not decide what is true. Varia makes visible how public knowledge changes.** + +## What Varia is + +- A deterministic observation engine +- An audit trail for revision histories +- A framework for extracting structured evidence from unstructured diffs +- A platform for evaluating model-assisted interpretations against ground truth + +## What Varia is not + +- A fact-checker or truth-verification system +- A moderation or content policing tool +- A predictive model for future edits +- A judgment engine for healthcare, finance, or legal decisions +- A replacement for domain expertise + +## Open source + +**What changed?** — Everything Varia produces is auditable, reproducible, and inspectable. + +**Not included** — Domain-specific decision judgment. Varia observes change. It does not tell you what to do about it. + +## The line + +Varia stops at the boundary between observability and judgment. If a tool needs to decide whether a change is *good*, *bad*, *true*, or *false*, that tool lives outside Varia. Varia answers only: "What changed?" diff --git a/docs/cli.md b/docs/cli.md new file mode 100644 index 0000000..1193376 --- /dev/null +++ b/docs/cli.md @@ -0,0 +1,76 @@ +# CLI command reference + +## `wikihistory analyze` + +Fetch revisions and run all analyzers. + +```bash +wikihistory analyze --page [options] +``` + +**Options:** +| Flag | Default | Description | +|---|---|---| +| `--page` | required | Page title | +| `--wiki` | `en.wikipedia.org` | Wiki domain | +| `--limit` | `10` | Number of revisions | +| `--output` | stdout | Output file path | +| `--format` | `json` | Output format (json, ndjson) | + +## `wikihistory claim` + +Extract claim-level events from analysis output. + +```bash +wikihistory claim --input --output +``` + +## `wikihistory export` + +Export event data to a portable format. + +```bash +wikihistory export --input --format --output +``` + +**Formats:** `json`, `csv`, `sqlite` + +## `wikihistory watch` + +Watch a page for new revisions in real time. + +```bash +wikihistory watch --page --interval +``` + +## `wikihistory cron` + +Schedule recurring analysis of a page set. + +```bash +wikihistory cron --config --db +``` + +## `wikihistory visualize` + +Render a timeline of events. + +```bash +wikihistory visualize --input --output +``` + +## `wikihistory diff` + +Compare two analysis snapshots. + +```bash +wikihistory diff --before --after +``` + +## `wikihistory eval` + +Run the evaluation harness against ground truth. + +```bash +wikihistory eval --input --ground-truth +``` diff --git a/docs/concepts.md b/docs/concepts.md new file mode 100644 index 0000000..760ca09 --- /dev/null +++ b/docs/concepts.md @@ -0,0 +1,50 @@ +# Concepts: Three-Knowledge-Split + +Varia separates knowledge into three layers that never mix. + +## L1 — Deterministic (observable facts) + +Byte-for-byte reproducible analysis of revision histories. No model is ever called at L1. Every output is a function of the input alone. + +**What L1 produces:** +- Section additions, removals, renames +- Citation additions and removals +- Revert events (exact, partial) +- Template changes + +**Invariant:** L1 never calls a model. + +## L2 — Model-Assisted (interpretation) + +Receives only L1 evidence (never raw wikitext). Produces structured interpretations with confidence scores. + +**What L2 produces:** +- Claim classifications +- Edit summaries +- Intent categorizations + +**Invariant:** L2 never sees raw Wikipedia text. + +## L3 — Independent Ground Truth + +Talk page consensus, RFC closures, ArbCom decisions, published corrections. These are sourced independently and never redefined by L1 or L2. + +**Invariant:** L3 is never redefined by L1 or L2. + +## Design principles + +1. **No single accuracy score** conflates layers. +2. **Every interpretation** carries a confidence score. +3. **Deterministic facts** are always presented before interpretations. +4. **Layers are independently testable.** + +## Event model + +All L1 output is structured as typed events: + +- `section_added`, `section_removed`, `section_renamed` +- `citation_added`, `citation_removed` +- `revert_detected` +- `template_added`, `template_removed`, `template_changed` + +See [schema.md](schema.md) for the full reference. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..631049d --- /dev/null +++ b/docs/index.md @@ -0,0 +1,27 @@ +# Varia: an open-source deterministic observation engine for public revision histories + +**Varia answers: "What changed?"** + +Varia is a domain-neutral observability layer for public knowledge. It ingests revision histories from Wikimedia, Fandom, and other public wikis, applies deterministic analysis to extract structured evidence, and surfaces what changed — without deciding what is true. + +## Why Varia? + +- **Deterministic by default.** L1 analysis is byte-for-byte reproducible. No model, no variance. +- **Three-Knowledge-Split.** Separate layers for observable facts, model-assisted interpretation, and independent ground truth. +- **Pluggable architecture.** Swap analyzers, storage backends, and model adapters without changing the core pipeline. + +## Contents + +- [Quick start](quickstart.md) +- [Concepts: Three-Knowledge-Split](concepts.md) +- [Event schema reference](schema.md) +- [CLI command reference](cli.md) +- [SDK / package reference](sdk.md) +- [Tutorials](tutorials/wikipedia-history.md) +- [Boundary](boundary.md) + +## Open source + +**What changed?** — Everything Varia produces is auditable, reproducible, and inspectable. + +**Not included** — Domain-specific decision judgment. Varia observes change. It does not tell you what to do about it. diff --git a/docs/quickstart.md b/docs/quickstart.md new file mode 100644 index 0000000..df1e55e --- /dev/null +++ b/docs/quickstart.md @@ -0,0 +1,40 @@ +# Quick start + +## Prerequisites + +- [Bun](https://bun.sh) v1.2+ runtime +- Internet connection (for fetching revision data from Wikimedia) + +## Install the CLI + +```bash +bun add -g @var-ia/cli +``` + +Or use directly with `npx`: + +```bash +npx wikihistory analyze --page "Earth" +``` + +## Your first analysis + +```bash +wikihistory analyze --page "Earth" --limit 10 +``` + +This fetches the 10 most recent revisions of the Wikipedia page "Earth" and runs all available analyzers (section changes, citation changes, revert detection, template changes). + +Output is printed to stdout as structured JSON. + +## Saving output + +```bash +wikihistory analyze --page "Earth" --limit 50 --output ./output/earth-events.json +``` + +## Next steps + +- [Concepts: Three-Knowledge-Split](concepts.md) +- [CLI reference](cli.md) +- [Tutorial: Track Wikipedia changes](tutorials/wikipedia-history.md) diff --git a/docs/schema.md b/docs/schema.md new file mode 100644 index 0000000..01523c2 --- /dev/null +++ b/docs/schema.md @@ -0,0 +1,85 @@ +# Event schema reference + +## Core type: `EvidenceEvent` + +All events share a common envelope: + +```typescript +interface EvidenceEvent { + id: string // deterministic content hash + type: EventType // discriminator + page: string // wiki page title + wiki: string // wiki identifier (e.g. "en.wikipedia.org") + timestamp: string // ISO 8601 + revisionId: number // source revision + previousRevisionId: number // parent revision + confidence: number // 1.0 for L1, 0-1 for L2 + metadata: Record // type-specific payload +} +``` + +## `EventType` + +```typescript +type EventType = + | "section_added" + | "section_removed" + | "section_renamed" + | "citation_added" + | "citation_removed" + | "revert_detected" + | "claim_added" + | "claim_removed" + | "claim_modified" + | "template_added" + | "template_removed" + | "template_changed" +``` + +## Claim events + +```typescript +interface ClaimAddedEvent extends EvidenceEvent { + type: "claim_added"; + metadata: { + claimText: string; + sourceCitation?: string; + sectionTitle: string; + }; +} +``` + +## Citation events + +```typescript +interface CitationAddedEvent extends EvidenceEvent { + type: "citation_added"; + metadata: { + citationKey: string; + citationText: string; + sectionTitle: string; + sourceType?: string; + }; +} +``` + +## Revert events + +```typescript +interface RevertDetectedEvent extends EvidenceEvent { + type: "revert_detected"; + metadata: { + revertedRevisionId: number; + revertType: "exact" | "partial"; + matchScore: number; + }; +} +``` + +## Deterministic identity + +Every `EvidenceEvent.id` is a SHA-256 content hash of its type + page + timestamp + metadata. This ensures: + +- Same input always produces the same event ID +- Duplicate detection is trivial +- Events can be compared across systems by ID alone diff --git a/docs/sdk.md b/docs/sdk.md new file mode 100644 index 0000000..b174385 --- /dev/null +++ b/docs/sdk.md @@ -0,0 +1,62 @@ +# SDK / package reference + +## `@var-ia/evidence-graph` + +Core types, event schemas, and utilities. Zero runtime dependencies. + +```typescript +import type { EvidenceEvent, EventType } from "@var-ia/evidence-graph"; +import { createClaimIdentity, hashEvent } from "@var-ia/evidence-graph"; +``` + +**Exports:** +- Interfaces: `EvidenceEvent`, `ClaimAddedEvent`, `CitationAddedEvent`, `RevertDetectedEvent` +- Types: `EventType` +- Utilities: `createClaimIdentity`, `hashEvent`, `compareEvents`, `mergeEventStreams` + +## `@var-ia/ingestion` + +Wikimedia API adapters. Fetches revision history and parses wikitext. + +```typescript +import { RevisionFetcher } from "@var-ia/ingestion"; +const fetcher = new RevisionFetcher({ wiki: "en.wikipedia.org" }); +``` + +Includes: `RevisionFetcher`, `WikitextParser` + +## `@var-ia/analyzers` + +Deterministic analyzers for section diffs, citation tracking, revert detection, and template analysis. + +```typescript +import { SectionDiffer, CitationTracker, RevertDetector, TemplateAnalyzer } from "@var-ia/analyzers"; +``` + +All L1 analyzers share a common interface — receive a pair of revisions and produce an array of `EvidenceEvent`. + +## `@var-ia/interpreter` + +Pluggable model adapter interface for L2. + +```typescript +import type { ModelAdapter } from "@var-ia/interpreter"; +import { createOpenAIAdapter } from "@var-ia/interpreter/openai"; +``` + +## `@var-ia/cli` + +The `wikihistory` CLI tool. Combines ingestion, analysis, and output. + +## `@var-ia/persistence` + +SQLite storage adapter (uses `bun:sqlite`). + +```typescript +import { EventStore } from "@var-ia/persistence"; +const store = new EventStore("varia.db"); +``` + +## `@var-ia/eval` + +Evaluation harness for measuring analyzer and model accuracy against L3 ground truth. diff --git a/docs/tutorials/citation-churn.md b/docs/tutorials/citation-churn.md new file mode 100644 index 0000000..87dbbee --- /dev/null +++ b/docs/tutorials/citation-churn.md @@ -0,0 +1,34 @@ +# Tutorial: Monitor citation churn on a medical/scientific page + +## Goal + +Track citation additions and removals on a high-traffic medical Wikipedia page to detect sourcing changes over time. + +## Steps + +### 1. Analyze a medical page + +```bash +wikihistory analyze --page "COVID-19" --limit 100 --output ./covid-citations.json +``` + +### 2. Filter citation events + +```bash +wikihistory claim --input ./covid-citations.json --type citation_added,citation_removed --output ./citations-only.json +``` + +### 3. Track citation churn + +TODO: Use `wikihistory export` to produce a churn report showing which citations appear, disappear, and reappear across revisions. + +## Use case: identifying biased sourcing + +TODO: Describe how citation churn patterns can reveal coordinated editing or shifts in cited evidence. + +## TODO + +- Add churn analysis script or workflow +- Add example of a citation being added then removed in a subsequent edit +- Add section on grouping citations by source type (journal, news, government) +- Add note about the L1/L2 boundary — Varia tracks the citation change, not the quality of the source diff --git a/docs/tutorials/dispute-timeline.md b/docs/tutorials/dispute-timeline.md new file mode 100644 index 0000000..083e1c4 --- /dev/null +++ b/docs/tutorials/dispute-timeline.md @@ -0,0 +1,40 @@ +# Tutorial: Build a dispute timeline from talk page activity + +## Goal + +Combine article revision history with talk page activity to build a timeline of a dispute. + +## Steps + +### 1. Analyze the article + +```bash +wikihistory analyze --page "Global_warming" --limit 100 --output ./gw-revisions.json +``` + +### 2. Get revert events + +```bash +wikihistory claim --input ./gw-revisions.json --type revert_detected --output ./gw-reverts.json +``` + +### 3. Cross-reference with talk page + +TODO: Fetch talk page revision history and correlate revert clusters with discussion periods. + +### 4. Build the timeline + +```bash +wikihistory visualize --input ./gw-reverts.json --output ./gw-dispute-timeline.html +``` + +## Use case: editorial dispute analysis + +TODO: Describe how revert frequency, participant diversity, and talk page activity together indicate dispute intensity. + +## TODO + +- Add talk page ingestion workflow +- Add correlation analysis between reverts and talk page threads +- Add example output showing a dispute timeline with annotated revert clusters +- Add section on privacy considerations (usernames in output) diff --git a/docs/tutorials/fandom-canon.md b/docs/tutorials/fandom-canon.md new file mode 100644 index 0000000..33caaac --- /dev/null +++ b/docs/tutorials/fandom-canon.md @@ -0,0 +1,41 @@ +# Tutorial: Track canon changes on a Fandom wiki + +## Goal + +Monitor how a Fandom wiki's canon pages change over time, tracking additions and removals of lore-critical statements. + +## Steps + +### 1. Configure a Fandom wiki + +```bash +wikihistory analyze --page "Darth_Vader" --wiki "starwars.fandom.com" --limit 20 +``` + +### 2. Run analyzers + +```bash +wikihistory analyze --page "Darth_Vader" --wiki "starwars.fandom.com" --limit 50 --output ./canon-events.json +``` + +### 3. Examine claim changes + +```bash +wikihistory claim --input ./canon-events.json --output ./canon-claims.json +``` + +### 4. Watch for new edits + +```bash +wikihistory watch --page "Darth_Vader" --wiki "starwars.fandom.com" --interval 300 +``` + +## Use case: preserving canonicity + +TODO: Describe how to monitor canon disputes over time, compare claim stability across different Fandom communities. + +## TODO + +- Add example output for a canon-relevant event +- Add note about Fandom API differences from Wikimedia +- Add section on multi-page monitoring (tracking a topic across pages) diff --git a/docs/tutorials/wikipedia-history.md b/docs/tutorials/wikipedia-history.md new file mode 100644 index 0000000..6329a1c --- /dev/null +++ b/docs/tutorials/wikipedia-history.md @@ -0,0 +1,50 @@ +# Tutorial: Track changes on a Wikipedia page + +## Goal + +Use Varia to analyze the revision history of a Wikipedia page and see what changed over the last 50 edits. + +## Steps + +### 1. Install the CLI + +```bash +bun add -g @var-ia/cli +``` + +### 2. Analyze + +```bash +wikihistory analyze --page "Earth" --limit 50 --output ./earth-events.json +``` + +### 3. View results + +```bash +cat ./earth-events.json | bunx json +``` + +### 4. Understand the events + +- **Section events** — sections were added, removed, or renamed +- **Citation events** — citations were added or removed +- **Revert events** — someone reverted an edit + +### 5. Visualize + +```bash +wikihistory visualize --input ./earth-events.json --output ./earth-timeline.html +``` + +## Next steps + +- [Track canon changes on Fandom](fandom-canon.md) +- [Monitor citation churn on medical pages](citation-churn.md) +- [Build a dispute timeline](dispute-timeline.md) + +## TODO + +- Add expected output examples for each step +- Add section on filtering events by type +- Add section on configuring wiki source (other language Wikipedias) +- Add troubleshooting tips for rate limits From 04a9e61b888a8f34fc95112f1f20f64a2e59da86 Mon Sep 17 00:00:00 2001 From: Kanav Jain Date: Thu, 14 May 2026 13:14:38 -0500 Subject: [PATCH 002/183] chore: initial scaffold for varia-ui --- .gitignore | 4 + AGENTS.md | 20 + README.md | 20 + bun.lock | 142 +++++++ index.html | 29 ++ package.json | 15 + public/sample-data.jsonl | 12 + src/citations/CitationChurnChart.ts | 129 +++++++ src/citations/SourceChangeTable.ts | 93 +++++ src/disputes/EditVelocityChart.ts | 129 +++++++ src/disputes/RevertClusterDisplay.ts | 116 ++++++ src/disputes/TalkPageTimeline.ts | 90 +++++ src/export/ImportUpload.ts | 148 ++++++++ src/export/SchemaViewer.ts | 97 +++++ src/export/TimelineExport.ts | 96 +++++ src/language/CertaintyTimeline.ts | 134 +++++++ src/language/WordingDiffCard.ts | 106 ++++++ src/main.ts | 79 ++++ src/styles/main.css | 533 +++++++++++++++++++++++++++ src/timeline/DiffView.ts | 126 +++++++ src/timeline/EventFilter.ts | 88 +++++ src/timeline/TimelineView.ts | 111 ++++++ src/types.ts | 63 ++++ tsconfig.json | 19 + vite.config.ts | 8 + 25 files changed, 2407 insertions(+) create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 README.md create mode 100644 bun.lock create mode 100644 index.html create mode 100644 package.json create mode 100644 public/sample-data.jsonl create mode 100644 src/citations/CitationChurnChart.ts create mode 100644 src/citations/SourceChangeTable.ts create mode 100644 src/disputes/EditVelocityChart.ts create mode 100644 src/disputes/RevertClusterDisplay.ts create mode 100644 src/disputes/TalkPageTimeline.ts create mode 100644 src/export/ImportUpload.ts create mode 100644 src/export/SchemaViewer.ts create mode 100644 src/export/TimelineExport.ts create mode 100644 src/language/CertaintyTimeline.ts create mode 100644 src/language/WordingDiffCard.ts create mode 100644 src/main.ts create mode 100644 src/styles/main.css create mode 100644 src/timeline/DiffView.ts create mode 100644 src/timeline/EventFilter.ts create mode 100644 src/timeline/TimelineView.ts create mode 100644 src/types.ts create mode 100644 tsconfig.json create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d70bb9c --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules +dist +.DS_Store +*.local diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..5d142fa --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,20 @@ +# varia-ui — Agent Instructions + +Standalone visualization UI for Varia. Not part of the monorepo — no `@var-ia/*` package imports. + +## Commands + +```bash +bun run dev # vite dev server +bun run build # tsc + vite build +bun run preview # vite preview +``` + +## Conventions + +- Each component is a class with a `render()` method. +- Components take a container `HTMLElement` and data, manipulate DOM directly. +- No framework. No JSX. No virtual DOM. +- Types mirror `@var-ia/evidence-graph` schemas (defined locally in `src/types.ts`). +- CSS custom properties in `main.css` — no CSS framework. +- Sample data in `public/sample-data.jsonl` — valid `EvidenceEvent` objects. diff --git a/README.md b/README.md new file mode 100644 index 0000000..639f797 --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +# varia-ui + +Generic visualization interface for the [Varia](https://github.com/anomalyco/varia) observation engine. + +Standalone web app built with vanilla TypeScript and Vite. Loads JSONL event data (compatible with `@var-ia/evidence-graph`) and renders timeline, diff, citation, and certainty visualizations. + +## Usage + +```bash +bun install +bun run dev +``` + +Open `http://localhost:5173`. Sample data loads automatically. Drag a JSONL file onto the upload zone to inspect your own data. + +## Stack + +- Vite + vanilla TypeScript +- No framework dependencies +- DOM-based rendering with CSS custom properties diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..f345c10 --- /dev/null +++ b/bun.lock @@ -0,0 +1,142 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "varia-ui", + "devDependencies": { + "typescript": "^5.7.0", + "vite": "^6.0.0", + }, + }, + }, + "packages": { + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.4", "", { "os": "android", "cpu": "arm" }, "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.4", "", { "os": "android", "cpu": "arm64" }, "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.60.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.60.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.60.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.60.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.60.4", "", { "os": "linux", "cpu": "arm" }, "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.60.4", "", { "os": "linux", "cpu": "arm" }, "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.60.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.60.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.60.4", "", { "os": "linux", "cpu": "none" }, "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.60.4", "", { "os": "linux", "cpu": "none" }, "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.60.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.60.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.60.4", "", { "os": "linux", "cpu": "none" }, "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.60.4", "", { "os": "linux", "cpu": "none" }, "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.60.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.60.4", "", { "os": "linux", "cpu": "x64" }, "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.60.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.60.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.60.4", "", { "os": "none", "cpu": "arm64" }, "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.60.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.60.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.60.4", "", { "os": "win32", "cpu": "x64" }, "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.4", "", { "os": "win32", "cpu": "x64" }, "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + + "postcss": ["postcss@8.5.14", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg=="], + + "rollup": ["rollup@4.60.4", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.4", "@rollup/rollup-android-arm64": "4.60.4", "@rollup/rollup-darwin-arm64": "4.60.4", "@rollup/rollup-darwin-x64": "4.60.4", "@rollup/rollup-freebsd-arm64": "4.60.4", "@rollup/rollup-freebsd-x64": "4.60.4", "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", "@rollup/rollup-linux-arm-musleabihf": "4.60.4", "@rollup/rollup-linux-arm64-gnu": "4.60.4", "@rollup/rollup-linux-arm64-musl": "4.60.4", "@rollup/rollup-linux-loong64-gnu": "4.60.4", "@rollup/rollup-linux-loong64-musl": "4.60.4", "@rollup/rollup-linux-ppc64-gnu": "4.60.4", "@rollup/rollup-linux-ppc64-musl": "4.60.4", "@rollup/rollup-linux-riscv64-gnu": "4.60.4", "@rollup/rollup-linux-riscv64-musl": "4.60.4", "@rollup/rollup-linux-s390x-gnu": "4.60.4", "@rollup/rollup-linux-x64-gnu": "4.60.4", "@rollup/rollup-linux-x64-musl": "4.60.4", "@rollup/rollup-openbsd-x64": "4.60.4", "@rollup/rollup-openharmony-arm64": "4.60.4", "@rollup/rollup-win32-arm64-msvc": "4.60.4", "@rollup/rollup-win32-ia32-msvc": "4.60.4", "@rollup/rollup-win32-x64-gnu": "4.60.4", "@rollup/rollup-win32-x64-msvc": "4.60.4", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "vite": ["vite@6.4.2", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ=="], + } +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..0338557 --- /dev/null +++ b/index.html @@ -0,0 +1,29 @@ + + + + + + Varia — Observation UI + + + +
+
+

Varia

+

Observation Engine — Timeline Viewer

+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..1b29965 --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "varia-ui", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "devDependencies": { + "typescript": "^5.7.0", + "vite": "^6.0.0" + } +} diff --git a/public/sample-data.jsonl b/public/sample-data.jsonl new file mode 100644 index 0000000..8955010 --- /dev/null +++ b/public/sample-data.jsonl @@ -0,0 +1,12 @@ +{"eventId":"evt-001","eventType":"claim_first_seen","claimId":"clm-001","fromRevisionId":1200001,"toRevisionId":1200002,"section":"Lead","before":"","after":"The Earth is the third planet from the Sun.","deterministicFacts":[{"fact":"Claim first appeared in the lead section","detail":"New claim introduced: 'The Earth is the third planet from the Sun.'"}],"modelInterpretation":{"semanticChange":"New factual claim introduced in lead","confidence":0.95,"policyDimension":"verifiability"},"layer":"observed","timestamp":"2024-01-15T10:30:00Z"} +{"eventId":"evt-002","eventType":"citation_added","claimId":"clm-001","fromRevisionId":1200002,"toRevisionId":1200003,"section":"Lead","before":"The Earth is the third planet from the Sun.","after":"The Earth is the third planet from the Sun.[1]","deterministicFacts":[{"fact":"Citation [1] added to support claim","detail":"Source: NASA Solar System Fact Sheet"}],"layer":"observed","timestamp":"2024-01-15T11:00:00Z"} +{"eventId":"evt-003","eventType":"claim_strengthened","claimId":"clm-001","fromRevisionId":1200003,"toRevisionId":1200005,"section":"Lead","before":"The Earth is the third planet from the Sun.","after":"The Earth is the third planet from the Sun and the only known planet to harbor life.","deterministicFacts":[{"fact":"Claim strengthened with additional qualifier","detail":"Added 'and the only known planet to harbor life'"}],"modelInterpretation":{"semanticChange":"Claim scope expanded to include life-hosting property","confidence":0.88,"policyDimension":"npov"},"layer":"model_interpretation","timestamp":"2024-01-16T09:15:00Z"} +{"eventId":"evt-004","eventType":"citation_replaced","claimId":"clm-001","fromRevisionId":1200005,"toRevisionId":1200007,"section":"Lead","before":"The Earth is the third planet from the Sun and the only known planet to harbor life.[1]","after":"The Earth is the third planet from the Sun and the only known planet to harbor life.[2]","deterministicFacts":[{"fact":"Citation [1] replaced with [2]","detail":"Old: NASA Fact Sheet → New: IPCC Report 2023"}],"layer":"observed","timestamp":"2024-01-17T14:00:00Z"} +{"eventId":"evt-005","eventType":"revert_detected","claimId":"clm-002","fromRevisionId":1200007,"toRevisionId":1200008,"section":"Climate","before":"Global temperatures have risen 1.5°C above pre-industrial levels.","after":"Global temperatures have risen 0.8°C above pre-industrial levels.","deterministicFacts":[{"fact":"Revision r1200007 reverted to earlier content","detail":"Temperature claim rolled back from 1.5°C to 0.8°C"}],"modelInterpretation":{"semanticChange":"Contested climate statistic — possible NPOV dispute","confidence":0.72,"policyDimension":"npov","discussionType":"sourcing_dispute"},"layer":"model_interpretation","timestamp":"2024-01-18T08:30:00Z"} +{"eventId":"evt-006","eventType":"claim_softened","claimId":"clm-002","fromRevisionId":1200008,"toRevisionId":1200009,"section":"Climate","before":"Global temperatures have risen 0.8°C above pre-industrial levels.","after":"Global temperatures have risen approximately 0.8°C above pre-industrial levels.","deterministicFacts":[{"fact":"Claim softened with hedging language","detail":"Added 'approximately' to temperature claim"}],"modelInterpretation":{"semanticChange":"Certainty reduced via hedging","confidence":0.65,"policyDimension":"due_weight"},"layer":"model_interpretation","timestamp":"2024-01-18T12:00:00Z"} +{"eventId":"evt-007","eventType":"talk_reply_added","fromRevisionId":1200009,"toRevisionId":1200010,"section":"Talk:Climate","before":"","after":"This temperature figure needs a better source. The IPCC report says 1.1°C. —User123","deterministicFacts":[{"fact":"Reply added to talk page discussing temperature sourcing","detail":"User challenged the accuracy of the temperature claim"}],"modelInterpretation":{"semanticChange":"Talk page discussion about sourcing dispute","confidence":0.85,"policyDimension":"verifiability","discussionType":"sourcing_dispute"},"layer":"model_interpretation","timestamp":"2024-01-18T15:00:00Z"} +{"eventId":"evt-008","eventType":"claim_reworded","claimId":"clm-003","fromRevisionId":1200010,"toRevisionId":1200012,"section":"History","before":"The war began in 1939 when Germany invaded Poland.","after":"The war began in 1939 following the German invasion of Poland.","deterministicFacts":[{"fact":"Claim reworded for neutrality","detail":"Active voice changed to passive construction"}],"modelInterpretation":{"semanticChange":"Wording adjusted for neutral tone — no factual change","confidence":0.91,"policyDimension":"npov"},"layer":"model_interpretation","timestamp":"2024-01-19T10:00:00Z"} +{"eventId":"evt-009","eventType":"citation_removed","claimId":"clm-003","fromRevisionId":1200012,"toRevisionId":1200013,"section":"History","before":"The war began in 1939 following the German invasion of Poland.[3]","after":"The war began in 1939 following the German invasion of Poland.","deterministicFacts":[{"fact":"Citation [3] removed from claim","detail":"Source was a deprecated blog"}],"layer":"observed","timestamp":"2024-01-19T11:30:00Z"} +{"eventId":"evt-010","eventType":"claim_removed","claimId":"clm-004","fromRevisionId":1200013,"toRevisionId":1200014,"section":"Economy","before":"The country's GDP grew 12% in 2023 according to unofficial estimates.","after":"","deterministicFacts":[{"fact":"Claim removed from section","detail":"Removed unsourced GDP growth figure"}],"modelInterpretation":{"semanticChange":"Unverified economic statistic removed","confidence":0.78,"policyDimension":"verifiability"},"layer":"model_interpretation","timestamp":"2024-01-20T09:00:00Z"} +{"eventId":"evt-011","eventType":"citation_added","claimId":"clm-001","fromRevisionId":1200014,"toRevisionId":1200015,"section":"Lead","before":"The Earth is the third planet from the Sun and the only known planet to harbor life.[2]","after":"The Earth is the third planet from the Sun and the only known planet to harbor life.[2][3]","deterministicFacts":[{"fact":"Additional citation [3] added to support life-harboring claim","detail":"Source: Encyclopedia of Astrobiology"}],"layer":"observed","timestamp":"2024-01-21T16:00:00Z"} +{"eventId":"evt-012","eventType":"talk_thread_opened","fromRevisionId":1200015,"toRevisionId":1200016,"section":"Talk:Earth","before":"","after":"== Citation quality ==\nThe sources in the lead need review. [3] is from a predatory journal. —EditorXYZ","deterministicFacts":[{"fact":"New talk thread about citation quality","detail":"Discussion about potential unreliable source in lead section"}],"modelInterpretation":{"semanticChange":"Editor flagged citation quality concern","confidence":0.82,"policyDimension":"verifiability","discussionType":"sourcing_dispute"},"layer":"model_interpretation","timestamp":"2024-01-22T08:00:00Z"} diff --git a/src/citations/CitationChurnChart.ts b/src/citations/CitationChurnChart.ts new file mode 100644 index 0000000..48ce5ee --- /dev/null +++ b/src/citations/CitationChurnChart.ts @@ -0,0 +1,129 @@ +import type { EvidenceEvent } from "../types.js"; + +interface ChurnData { + revId: number; + added: number; + removed: number; + replaced: number; + label: string; +} + +export class CitationChurnChart { + private container: HTMLElement; + private events: EvidenceEvent[] = []; + + constructor(container: HTMLElement) { + this.container = container; + } + + setData(events: EvidenceEvent[]): void { + this.events = events; + this.render(); + } + + render(): void { + this.container.innerHTML = ""; + + const title = document.createElement("div"); + title.className = "panel-title"; + title.textContent = "Citation Churn"; + this.container.appendChild(title); + + const churnData = this.computeChurn(); + + if (churnData.length === 0) { + const empty = document.createElement("div"); + empty.style.cssText = "color:var(--text-dim);font-size:0.85rem;padding:1rem 0;"; + empty.textContent = "No citation events found."; + this.container.appendChild(empty); + return; + } + + const maxVal = Math.max( + ...churnData.map((d) => Math.max(d.added, d.removed, d.replaced)), + 1 + ); + + const chart = document.createElement("div"); + chart.className = "bar-chart"; + + for (const point of churnData) { + const group = document.createElement("div"); + group.className = "bar-group"; + + const addedBar = createBar(point.added, maxVal, "added"); + const removedBar = createBar(point.removed, maxVal, "removed"); + const replacedBar = createBar(point.replaced, maxVal, "replaced", true); + + group.appendChild(addedBar); + group.appendChild(removedBar); + group.appendChild(replacedBar); + + chart.appendChild(group); + } + + this.container.appendChild(chart); + + const legend = document.createElement("div"); + legend.style.cssText = "display:flex;gap:1rem;font-size:0.75rem;color:var(--text-dim);margin-top:0.25rem;"; + + const addLegend = createLegendItem("Added", "var(--green)"); + const remLegend = createLegendItem("Removed", "var(--red)"); + const repLegend = createLegendItem("Replaced", "var(--orange)"); + legend.append(addLegend, remLegend, repLegend); + this.container.appendChild(legend); + } + + private computeChurn(): ChurnData[] { + const citationEvents = this.events.filter( + (e) => + e.eventType === "citation_added" || + e.eventType === "citation_removed" || + e.eventType === "citation_replaced" + ); + + const grouped = new Map(); + + for (const event of citationEvents) { + let entry = grouped.get(event.toRevisionId); + if (!entry) { + entry = { + revId: event.toRevisionId, + added: 0, + removed: 0, + replaced: 0, + label: `r${event.toRevisionId}`, + }; + grouped.set(event.toRevisionId, entry); + } + + if (event.eventType === "citation_added") entry.added++; + else if (event.eventType === "citation_removed") entry.removed++; + else if (event.eventType === "citation_replaced") entry.replaced++; + } + + return Array.from(grouped.values()).sort((a, b) => a.revId - b.revId); + } +} + +function createBar(value: number, max: number, cls: string, isReplaced = false): HTMLElement { + const bar = document.createElement("div"); + bar.className = `bar ${cls}`; + const pct = max > 0 ? (value / max) * 100 : 0; + bar.style.height = `${Math.max(pct, value > 0 ? 3 : 0)}%`; + if (isReplaced) { + bar.style.background = "var(--orange)"; + } + bar.title = `${cls}: ${value}`; + return bar; +} + +function createLegendItem(label: string, color: string): HTMLElement { + const item = document.createElement("span"); + item.style.cssText = "display:flex;align-items:center;gap:0.3rem;"; + const dot = document.createElement("span"); + dot.style.cssText = `display:inline-block;width:8px;height:8px;border-radius:2px;background:${color};`; + item.appendChild(dot); + item.appendChild(document.createTextNode(label)); + return item; +} diff --git a/src/citations/SourceChangeTable.ts b/src/citations/SourceChangeTable.ts new file mode 100644 index 0000000..cb8be41 --- /dev/null +++ b/src/citations/SourceChangeTable.ts @@ -0,0 +1,93 @@ +import type { EvidenceEvent } from "../types.js"; + +interface SourceChange { + revisionId: number; + timestamp: string; + section: string; + changeType: string; + detail: string; +} + +export class SourceChangeTable { + private container: HTMLElement; + private events: EvidenceEvent[] = []; + + constructor(container: HTMLElement) { + this.container = container; + } + + setData(events: EvidenceEvent[]): void { + this.events = events; + this.render(); + } + + render(): void { + this.container.innerHTML = ""; + + const title = document.createElement("div"); + title.className = "panel-title"; + title.textContent = "Source Changes"; + this.container.appendChild(title); + + const changes = this.computeChanges(); + + if (changes.length === 0) { + const empty = document.createElement("div"); + empty.style.cssText = "color:var(--text-dim);font-size:0.85rem;padding:1rem 0;"; + empty.textContent = "No source/citation changes found."; + this.container.appendChild(empty); + return; + } + + const table = document.createElement("table"); + table.className = "data-table"; + + const thead = document.createElement("thead"); + thead.innerHTML = ` + + Rev + Date + Type + Section + Detail + + `; + table.appendChild(thead); + + const tbody = document.createElement("tbody"); + for (const ch of changes) { + const tr = document.createElement("tr"); + const ts = new Date(ch.timestamp); + tr.innerHTML = ` + r${ch.revisionId} + ${ts.toLocaleDateString()} + ${ch.changeType} + ${ch.section} + ${ch.detail} + `; + tbody.appendChild(tr); + } + table.appendChild(tbody); + this.container.appendChild(table); + } + + private computeChanges(): SourceChange[] { + const citationEvents = this.events.filter( + (e) => + e.eventType === "citation_added" || + e.eventType === "citation_removed" || + e.eventType === "citation_replaced" + ); + + return citationEvents.map((e) => ({ + revisionId: e.toRevisionId, + timestamp: e.timestamp, + section: e.section, + changeType: e.eventType.replace("citation_", ""), + detail: + e.deterministicFacts.length > 0 + ? e.deterministicFacts[0].fact + : e.eventType, + })); + } +} diff --git a/src/disputes/EditVelocityChart.ts b/src/disputes/EditVelocityChart.ts new file mode 100644 index 0000000..ea1bd95 --- /dev/null +++ b/src/disputes/EditVelocityChart.ts @@ -0,0 +1,129 @@ +import type { EvidenceEvent } from "../types.js"; + +interface VelocityBucket { + date: string; + count: number; + reverts: number; + label: string; +} + +export class EditVelocityChart { + private container: HTMLElement; + private events: EvidenceEvent[] = []; + + constructor(container: HTMLElement) { + this.container = container; + } + + setData(events: EvidenceEvent[]): void { + this.events = events; + this.render(); + } + + render(): void { + this.container.innerHTML = ""; + + const title = document.createElement("div"); + title.className = "panel-title"; + title.textContent = "Edit Velocity"; + this.container.appendChild(title); + + const buckets = this.computeBuckets(); + + if (buckets.length === 0) { + const empty = document.createElement("div"); + empty.style.cssText = "color:var(--text-dim);font-size:0.85rem;padding:1rem 0;"; + empty.textContent = "No events to chart."; + this.container.appendChild(empty); + return; + } + + const maxCount = Math.max(...buckets.map((b) => Math.max(b.count, b.reverts)), 1); + + const chart = document.createElement("div"); + chart.className = "bar-chart"; + + for (const bucket of buckets) { + const group = document.createElement("div"); + group.className = "bar-group"; + + const countBar = document.createElement("div"); + countBar.className = "bar added"; + countBar.style.height = `${(bucket.count / maxCount) * 100}%`; + countBar.title = `Events: ${bucket.count}`; + group.appendChild(countBar); + + const revertBar = document.createElement("div"); + revertBar.className = "bar removed"; + revertBar.style.height = `${(bucket.reverts / maxCount) * 100}%`; + revertBar.title = `Reverts: ${bucket.reverts}`; + group.appendChild(revertBar); + + chart.appendChild(group); + } + + this.container.appendChild(chart); + + const xLabels = document.createElement("div"); + xLabels.style.cssText = "display:flex;justify-content:space-between;font-size:0.65rem;color:var(--text-dim);margin-top:0.25rem;"; + + const step = Math.max(1, Math.floor(buckets.length / 6)); + for (let i = 0; i < buckets.length; i += step) { + const label = document.createElement("span"); + label.textContent = buckets[i].label; + xLabels.appendChild(label); + } + if (buckets.length > 0) { + const lastLabel = document.createElement("span"); + lastLabel.textContent = buckets[buckets.length - 1].label; + xLabels.appendChild(lastLabel); + } + + this.container.appendChild(xLabels); + + const legend = document.createElement("div"); + legend.style.cssText = "display:flex;gap:1rem;font-size:0.75rem;color:var(--text-dim);margin-top:0.25rem;"; + const allLegend = createBarLegend("All events", "var(--green)"); + const revertLegend = createBarLegend("Reverts", "var(--red)"); + legend.append(allLegend, revertLegend); + this.container.appendChild(legend); + } + + private computeBuckets(): VelocityBucket[] { + if (this.events.length === 0) return []; + + const sorted = [...this.events].sort( + (a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime() + ); + + const buckets = new Map(); + + for (const event of sorted) { + const d = new Date(event.timestamp); + const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; + + let bucket = buckets.get(key); + if (!bucket) { + bucket = { date: key, count: 0, reverts: 0, label: `${d.getMonth() + 1}/${d.getDate()}` }; + buckets.set(key, bucket); + } + + bucket.count++; + if (event.eventType === "revert_detected") { + bucket.reverts++; + } + } + + return Array.from(buckets.values()).sort((a, b) => a.date.localeCompare(b.date)); + } +} + +function createBarLegend(label: string, color: string): HTMLElement { + const item = document.createElement("span"); + item.style.cssText = "display:flex;align-items:center;gap:0.3rem;"; + const dot = document.createElement("span"); + dot.style.cssText = `display:inline-block;width:8px;height:8px;border-radius:2px;background:${color};`; + item.appendChild(dot); + item.appendChild(document.createTextNode(label)); + return item; +} diff --git a/src/disputes/RevertClusterDisplay.ts b/src/disputes/RevertClusterDisplay.ts new file mode 100644 index 0000000..b87a187 --- /dev/null +++ b/src/disputes/RevertClusterDisplay.ts @@ -0,0 +1,116 @@ +import type { EvidenceEvent } from "../types.js"; + +interface RevertCluster { + revisionId: number; + timestamp: string; + section: string; + facts: string[]; + count: number; +} + +export class RevertClusterDisplay { + private container: HTMLElement; + private events: EvidenceEvent[] = []; + + constructor(container: HTMLElement) { + this.container = container; + } + + setData(events: EvidenceEvent[]): void { + this.events = events; + this.render(); + } + + render(): void { + this.container.innerHTML = ""; + + const title = document.createElement("div"); + title.className = "panel-title"; + title.textContent = "Revert Clusters"; + this.container.appendChild(title); + + const clusters = this.computeClusters(); + + if (clusters.length === 0) { + const empty = document.createElement("div"); + empty.style.cssText = "color:var(--text-dim);font-size:0.85rem;padding:1rem 0;"; + empty.textContent = "No revert events detected."; + this.container.appendChild(empty); + return; + } + + for (const cluster of clusters) { + const card = document.createElement("div"); + card.className = "card"; + card.style.marginBottom = "0.5rem"; + + const header = document.createElement("div"); + header.style.cssText = "display:flex;justify-content:space-between;align-items:center;margin-bottom:0.3rem;"; + + const label = document.createElement("div"); + label.className = "card-label"; + const ts = new Date(cluster.timestamp); + label.textContent = `r${cluster.revisionId} · ${ts.toLocaleDateString()}`; + header.appendChild(label); + + const badge = document.createElement("span"); + badge.style.cssText = "background:var(--red);color:#fff;font-size:0.7rem;padding:0.1rem 0.4rem;border-radius:4px;font-weight:600;"; + badge.textContent = `${cluster.count} revert${cluster.count > 1 ? "s" : ""}`; + header.appendChild(badge); + + card.appendChild(header); + + if (cluster.section) { + const section = document.createElement("div"); + section.style.cssText = "font-size:0.78rem;color:var(--text-dim);margin-bottom:0.3rem;font-family:var(--font-mono);"; + section.textContent = cluster.section; + card.appendChild(section); + } + + for (const fact of cluster.facts) { + const factEl = document.createElement("div"); + factEl.style.cssText = "font-size:0.8rem;padding:0.2rem 0;border-left:2px solid var(--red);padding-left:0.5rem;margin-top:0.2rem;"; + factEl.textContent = fact; + card.appendChild(factEl); + } + + this.container.appendChild(card); + } + } + + private computeClusters(): RevertCluster[] { + const revertEvents = this.events.filter((e) => e.eventType === "revert_detected"); + + const grouped = new Map(); + + for (const event of revertEvents) { + const key = `${event.toRevisionId}-${event.section}`; + let cluster = grouped.get(key); + if (!cluster) { + cluster = { + revisionId: event.toRevisionId, + timestamp: event.timestamp, + section: event.section, + facts: [], + count: 0, + }; + grouped.set(key, cluster); + } + + cluster.count++; + for (const fact of event.deterministicFacts) { + if (!cluster.facts.includes(fact.fact)) { + cluster.facts.push(fact.fact); + } + } + + if (event.timestamp > cluster.timestamp) { + cluster.timestamp = event.timestamp; + } + } + + return Array.from(grouped.values()).sort( + (a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime() + ); + } +} diff --git a/src/disputes/TalkPageTimeline.ts b/src/disputes/TalkPageTimeline.ts new file mode 100644 index 0000000..d2dd84c --- /dev/null +++ b/src/disputes/TalkPageTimeline.ts @@ -0,0 +1,90 @@ +import type { EvidenceEvent } from "../types.js"; + +interface TalkActivity { + revisionId: number; + timestamp: string; + eventType: string; + summary: string; +} + +export class TalkPageTimeline { + private container: HTMLElement; + private events: EvidenceEvent[] = []; + + constructor(container: HTMLElement) { + this.container = container; + } + + setData(events: EvidenceEvent[]): void { + this.events = events; + this.render(); + } + + render(): void { + this.container.innerHTML = ""; + + const title = document.createElement("div"); + title.className = "panel-title"; + title.textContent = "Talk Page Activity"; + this.container.appendChild(title); + + const activities = this.computeActivities(); + + if (activities.length === 0) { + const empty = document.createElement("div"); + empty.style.cssText = "color:var(--text-dim);font-size:0.85rem;padding:1rem 0;"; + empty.textContent = "No talk page events found."; + this.container.appendChild(empty); + return; + } + + const timeline = document.createElement("div"); + timeline.className = "timeline"; + + for (const act of activities) { + const item = document.createElement("div"); + item.className = "timeline-item"; + + const typeLabel = document.createElement("div"); + typeLabel.className = "event-type"; + typeLabel.textContent = act.eventType.replace(/_/g, " "); + item.appendChild(typeLabel); + + const summary = document.createElement("div"); + summary.className = "event-summary"; + summary.textContent = act.summary; + item.appendChild(summary); + + const meta = document.createElement("div"); + meta.className = "event-meta"; + const ts = new Date(act.timestamp); + meta.textContent = `${ts.toLocaleDateString()} ${ts.toLocaleTimeString()} · r${act.revisionId}`; + item.appendChild(meta); + + timeline.appendChild(item); + } + + this.container.appendChild(timeline); + } + + private computeActivities(): TalkActivity[] { + const talkTypes = new Set([ + "talk_page_correlated", + "talk_thread_opened", + "talk_thread_archived", + "talk_reply_added", + ]); + + return this.events + .filter((e) => talkTypes.has(e.eventType)) + .map((e) => ({ + revisionId: e.toRevisionId, + timestamp: e.timestamp, + eventType: e.eventType, + summary: + e.deterministicFacts.length > 0 + ? e.deterministicFacts[0].fact + : e.modelInterpretation?.semanticChange ?? e.eventType, + })); + } +} diff --git a/src/export/ImportUpload.ts b/src/export/ImportUpload.ts new file mode 100644 index 0000000..1e112a2 --- /dev/null +++ b/src/export/ImportUpload.ts @@ -0,0 +1,148 @@ +import type { EvidenceEvent } from "../types.js"; + +const VALID_EVENT_TYPES = new Set([ + "claim_first_seen", "claim_removed", "claim_softened", "claim_strengthened", + "claim_reworded", "claim_moved", "claim_reintroduced", + "citation_added", "citation_removed", "citation_replaced", + "template_added", "template_removed", "revert_detected", + "section_reorganized", "lead_promotion", "lead_demotion", "page_moved", + "wikilink_added", "wikilink_removed", "category_added", "category_removed", + "protection_changed", "talk_page_correlated", "talk_thread_opened", + "talk_thread_archived", "talk_reply_added", "template_parameter_changed", +]); + +const VALID_LAYERS = new Set([ + "observed", "policy_coded", "model_interpretation", "speculative", "unknown", +]); + +export class ImportUpload { + private container: HTMLElement; + private onData: (events: EvidenceEvent[]) => void; + + constructor(container: HTMLElement, onData: (events: EvidenceEvent[]) => void) { + this.container = container; + this.onData = onData; + this.render(); + } + + private render(): void { + this.container.innerHTML = ""; + + const uploadZone = document.createElement("div"); + uploadZone.className = "upload-zone"; + uploadZone.textContent = "Upload JSONL"; + + const fileInput = document.createElement("input"); + fileInput.type = "file"; + fileInput.accept = ".jsonl,.json"; + fileInput.style.display = "none"; + + uploadZone.addEventListener("click", () => fileInput.click()); + uploadZone.addEventListener("dragover", (e) => { + e.preventDefault(); + uploadZone.style.borderColor = "var(--accent)"; + }); + uploadZone.addEventListener("dragleave", () => { + uploadZone.style.borderColor = ""; + }); + uploadZone.addEventListener("drop", (e) => { + e.preventDefault(); + uploadZone.style.borderColor = ""; + const files = e.dataTransfer?.files; + if (files && files.length > 0) { + this.handleFile(files[0]); + } + }); + + fileInput.addEventListener("change", () => { + const file = fileInput.files?.[0]; + if (file) { + this.handleFile(file); + } + }); + + this.container.appendChild(uploadZone); + this.container.appendChild(fileInput); + + const status = document.createElement("div"); + status.id = "import-status"; + status.style.cssText = "font-size:0.75rem;color:var(--text-dim);"; + this.container.appendChild(status); + } + + private async handleFile(file: File): Promise { + const status = document.getElementById("import-status"); + if (!status) return; + + try { + const text = await file.text(); + const lines = text.split("\n").filter((l) => l.trim().length > 0); + const events: EvidenceEvent[] = []; + const errors: string[] = []; + + for (let i = 0; i < lines.length; i++) { + try { + const parsed = JSON.parse(lines[i]); + const validation = this.validateEvent(parsed); + if (validation.valid) { + events.push(parsed as EvidenceEvent); + } else { + errors.push(`Line ${i + 1}: ${validation.error}`); + } + } catch { + errors.push(`Line ${i + 1}: Invalid JSON`); + } + } + + if (events.length > 0) { + status.textContent = `Imported ${events.length} events${errors.length > 0 ? ` (${errors.length} errors)` : ""}`; + status.style.color = "var(--green)"; + this.onData(events); + } else { + status.textContent = "No valid events found."; + status.style.color = "var(--red)"; + } + + if (errors.length > 0) { + console.warn("Import errors:", errors); + } + } catch (err) { + status.textContent = `Error reading file: ${err}`; + status.style.color = "var(--red)"; + } + } + + private validateEvent(obj: unknown): { valid: boolean; error?: string } { + if (!obj || typeof obj !== "object") { + return { valid: false, error: "Not an object" }; + } + + const e = obj as Record; + + if (typeof e.eventType !== "string" || !VALID_EVENT_TYPES.has(e.eventType)) { + return { valid: false, error: `Invalid or missing eventType: ${e.eventType}` }; + } + + if (typeof e.fromRevisionId !== "number" || typeof e.toRevisionId !== "number") { + return { valid: false, error: "Missing fromRevisionId or toRevisionId" }; + } + + if (typeof e.timestamp !== "string" || isNaN(new Date(e.timestamp).getTime())) { + return { valid: false, error: "Invalid or missing timestamp" }; + } + + if (typeof e.section !== "string") { + return { valid: false, error: "Missing section" }; + } + + if (typeof e.before !== "string" || typeof e.after !== "string") { + return { valid: false, error: "Missing before or after text" }; + } + + if (e.layer && typeof e.layer === "string" && !VALID_LAYERS.has(e.layer)) { + return { valid: false, error: `Invalid layer: ${e.layer}` }; + } + + return { valid: true }; + } +} diff --git a/src/export/SchemaViewer.ts b/src/export/SchemaViewer.ts new file mode 100644 index 0000000..00e957d --- /dev/null +++ b/src/export/SchemaViewer.ts @@ -0,0 +1,97 @@ +export class SchemaViewer { + private container: HTMLElement; + private events: Record[] = []; + + constructor(container: HTMLElement) { + this.container = container; + this.render(); + } + + setData(events: Record[]): void { + this.events = events; + this.render(); + } + + render(): void { + this.container.innerHTML = ""; + + const title = document.createElement("div"); + title.className = "panel-title"; + title.textContent = "Event Schema Inspector"; + this.container.appendChild(title); + + if (this.events.length === 0) { + const empty = document.createElement("div"); + empty.style.cssText = "color:var(--text-dim);font-size:0.85rem;padding:1rem 0;"; + empty.textContent = "Upload data to inspect schema."; + this.container.appendChild(empty); + return; + } + + const keys = this.collectKeys(); + + const schema = document.createElement("div"); + schema.className = "schema-viewer"; + + const headerEl = document.createElement("div"); + headerEl.style.cssText = "color:var(--text-dim);margin-bottom:0.5rem;font-weight:600;"; + headerEl.textContent = `Schema (${this.events.length} events, ${keys.length} fields)`; + schema.appendChild(headerEl); + + for (const key of keys) { + const row = document.createElement("div"); + row.style.cssText = "display:flex;gap:0.5rem;padding:0.15rem 0;"; + + const keyName = document.createElement("span"); + keyName.style.cssText = "color:var(--accent);min-width:200px;"; + keyName.textContent = key; + row.appendChild(keyName); + + const typeInfo = this.getTypeInfo(key); + const typeName = document.createElement("span"); + typeName.style.cssText = "color:var(--text-dim);min-width:100px;"; + typeName.textContent = typeInfo.type; + row.appendChild(typeName); + + const presence = document.createElement("span"); + presence.style.cssText = "color:var(--text-dim);"; + presence.textContent = `${typeInfo.presence}% present`; + row.appendChild(presence); + + schema.appendChild(row); + } + + this.container.appendChild(schema); + } + + private collectKeys(): string[] { + const keySet = new Set(); + for (const event of this.events) { + for (const key of Object.keys(event)) { + keySet.add(key); + } + } + const keys = Array.from(keySet); + keys.sort(); + return keys; + } + + private getTypeInfo(key: string): { type: string; presence: number } { + let present = 0; + const types = new Set(); + + for (const event of this.events) { + if (key in event) { + present++; + const val = event[key]; + if (val === null) types.add("null"); + else if (Array.isArray(val)) types.add("array"); + else types.add(typeof val); + } + } + + const typeStr = Array.from(types).join(" | "); + const pct = Math.round((present / this.events.length) * 100); + return { type: typeStr, presence: pct }; + } +} diff --git a/src/export/TimelineExport.ts b/src/export/TimelineExport.ts new file mode 100644 index 0000000..b04a785 --- /dev/null +++ b/src/export/TimelineExport.ts @@ -0,0 +1,96 @@ +import type { EvidenceEvent } from "../types.js"; + +export type ExportFormat = "json" | "jsonl" | "csv"; + +export class TimelineExport { + private container: HTMLElement; + private events: EvidenceEvent[] = []; + + constructor(container: HTMLElement) { + this.container = container; + this.render(); + } + + setData(events: EvidenceEvent[]): void { + this.events = events; + } + + private render(): void { + this.container.innerHTML = ""; + + const jsonBtn = document.createElement("button"); + jsonBtn.className = "export-btn"; + jsonBtn.textContent = "Export JSON"; + jsonBtn.addEventListener("click", () => this.export("json")); + + const jsonlBtn = document.createElement("button"); + jsonlBtn.className = "export-btn"; + jsonlBtn.textContent = "Export JSONL"; + jsonlBtn.addEventListener("click", () => this.export("jsonl")); + + const csvBtn = document.createElement("button"); + csvBtn.className = "export-btn"; + csvBtn.textContent = "Export CSV"; + csvBtn.addEventListener("click", () => this.export("csv")); + + this.container.appendChild(jsonBtn); + this.container.appendChild(jsonlBtn); + this.container.appendChild(csvBtn); + } + + private export(format: ExportFormat): void { + if (this.events.length === 0) { + alert("No data to export. Upload a JSONL file first."); + return; + } + + let content: string; + let mimeType: string; + let extension: string; + + switch (format) { + case "json": + content = JSON.stringify(this.events, null, 2); + mimeType = "application/json"; + extension = "json"; + break; + case "jsonl": + content = this.events.map((e) => JSON.stringify(e)).join("\n"); + mimeType = "application/jsonl"; + extension = "jsonl"; + break; + case "csv": { + const headers = [ + "eventType", "timestamp", "section", "fromRevisionId", "toRevisionId", + "claimId", "layer", "before", "after", + ]; + const rows = this.events.map((e) => + headers + .map((h) => { + const val = (e as unknown as Record)[h]; + if (val === undefined || val === null) return ""; + const str = String(val); + return str.includes(",") || str.includes('"') || str.includes("\n") + ? `"${str.replace(/"/g, '""')}"` + : str; + }) + .join(",") + ); + content = [headers.join(","), ...rows].join("\n"); + mimeType = "text/csv"; + extension = "csv"; + break; + } + } + + const blob = new Blob([content], { type: mimeType }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `varia-timeline.${extension}`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } +} diff --git a/src/language/CertaintyTimeline.ts b/src/language/CertaintyTimeline.ts new file mode 100644 index 0000000..09d9e67 --- /dev/null +++ b/src/language/CertaintyTimeline.ts @@ -0,0 +1,134 @@ +import type { EvidenceEvent } from "../types.js"; + +interface CertaintyPoint { + revisionId: number; + timestamp: string; + confidence: number; + label: string; +} + +export class CertaintyTimeline { + private container: HTMLElement; + private events: EvidenceEvent[] = []; + + constructor(container: HTMLElement) { + this.container = container; + } + + setData(events: EvidenceEvent[]): void { + this.events = events; + this.render(); + } + + render(): void { + this.container.innerHTML = ""; + + const title = document.createElement("div"); + title.className = "panel-title"; + title.textContent = "Certainty Over Time"; + this.container.appendChild(title); + + const points = this.computePoints(); + + if (points.length === 0) { + const empty = document.createElement("div"); + empty.style.cssText = "color:var(--text-dim);font-size:0.85rem;padding:1rem 0;"; + empty.textContent = "No model interpretations with confidence scores found."; + this.container.appendChild(empty); + return; + } + + const chart = document.createElement("div"); + chart.className = "certainty-chart"; + chart.style.position = "relative"; + + const minConf = Math.min(...points.map((p) => p.confidence)); + const maxConf = Math.max(...points.map((p) => p.confidence)); + const range = maxConf - minConf || 1; + const totalWidth = points.length * 60; + + for (let i = 0; i < points.length; i++) { + const p = points[i]; + const y = 100 - ((p.confidence - minConf) / range) * 80 - 10; + const x = i * 60 + 30; + + const dot = document.createElement("div"); + dot.className = "certainty-dot"; + const confidenceLevel = + p.confidence >= 0.7 ? "high" : p.confidence >= 0.4 ? "medium" : "low"; + dot.classList.add(confidenceLevel); + dot.style.left = `${x}px`; + dot.style.top = `${y}%`; + dot.title = `r${p.revisionId}: ${(p.confidence * 100).toFixed(0)}%`; + + dot.addEventListener("click", () => { + const info = document.getElementById("certainty-info"); + if (info) { + info.textContent = `r${p.revisionId}: ${p.label} (${(p.confidence * 100).toFixed(0)}% confidence)`; + } + }); + + chart.appendChild(dot); + + if (i < points.length - 1) { + const next = points[i + 1]; + const nextY = 100 - ((next.confidence - minConf) / range) * 80 - 10; + const nextX = (i + 1) * 60 + 30; + + const line = document.createElement("div"); + line.className = "certainty-line"; + const angle = Math.atan2(nextY - y, nextX - x) * (180 / Math.PI); + const lineLength = Math.sqrt( + Math.pow(nextX - x, 2) + Math.pow(((nextY - y) / 100) * (chart.offsetHeight || 100), 2) + ); + line.style.width = `${lineLength}px`; + line.style.left = `${x}px`; + line.style.top = `${y}%`; + line.style.transformOrigin = "0 50%"; + line.style.transform = `rotate(${angle}deg)`; + chart.appendChild(line); + } + } + + chart.style.minHeight = "100px"; + chart.style.minWidth = `${totalWidth}px`; + this.container.appendChild(chart); + + const info = document.createElement("div"); + info.id = "certainty-info"; + info.style.cssText = "font-size:0.78rem;color:var(--text-dim);margin-top:0.25rem;font-family:var(--font-mono);"; + info.textContent = "Click a dot for details."; + this.container.appendChild(info); + + const legend = document.createElement("div"); + legend.style.cssText = "display:flex;gap:1rem;font-size:0.75rem;color:var(--text-dim);margin-top:0.25rem;"; + const high = createCertaintyLegend("High (≥70%)", "var(--green)"); + const med = createCertaintyLegend("Medium (40-69%)", "var(--yellow)"); + const low = createCertaintyLegend("Low (<40%)", "var(--red)"); + legend.append(high, med, low); + this.container.appendChild(legend); + } + + private computePoints(): CertaintyPoint[] { + const modelEvents = this.events.filter( + (e) => e.modelInterpretation && e.layer === "model_interpretation" + ); + + return modelEvents.map((e) => ({ + revisionId: e.toRevisionId, + timestamp: e.timestamp, + confidence: e.modelInterpretation!.confidence, + label: e.modelInterpretation!.semanticChange, + })); + } +} + +function createCertaintyLegend(label: string, color: string): HTMLElement { + const item = document.createElement("span"); + item.style.cssText = "display:flex;align-items:center;gap:0.3rem;"; + const dot = document.createElement("span"); + dot.style.cssText = `display:inline-block;width:8px;height:8px;border-radius:50%;background:${color};`; + item.appendChild(dot); + item.appendChild(document.createTextNode(label)); + return item; +} diff --git a/src/language/WordingDiffCard.ts b/src/language/WordingDiffCard.ts new file mode 100644 index 0000000..9fc20f3 --- /dev/null +++ b/src/language/WordingDiffCard.ts @@ -0,0 +1,106 @@ +import type { EvidenceEvent } from "../types.js"; + +interface WordingChange { + revisionId: number; + timestamp: string; + section: string; + eventType: string; + before: string; + after: string; + summary: string; +} + +export class WordingDiffCard { + private container: HTMLElement; + private events: EvidenceEvent[] = []; + + constructor(container: HTMLElement) { + this.container = container; + } + + setData(events: EvidenceEvent[]): void { + this.events = events; + this.render(); + } + + render(): void { + this.container.innerHTML = ""; + + const title = document.createElement("div"); + title.className = "panel-title"; + title.textContent = "Wording Changes"; + this.container.appendChild(title); + + const changes = this.computeChanges(); + + if (changes.length === 0) { + const empty = document.createElement("div"); + empty.style.cssText = "color:var(--text-dim);font-size:0.85rem;padding:1rem 0;"; + empty.textContent = "No wording-related events found."; + this.container.appendChild(empty); + return; + } + + const grid = document.createElement("div"); + grid.className = "card-grid"; + + for (const ch of changes) { + const card = document.createElement("div"); + card.className = "card"; + + const label = document.createElement("div"); + label.className = "card-label"; + const ts = new Date(ch.timestamp); + label.textContent = `r${ch.revisionId} · ${ts.toLocaleDateString()} · ${ch.section}`; + card.appendChild(label); + + const body = document.createElement("div"); + body.className = "card-body"; + body.textContent = ch.summary; + card.appendChild(body); + + if (ch.before !== ch.after) { + const beforeEl = document.createElement("div"); + beforeEl.className = "card-diff"; + beforeEl.style.cssText += ";border-left:3px solid var(--red);margin-bottom:2px;"; + beforeEl.textContent = `− ${ch.before}`; + card.appendChild(beforeEl); + + const afterEl = document.createElement("div"); + afterEl.className = "card-diff"; + afterEl.style.cssText += ";border-left:3px solid var(--green);"; + afterEl.textContent = `+ ${ch.after}`; + card.appendChild(afterEl); + } + + grid.appendChild(card); + } + + this.container.appendChild(grid); + } + + private computeChanges(): WordingChange[] { + const wordingTypes = new Set([ + "claim_reworded", + "claim_softened", + "claim_strengthened", + "claim_first_seen", + "claim_removed", + ]); + + const wordingEvents = this.events.filter((e) => wordingTypes.has(e.eventType)); + + return wordingEvents.map((e) => ({ + revisionId: e.toRevisionId, + timestamp: e.timestamp, + section: e.section, + eventType: e.eventType, + before: e.before, + after: e.after, + summary: + e.deterministicFacts.length > 0 + ? e.deterministicFacts[0].fact + : e.eventType.replace(/_/g, " "), + })); + } +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..ad2dc12 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,79 @@ +import type { EvidenceEvent, EventType } from "./types.js"; +import { TimelineView } from "./timeline/TimelineView.js"; +import { EventFilter } from "./timeline/EventFilter.js"; +import { DiffView } from "./timeline/DiffView.js"; +import { CitationChurnChart } from "./citations/CitationChurnChart.js"; +import { CertaintyTimeline } from "./language/CertaintyTimeline.js"; +import { ImportUpload } from "./export/ImportUpload.js"; +import { TimelineExport } from "./export/TimelineExport.js"; +import "./styles/main.css"; + +let allEvents: EvidenceEvent[] = []; + +const timelineView = new TimelineView({ + container: document.getElementById("panel-timeline")!, + onSelectEvent: (event) => { + diffView.showDiff(event); + }, +}); + +const diffView = new DiffView(document.getElementById("panel-diff")!); + +const citationChart = new CitationChurnChart(document.getElementById("panel-citations")!); +const certaintyTimeline = new CertaintyTimeline(document.getElementById("panel-certainty")!); + +const filter = new EventFilter({ + container: document.getElementById("filter-area")!, + allTypes: [], + onChange: (activeTypes) => { + applyFilters(activeTypes); + }, +}); + +const exportCtrl = new TimelineExport(document.getElementById("export-area")!); + +new ImportUpload(document.getElementById("import-area")!, (events) => { + allEvents = events; + exportCtrl.setData(events); + const allTypes = collectEventTypes(events); + filter.setTypes(allTypes); + updateAll(events); +}); + +async function loadSampleData(): Promise { + try { + const resp = await fetch("/sample-data.jsonl"); + const text = await resp.text(); + const lines = text.split("\n").filter((l) => l.trim().length > 0); + const events: EvidenceEvent[] = lines.map((l) => JSON.parse(l)); + allEvents = events; + exportCtrl.setData(events); + const allTypes = collectEventTypes(events); + filter.setTypes(allTypes); + updateAll(events); + } catch (err) { + console.warn("Could not load sample data:", err); + } +} + +function collectEventTypes(events: EvidenceEvent[]): EventType[] { + const set = new Set(); + for (const e of events) { + set.add(e.eventType); + } + return Array.from(set).sort(); +} + +function updateAll(events: EvidenceEvent[]): void { + timelineView.setData(events); + citationChart.setData(events); + certaintyTimeline.setData(events); +} + +function applyFilters(activeTypes: Set): void { + const filtered = allEvents.filter((e) => activeTypes.has(e.eventType)); + timelineView.applyFilter(filtered); + exportCtrl.setData(filtered); +} + +loadSampleData(); diff --git a/src/styles/main.css b/src/styles/main.css new file mode 100644 index 0000000..a9efd57 --- /dev/null +++ b/src/styles/main.css @@ -0,0 +1,533 @@ +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +:root { + --bg: #0f1117; + --bg-panel: #171922; + --bg-hover: #1e2030; + --border: #2a2d3e; + --text: #e2e4f0; + --text-dim: #888ba8; + --accent: #7c8aff; + --green: #4ade80; + --red: #f87171; + --orange: #fb923c; + --yellow: #facc15; + --purple: #a78bfa; + --cyan: #22d3ee; + --radius: 8px; + --font-mono: "SF Mono", "Fira Code", "Cascadia Code", monospace; +} + +html { + font-size: 14px; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + background: var(--bg); + color: var(--text); + line-height: 1.5; +} + +#app { + max-width: 1400px; + margin: 0 auto; + padding: 1rem; +} + +#app-header { + margin-bottom: 1rem; + border-bottom: 1px solid var(--border); + padding-bottom: 0.75rem; +} + +#app-header h1 { + font-size: 1.5rem; + font-weight: 600; + color: var(--accent); +} + +.subtitle { + color: var(--text-dim); + font-size: 0.85rem; +} + +#toolbar { + display: flex; + gap: 0.75rem; + margin-bottom: 1rem; + flex-wrap: wrap; + align-items: center; +} + +#toolbar > div { + display: flex; + gap: 0.5rem; + align-items: center; +} + +.panel { + background: var(--bg-panel); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 1rem; + overflow: auto; +} + +.panel-title { + font-size: 0.85rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--text-dim); + margin-bottom: 0.75rem; +} + +#dashboard { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; +} + +#panel-timeline { + grid-column: 1 / 2; + grid-row: 1 / 3; + max-height: 80vh; + overflow-y: auto; +} + +#panel-diff { + grid-column: 2 / 3; + grid-row: 1 / 2; +} + +#panel-citations { + grid-column: 2 / 3; + grid-row: 2 / 3; +} + +#panel-certainty { + grid-column: 1 / 3; + grid-row: 3 / 4; +} + +@media (max-width: 900px) { + #dashboard { + grid-template-columns: 1fr; + } + #panel-timeline, + #panel-diff, + #panel-citations, + #panel-certainty { + grid-column: 1 / 2; + } + #panel-timeline { + grid-row: auto; + } + #panel-diff { + grid-row: auto; + } + #panel-citations { + grid-row: auto; + } + #panel-certainty { + grid-row: auto; + } +} + +/* --- Buttons & Inputs --- */ + +button { + background: var(--bg-hover); + color: var(--text); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 0.4rem 0.75rem; + cursor: pointer; + font-size: 0.85rem; + transition: background 0.15s; +} + +button:hover { + background: var(--border); +} + +button.active { + background: var(--accent); + color: #fff; + border-color: var(--accent); +} + +select { + background: var(--bg-hover); + color: var(--text); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 0.4rem 0.75rem; + font-size: 0.85rem; +} + +/* --- Timeline --- */ + +.timeline { + position: relative; + padding-left: 2rem; +} + +.timeline::before { + content: ""; + position: absolute; + left: 0.5rem; + top: 0; + bottom: 0; + width: 2px; + background: var(--border); +} + +.timeline-item { + position: relative; + padding: 0.5rem 0.75rem; + margin-bottom: 0.25rem; + border-radius: var(--radius); + cursor: pointer; + transition: background 0.15s; +} + +.timeline-item:hover { + background: var(--bg-hover); +} + +.timeline-item.selected { + background: var(--bg-hover); + border-left: 3px solid var(--accent); +} + +.timeline-item::before { + content: ""; + position: absolute; + left: -1.5rem; + top: 0.75rem; + width: 10px; + height: 10px; + border-radius: 50%; + background: var(--border); +} + +.timeline-item.event-citation_added::before, +.timeline-item.event-citation_replaced::before, +.timeline-item.event-citation_removed::before { + background: var(--green); +} + +.timeline-item.event-claim_removed::before { + background: var(--red); +} + +.timeline-item.event-claim_softened::before { + background: var(--orange); +} + +.timeline-item.event-claim_strengthened::before { + background: var(--green); +} + +.timeline-item.event-claim_reworded::before { + background: var(--yellow); +} + +.timeline-item.event-revert_detected::before { + background: var(--red); +} + +.timeline-item.event-talk_reply_added::before { + background: var(--purple); +} + +.timeline-item .event-type { + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--text-dim); +} + +.timeline-item .event-summary { + font-size: 0.9rem; + margin-top: 0.15rem; +} + +.timeline-item .event-meta { + font-size: 0.75rem; + color: var(--text-dim); + margin-top: 0.15rem; + font-family: var(--font-mono); +} + +/* --- Diff --- */ + +.diff-container { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.5rem; +} + +.diff-pane { + border: 1px solid var(--border); + border-radius: var(--radius); + overflow: hidden; +} + +.diff-pane-header { + background: var(--bg-hover); + padding: 0.3rem 0.5rem; + font-size: 0.75rem; + color: var(--text-dim); + font-weight: 600; + border-bottom: 1px solid var(--border); +} + +.diff-lines { + font-family: var(--font-mono); + font-size: 0.8rem; + line-height: 1.6; + padding: 0.5rem; + white-space: pre-wrap; + word-break: break-word; +} + +.diff-line-added { + background: rgba(74, 222, 128, 0.1); + color: var(--green); +} + +.diff-line-removed { + background: rgba(248, 113, 113, 0.1); + color: var(--red); +} + +.diff-line-unchanged { + color: var(--text-dim); +} + +/* --- Bar Chart --- */ + +.bar-chart { + display: flex; + align-items: flex-end; + gap: 4px; + height: 120px; + padding: 0.5rem 0; +} + +.bar-group { + display: flex; + gap: 2px; + flex: 1; + align-items: flex-end; + height: 100%; +} + +.bar { + flex: 1; + min-width: 3px; + border-radius: 2px 2px 0 0; + transition: height 0.2s; + position: relative; +} + +.bar.added { + background: var(--green); +} + +.bar.removed { + background: var(--red); +} + +.bar:hover { + opacity: 0.8; +} + +.bar-chart-label { + text-align: center; + font-size: 0.65rem; + color: var(--text-dim); + padding-top: 0.25rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* --- Certainty Chart --- */ + +.certainty-chart { + position: relative; + height: 100px; + margin: 0.5rem 0; +} + +.certainty-line { + position: absolute; + left: 0; + right: 0; + height: 2px; + background: var(--accent); + border-radius: 1px; +} + +.certainty-dot { + position: absolute; + width: 10px; + height: 10px; + border-radius: 50%; + background: var(--accent); + transform: translate(-50%, -50%); + cursor: pointer; + transition: transform 0.15s; +} + +.certainty-dot:hover { + transform: translate(-50%, -50%) scale(1.5); +} + +.certainty-dot.high { + background: var(--green); +} + +.certainty-dot.medium { + background: var(--yellow); +} + +.certainty-dot.low { + background: var(--red); +} + +/* --- Table --- */ + +.data-table { + width: 100%; + border-collapse: collapse; + font-size: 0.8rem; +} + +.data-table th { + text-align: left; + padding: 0.4rem 0.5rem; + color: var(--text-dim); + font-weight: 600; + border-bottom: 1px solid var(--border); + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.data-table td { + padding: 0.4rem 0.5rem; + border-bottom: 1px solid var(--border); + font-family: var(--font-mono); + font-size: 0.78rem; +} + +.data-table tr:hover td { + background: var(--bg-hover); +} + +/* --- Cards --- */ + +.card-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 0.75rem; +} + +.card { + background: var(--bg); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 0.75rem; +} + +.card-label { + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--text-dim); + margin-bottom: 0.25rem; +} + +.card-body { + font-size: 0.85rem; +} + +.card-diff { + font-family: var(--font-mono); + font-size: 0.8rem; + margin-top: 0.4rem; + padding: 0.4rem; + background: var(--bg-panel); + border-radius: 4px; +} + +/* --- Export --- */ + +.export-btn { + background: var(--accent); + color: #fff; + border-color: var(--accent); +} + +.export-btn:hover { + opacity: 0.9; +} + +/* --- Upload Zone --- */ + +.upload-zone { + border: 2px dashed var(--border); + border-radius: var(--radius); + padding: 0.4rem 0.75rem; + cursor: pointer; + font-size: 0.85rem; + color: var(--text-dim); + transition: border-color 0.15s; + display: inline-block; +} + +.upload-zone:hover { + border-color: var(--accent); + color: var(--accent); +} + +/* --- Filter tags --- */ + +.filter-tag { + display: inline-flex; + align-items: center; + gap: 0.3rem; + background: var(--bg-hover); + border: 1px solid var(--border); + border-radius: 4px; + padding: 0.2rem 0.5rem; + font-size: 0.75rem; + cursor: pointer; + transition: background 0.15s; +} + +.filter-tag.active { + background: var(--accent); + color: #fff; + border-color: var(--accent); +} + +/* --- Schema viewer --- */ + +.schema-viewer { + font-family: var(--font-mono); + font-size: 0.78rem; + line-height: 1.6; + white-space: pre-wrap; +} diff --git a/src/timeline/DiffView.ts b/src/timeline/DiffView.ts new file mode 100644 index 0000000..16ad34f --- /dev/null +++ b/src/timeline/DiffView.ts @@ -0,0 +1,126 @@ +import type { EvidenceEvent, DiffLine } from "../types.js"; + +function computeDiff(before: string, after: string): DiffLine[] { + const beforeLines = before.split("\n"); + const afterLines = after.split("\n"); + const lines: DiffLine[] = []; + const maxLen = Math.max(beforeLines.length, afterLines.length); + + for (let i = 0; i < maxLen; i++) { + const b = beforeLines[i] ?? ""; + const a = afterLines[i] ?? ""; + + if (i >= beforeLines.length) { + lines.push({ type: "added", content: a, lineNumber: i + 1 }); + } else if (i >= afterLines.length) { + lines.push({ type: "removed", content: b, lineNumber: i + 1 }); + } else if (b === a) { + lines.push({ type: "unchanged", content: b, lineNumber: i + 1 }); + } else { + lines.push({ type: "removed", content: b, lineNumber: i + 1 }); + lines.push({ type: "added", content: a, lineNumber: i + 1 }); + } + } + + return lines; +} + +export class DiffView { + private container: HTMLElement; + private event: EvidenceEvent | null = null; + + constructor(container: HTMLElement) { + this.container = container; + this.renderEmpty(); + } + + showDiff(event: EvidenceEvent): void { + this.event = event; + this.render(); + } + + clear(): void { + this.event = null; + this.renderEmpty(); + } + + private renderEmpty(): void { + this.container.innerHTML = ""; + const title = document.createElement("div"); + title.className = "panel-title"; + title.textContent = "Diff Viewer"; + this.container.appendChild(title); + + const hint = document.createElement("div"); + hint.style.cssText = "color:var(--text-dim);font-size:0.85rem;padding:1rem 0;"; + hint.textContent = "Click an event in the timeline to see the diff."; + this.container.appendChild(hint); + } + + private render(): void { + if (!this.event) { + this.renderEmpty(); + return; + } + + const event = this.event; + this.container.innerHTML = ""; + + const title = document.createElement("div"); + title.className = "panel-title"; + title.textContent = `Diff · r${event.fromRevisionId} → r${event.toRevisionId}`; + this.container.appendChild(title); + + const sectionInfo = document.createElement("div"); + sectionInfo.style.cssText = "font-size:0.78rem;color:var(--text-dim);margin-bottom:0.5rem;font-family:var(--font-mono);"; + sectionInfo.textContent = event.section || "(no section)"; + this.container.appendChild(sectionInfo); + + const diffLines = computeDiff(event.before, event.after); + + const diffContainer = document.createElement("div"); + diffContainer.className = "diff-container"; + + const beforePane = document.createElement("div"); + beforePane.className = "diff-pane"; + const beforeHeader = document.createElement("div"); + beforeHeader.className = "diff-pane-header"; + beforeHeader.textContent = "Before"; + beforePane.appendChild(beforeHeader); + const beforeContent = document.createElement("div"); + beforeContent.className = "diff-lines"; + beforePane.appendChild(beforeContent); + + const afterPane = document.createElement("div"); + afterPane.className = "diff-pane"; + const afterHeader = document.createElement("div"); + afterHeader.className = "diff-pane-header"; + afterHeader.textContent = "After"; + afterPane.appendChild(afterHeader); + const afterContent = document.createElement("div"); + afterContent.className = "diff-lines"; + afterPane.appendChild(afterContent); + + for (const line of diffLines) { + if (line.type === "added") { + afterContent.appendChild(createDiffLine(line, "diff-line-added")); + } else if (line.type === "removed") { + beforeContent.appendChild(createDiffLine(line, "diff-line-removed")); + } else { + beforeContent.appendChild(createDiffLine(line, "diff-line-unchanged")); + afterContent.appendChild(createDiffLine(line, "diff-line-unchanged")); + } + } + + diffContainer.appendChild(beforePane); + diffContainer.appendChild(afterPane); + this.container.appendChild(diffContainer); + } +} + +function createDiffLine(line: DiffLine, className: string): HTMLElement { + const el = document.createElement("div"); + el.className = className; + el.textContent = line.content || " "; + return el; +} diff --git a/src/timeline/EventFilter.ts b/src/timeline/EventFilter.ts new file mode 100644 index 0000000..d169b97 --- /dev/null +++ b/src/timeline/EventFilter.ts @@ -0,0 +1,88 @@ +import type { EventType } from "../types.js"; + +export interface EventFilterOptions { + container: HTMLElement; + allTypes: EventType[]; + onChange: (activeTypes: Set) => void; +} + +export class EventFilter { + private container: HTMLElement; + private allTypes: EventType[]; + private activeTypes: Set; + private onChange: (activeTypes: Set) => void; + + constructor(opts: EventFilterOptions) { + this.container = opts.container; + this.allTypes = opts.allTypes; + this.activeTypes = new Set(opts.allTypes); + this.onChange = opts.onChange; + this.render(); + } + + setTypes(types: EventType[]): void { + this.allTypes = types; + this.activeTypes = new Set(types); + this.render(); + } + + getActiveTypes(): Set { + return this.activeTypes; + } + + toggleType(type: EventType): void { + if (this.activeTypes.has(type)) { + if (this.activeTypes.size > 1) { + this.activeTypes.delete(type); + } + } else { + this.activeTypes.add(type); + } + this.render(); + this.onChange(this.activeTypes); + } + + selectAll(): void { + this.activeTypes = new Set(this.allTypes); + this.render(); + this.onChange(this.activeTypes); + } + + clearAll(): void { + this.activeTypes = new Set(); + this.render(); + this.onChange(this.activeTypes); + } + + render(): void { + this.container.innerHTML = ""; + + const selectAll = document.createElement("button"); + selectAll.textContent = "All"; + selectAll.className = this.activeTypes.size === this.allTypes.length ? "active" : ""; + selectAll.addEventListener("click", () => this.selectAll()); + this.container.appendChild(selectAll); + + const clearBtn = document.createElement("button"); + clearBtn.textContent = "None"; + clearBtn.className = this.activeTypes.size === 0 ? "active" : ""; + clearBtn.addEventListener("click", () => this.clearAll()); + this.container.appendChild(clearBtn); + + for (const type of this.allTypes) { + const tag = document.createElement("span"); + tag.className = "filter-tag"; + if (this.activeTypes.has(type)) { + tag.classList.add("active"); + } + tag.textContent = formatEventTypeShort(type); + tag.title = formatEventTypeShort(type); + tag.addEventListener("click", () => this.toggleType(type)); + this.container.appendChild(tag); + } + } +} + +function formatEventTypeShort(type: EventType): string { + return type.replace(/_/g, " "); +} diff --git a/src/timeline/TimelineView.ts b/src/timeline/TimelineView.ts new file mode 100644 index 0000000..cd69961 --- /dev/null +++ b/src/timeline/TimelineView.ts @@ -0,0 +1,111 @@ +import type { EvidenceEvent, EventType } from "../types.js"; + +export interface TimelineViewOptions { + container: HTMLElement; + onSelectEvent?: (event: EvidenceEvent) => void; +} + +export class TimelineView { + private container: HTMLElement; + private events: EvidenceEvent[] = []; + private filtered: EvidenceEvent[] = []; + private selectedId: string | null = null; + private onSelectEvent?: (event: EvidenceEvent) => void; + + constructor(opts: TimelineViewOptions) { + this.container = opts.container; + this.onSelectEvent = opts.onSelectEvent; + } + + setData(events: EvidenceEvent[]): void { + this.events = events; + this.filtered = [...events]; + this.render(); + } + + applyFilter(filtered: EvidenceEvent[]): void { + this.filtered = filtered; + this.render(); + } + + selectEvent(eventId: string | null): void { + this.selectedId = eventId; + this.render(); + } + + render(): void { + this.container.innerHTML = ""; + + const title = document.createElement("div"); + title.className = "panel-title"; + title.textContent = "Revision Timeline"; + this.container.appendChild(title); + + const count = document.createElement("div"); + count.style.cssText = "font-size:0.75rem;color:var(--text-dim);margin-bottom:0.5rem;"; + count.textContent = `${this.filtered.length} of ${this.events.length} events`; + this.container.appendChild(count); + + if (this.filtered.length === 0) { + const empty = document.createElement("div"); + empty.style.cssText = "padding:1rem 0;color:var(--text-dim);font-size:0.85rem;"; + empty.textContent = "No events match the current filter."; + this.container.appendChild(empty); + return; + } + + const timeline = document.createElement("div"); + timeline.className = "timeline"; + + for (const event of this.filtered) { + const item = document.createElement("div"); + item.className = `timeline-item event-${event.eventType}`; + if (event.eventId && event.eventId === this.selectedId) { + item.classList.add("selected"); + } + + const typeLabel = document.createElement("div"); + typeLabel.className = "event-type"; + typeLabel.textContent = formatEventType(event.eventType); + item.appendChild(typeLabel); + + const summary = document.createElement("div"); + summary.className = "event-summary"; + summary.textContent = summarizeEvent(event); + item.appendChild(summary); + + const meta = document.createElement("div"); + meta.className = "event-meta"; + const ts = new Date(event.timestamp); + meta.textContent = `${ts.toLocaleDateString()} ${ts.toLocaleTimeString()} · r${event.fromRevisionId} → r${event.toRevisionId}`; + if (event.section) { + meta.textContent += ` · ${event.section}`; + } + item.appendChild(meta); + + item.addEventListener("click", () => { + this.selectedId = event.eventId ?? null; + this.render(); + this.onSelectEvent?.(event); + }); + + timeline.appendChild(item); + } + + this.container.appendChild(timeline); + } +} + +function formatEventType(type: EventType): string { + return type.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()); +} + +function summarizeEvent(event: EvidenceEvent): string { + if (event.deterministicFacts.length > 0) { + return event.deterministicFacts[0].fact; + } + if (event.modelInterpretation) { + return event.modelInterpretation.semanticChange; + } + return `${formatEventType(event.eventType)} in ${event.section}`; +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..5294a4f --- /dev/null +++ b/src/types.ts @@ -0,0 +1,63 @@ +export type EvidenceLayer = "observed" | "policy_coded" | "model_interpretation" | "speculative" | "unknown"; + +export type EventType = + | "claim_first_seen" + | "claim_removed" + | "claim_softened" + | "claim_strengthened" + | "claim_reworded" + | "claim_moved" + | "claim_reintroduced" + | "citation_added" + | "citation_removed" + | "citation_replaced" + | "template_added" + | "template_removed" + | "revert_detected" + | "section_reorganized" + | "lead_promotion" + | "lead_demotion" + | "page_moved" + | "wikilink_added" + | "wikilink_removed" + | "category_added" + | "category_removed" + | "protection_changed" + | "talk_page_correlated" + | "talk_thread_opened" + | "talk_thread_archived" + | "talk_reply_added" + | "template_parameter_changed"; + +export interface ModelInterpretation { + semanticChange: string; + confidence: number; + policyDimension?: string; + discussionType?: string; +} + +export interface EvidenceEvent { + eventId?: string; + eventType: EventType; + claimId?: string; + fromRevisionId: number; + toRevisionId: number; + section: string; + before: string; + after: string; + deterministicFacts: { fact: string; detail?: string }[]; + modelInterpretation?: ModelInterpretation; + layer: EvidenceLayer; + timestamp: string; +} + +export interface DiffLine { + type: "added" | "removed" | "unchanged"; + content: string; + lineNumber: number; +} + +export interface TimelineFilter { + eventTypes: Set; + dateRange: { start: string; end: string } | null; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..7cd3cf2 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "verbatimModuleSyntax": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "dist", + "rootDir": "src", + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..9221db8 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vite"; + +export default defineConfig({ + root: ".", + build: { + outDir: "dist", + }, +}); From 739a6ed25642a85400e51e13df12d8cce51ffc43 Mon Sep 17 00:00:00 2001 From: Kanav Jain Date: Thu, 14 May 2026 13:22:26 -0500 Subject: [PATCH 003/183] docs: add CC-BY-4.0 license --- LICENSE | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2e05cf3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,14 @@ +Creative Commons Attribution 4.0 International (CC BY 4.0) + +https://creativecommons.org/licenses/by/4.0/ + +You are free to: +- Share — copy and redistribute the material in any medium or format +- Adapt — remix, transform, and build upon the material for any purpose, even commercially + +Under the following terms: +- Attribution — You must give appropriate credit, provide a link to the license, + and indicate if changes were made. + +No additional restrictions — You may not apply legal terms or technological +measures that legally restrict others from doing anything the license permits. From 82748487af7845271e51b17259c49e9576b7b3f5 Mon Sep 17 00:00:00 2001 From: Kanav Jain Date: Thu, 14 May 2026 13:22:27 -0500 Subject: [PATCH 004/183] docs: add MIT license --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a8c12f1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 NextConsensus + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From 2763c42f73c53f8ee04311a4389d2119bbeac98e Mon Sep 17 00:00:00 2001 From: Kanav Jain Date: Thu, 14 May 2026 13:29:27 -0500 Subject: [PATCH 005/183] fix: correct stale links and license references in README --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a5aa6c5..82e339d 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# Varia +# Varia Docs -**Varia answers: "What changed?"** +Public documentation for [Varia](https://github.com/var-ia/var-ia), the open-source deterministic observation engine for public revision histories. -An open-source, deterministic observation engine for public revision histories. Varia tracks what changes in collaborative knowledge spaces — Wikipedia, Fandom wikis, public registries — and makes those changes auditable, analyzable, and interpretable. +**Varia answers: "What changed?"** --- @@ -33,4 +33,4 @@ Varia is in active development by [NextConsensus](https://nextconsensus.com). Th ## License -MIT +CC-BY-4.0 From b0376ed9ad16c22f4dcafaad1e6c1ab24a4044fc Mon Sep 17 00:00:00 2001 From: Kanav Jain Date: Thu, 14 May 2026 13:29:29 -0500 Subject: [PATCH 006/183] fix: correct stale links and license references in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 639f797..9213a19 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # varia-ui -Generic visualization interface for the [Varia](https://github.com/anomalyco/varia) observation engine. +Generic visualization interface for the [Varia](https://github.com/var-ia/var-ia) observation engine. Standalone web app built with vanilla TypeScript and Vite. Loads JSONL event data (compatible with `@var-ia/evidence-graph`) and renders timeline, diff, citation, and certainty visualizations. From 23a6d3653bc4acdc39453fc91f03fe3dce868111 Mon Sep 17 00:00:00 2001 From: Kanav Jain Date: Thu, 14 May 2026 13:32:14 -0500 Subject: [PATCH 007/183] docs: complete schema, tutorials, SDK, install page, and concepts --- docs/concepts.md | 52 ++++++-- docs/install.md | 46 ++++++++ docs/quickstart.md | 4 +- docs/schema.md | 177 +++++++++++++++++++--------- docs/sdk.md | 82 +++++++++++-- docs/tutorials/citation-churn.md | 47 ++++++-- docs/tutorials/dispute-timeline.md | 49 ++++++-- docs/tutorials/fandom-canon.md | 26 +++- docs/tutorials/wikipedia-history.md | 42 ++++++- 9 files changed, 422 insertions(+), 103 deletions(-) create mode 100644 docs/install.md diff --git a/docs/concepts.md b/docs/concepts.md index 760ca09..cfb4dfa 100644 --- a/docs/concepts.md +++ b/docs/concepts.md @@ -31,20 +31,52 @@ Talk page consensus, RFC closures, ArbCom decisions, published corrections. Thes **Invariant:** L3 is never redefined by L1 or L2. -## Design principles +## L2 — Model Adapter Providers -1. **No single accuracy score** conflates layers. -2. **Every interpretation** carries a confidence score. -3. **Deterministic facts** are always presented before interpretations. -4. **Layers are independently testable.** +Varia supports multiple model providers without coupling to any one: +- **OpenAI** — GPT-4, GPT-4o +- **Anthropic** — Claude 3.5 Sonnet, Claude 3 Opus +- **DeepSeek** — DeepSeek V2 +- **Ollama (local)** — llama.cpp-based models running on your own machine +- **BYOK** — Bring your own key to any OpenAI-compatible endpoint -## Event model +## Confidence Scores + +Every L2 interpretation carries a `confidence` score (0.0–1.0). The system supports calibration correction to map raw model confidence to empirical accuracy via bin-based calibration. + +## Cascading & Consensus + +- **CascadingRouter** — tries a primary adapter, falls back to cheaper/secondary if confidence is below threshold +- **ConsensusAdapter** — runs multiple adapters, picks the majority interpretation +- **CalibratedAdapter** — wraps any adapter with calibration bin correction + +## L3 — Ground Truth Sources + +Independent ground truth can come from: +- Talk page RFC closures +- Arbitration Committee (ArbCom) decisions +- Page protection events logged by administrators +- Published corrections and retractions +- External reliable source assessments + +## Design Principles + +1. **No single accuracy score** conflates layers. L1 accuracy is 100% by construction. L2 is measured per-adapter. L3 is independently sourced. +2. **Every interpretation** carries a confidence score. No unconfident output escapes L2. +3. **Deterministic facts** are always presented before interpretations. Consumers see the observable evidence before the model's reading of it. +4. **Layers are independently testable.** You can evaluate L2 without L3, and L3 without L2. + +## Event Model All L1 output is structured as typed events: -- `section_added`, `section_removed`, `section_renamed` -- `citation_added`, `citation_removed` -- `revert_detected` -- `template_added`, `template_removed`, `template_changed` +- **Claim events**: claim_first_seen, claim_removed, claim_softened, claim_strengthened, claim_reworded, claim_moved, claim_reintroduced +- **Citation events**: citation_added, citation_removed, citation_replaced +- **Template events**: template_added, template_removed, template_parameter_changed +- **Revert events**: revert_detected +- **Section/page events**: section_reorganized, lead_promotion, lead_demotion, page_moved +- **Link/category events**: wikilink_added, wikilink_removed, category_added, category_removed +- **Protection events**: protection_changed +- **Talk page events**: talk_page_correlated, talk_thread_opened, talk_thread_archived, talk_reply_added See [schema.md](schema.md) for the full reference. diff --git a/docs/install.md b/docs/install.md new file mode 100644 index 0000000..fd5f851 --- /dev/null +++ b/docs/install.md @@ -0,0 +1,46 @@ +# Installation + +## System requirements + +- **Bun** v1.2+ (recommended runtime) +- **Node.js** 20+ (alternative runtime) +- **macOS** or **Linux** (Windows not yet supported) + +## npm / bun (recommended) + +```bash +# with bun +bun add -g @var-ia/cli + +# with npm +npm install -g @var-ia/cli +``` + +## Docker + +```bash +# build from source +docker build -t varia https://github.com/var-ia/varia.git +docker run --rm varia analyze "Earth" --depth quick + +# or build locally from a cloned repo +git clone https://github.com/var-ia/varia.git +cd varia +docker build -t varia . +docker run --rm varia analyze "Earth" --depth quick +``` + +## From source + +```bash +git clone https://github.com/var-ia/varia.git +cd varia +bun install +bun run build +``` + +## Verify installation + +```bash +wikihistory --version +``` diff --git a/docs/quickstart.md b/docs/quickstart.md index df1e55e..b202269 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -2,9 +2,11 @@ ## Prerequisites -- [Bun](https://bun.sh) v1.2+ runtime +- [Bun](https://bun.sh) v1.2+ runtime (or Node.js 20+) - Internet connection (for fetching revision data from Wikimedia) +See [installation](install.md) for all options (npm, Docker, from source). + ## Install the CLI ```bash diff --git a/docs/schema.md b/docs/schema.md index 01523c2..346cfaa 100644 --- a/docs/schema.md +++ b/docs/schema.md @@ -1,85 +1,150 @@ # Event schema reference -## Core type: `EvidenceEvent` - -All events share a common envelope: +## `EventType` — all 27 event types ```typescript -interface EvidenceEvent { - id: string // deterministic content hash - type: EventType // discriminator - page: string // wiki page title - wiki: string // wiki identifier (e.g. "en.wikipedia.org") - timestamp: string // ISO 8601 - revisionId: number // source revision - previousRevisionId: number // parent revision - confidence: number // 1.0 for L1, 0-1 for L2 - metadata: Record // type-specific payload -} -``` - -## `EventType` +export type EventType = + // Claim lifecycle + | "claim_first_seen" + | "claim_removed" + | "claim_softened" + | "claim_strengthened" + | "claim_reworded" + | "claim_moved" + | "claim_reintroduced" -```typescript -type EventType = - | "section_added" - | "section_removed" - | "section_renamed" + // Citation changes | "citation_added" | "citation_removed" - | "revert_detected" - | "claim_added" - | "claim_removed" - | "claim_modified" + | "citation_replaced" + + // Template changes | "template_added" | "template_removed" - | "template_changed" + | "template_parameter_changed" + + // Section & page structure + | "section_reorganized" + | "lead_promotion" + | "lead_demotion" + | "page_moved" + + // Links & categories + | "wikilink_added" + | "wikilink_removed" + | "category_added" + | "category_removed" + + // Access control + | "protection_changed" + + // Content conflict + | "revert_detected" + + // Talk page correlation + | "talk_page_correlated" + | "talk_thread_opened" + | "talk_thread_archived" + | "talk_reply_added"; ``` -## Claim events +## Core interface: `EvidenceEvent` + +All events share a common envelope with `before`/`after` snapshots and +deterministic facts. There are no discriminated subtypes — all fields are +optional at the envelope level: ```typescript -interface ClaimAddedEvent extends EvidenceEvent { - type: "claim_added"; - metadata: { - claimText: string; - sourceCitation?: string; - sectionTitle: string; - }; +export interface EvidenceEvent { + eventId?: string; // deterministic content hash (see below) + eventType: EventType; // discriminator + claimId?: string; // claim identity hash, when applicable + fromRevisionId: number; // parent revision + toRevisionId: number; // source revision + section: string; // section title where change occurred + before: string; // text / state before the change + after: string; // text / state after the change + deterministicFacts: DeterministicFact[]; // L1 facts backing this event + modelInterpretation?: ModelInterpretation; // L2 interpretation (never present for L1 events) + layer: EvidenceLayer; // provenance layer + timestamp: string; // ISO 8601 } ``` -## Citation events +## Supporting types ```typescript -interface CitationAddedEvent extends EvidenceEvent { - type: "citation_added"; - metadata: { - citationKey: string; - citationText: string; - sectionTitle: string; - sourceType?: string; - }; +export type EvidenceLayer = + | "observed" // directly observed from the diff + | "policy_coded" // coded against a policy rule + | "model_interpretation" // produced by an L2 model + | "speculative" // low-confidence inference + | "unknown"; // layer could not be determined + +export interface DeterministicFact { + fact: string; + detail?: string; + provenance?: FactProvenance; +} + +export interface FactProvenance { + analyzer: string; // analyzer that produced this fact + version: string; // analyzer version + inputHashes: string[]; // hashes of input data used } ``` -## Revert events +## `ModelInterpretation` (L2 only) ```typescript -interface RevertDetectedEvent extends EvidenceEvent { - type: "revert_detected"; - metadata: { - revertedRevisionId: number; - revertType: "exact" | "partial"; - matchScore: number; - }; +export interface ModelInterpretation { + semanticChange: string; // natural-language summary of the change + confidence: number; // 0–1 confidence score + policyDimension?: PolicyDimension; // which policy is most relevant + discussionType?: // talk-page discussion category + | "notability_challenge" + | "sourcing_dispute" + | "neutrality_concern" + | "content_deletion" + | "content_addition" + | "naming_dispute" + | "procedural" + | "other"; } + +export type PolicyDimension = + | "verifiability" + | "npov" + | "blp" + | "due_weight" + | "protection" + | "edit_warring" + | "notability" + | "copyright" + | "civility"; ``` ## Deterministic identity -Every `EvidenceEvent.id` is a SHA-256 content hash of its type + page + timestamp + metadata. This ensures: +`eventId` is derived deterministically from the event content so the same +observation always produces the same ID: + +```typescript +import { createHash } from "node:crypto"; + +export function createEventIdentity( + event: Omit, +): string { + const factsStr = event.deterministicFacts + .map((f) => `${f.fact}:${f.detail ?? ""}`) + .join("|"); + const identityKey = + `${event.eventType}|${event.fromRevisionId}|${event.toRevisionId}|` + + `${event.section}|${event.before}|${event.after}|${event.timestamp}|${factsStr}`; + return createHash("sha256").update(identityKey).digest("hex").slice(0, 16); +} +``` -- Same input always produces the same event ID -- Duplicate detection is trivial -- Events can be compared across systems by ID alone +The hash is truncated to 16 hex characters (64 bits of collision resistance). +Because `modelInterpretation` is excluded from the hash, the same L1 evidence +always resolves to the same event ID regardless of L2 interpretation changes. diff --git a/docs/sdk.md b/docs/sdk.md index b174385..0c4b38f 100644 --- a/docs/sdk.md +++ b/docs/sdk.md @@ -1,6 +1,58 @@ # SDK / package reference -## `@var-ia/evidence-graph` +## Overview + +Varia's SDK is a set of 7 packages that compose into a pipeline: ingest → analyze → persist → interpret. + +Packages are published on npm under the `@var-ia` scope. All packages are ESM-only and written in TypeScript. + +## Basic pipeline + +```typescript +import { MediaWikiClient } from "@var-ia/ingestion"; +import { SectionDiffer, CitationTracker, RevertDetector } from "@var-ia/analyzers"; +import type { EvidenceEvent } from "@var-ia/evidence-graph"; + +const client = new MediaWikiClient({ api: "https://en.wikipedia.org/w/api.php" }); +const differ = new SectionDiffer(); +const citations = new CitationTracker(); + +// Fetch revisions +const revisions = await client.fetchRevisions({ page: "Earth", limit: 10 }); + +// Run analysis +const events: EvidenceEvent[] = []; +for (const rev of revisions) { + events.push(...differ.diff(rev.previous, rev.current)); + events.push(...citations.diff(rev.previous, rev.current)); +} +``` + +## Storage + +```typescript +import { EventStore } from "@var-ia/persistence"; + +const store = new EventStore("varia.db"); +await store.insertEvents(events); +const saved = await store.getEvents({ page: "Earth" }); +``` + +## L2 interpretation + +```typescript +import { createOpenAIAdapter } from "@var-ia/interpreter/openai"; + +const adapter = createOpenAIAdapter({ + model: "gpt-4", + apiKey: process.env.OPENAI_API_KEY, +}); +const interpreted = await adapter.interpret(events); +``` + +## Package reference + +### `@var-ia/evidence-graph` Core types, event schemas, and utilities. Zero runtime dependencies. @@ -9,12 +61,12 @@ import type { EvidenceEvent, EventType } from "@var-ia/evidence-graph"; import { createClaimIdentity, hashEvent } from "@var-ia/evidence-graph"; ``` -**Exports:** -- Interfaces: `EvidenceEvent`, `ClaimAddedEvent`, `CitationAddedEvent`, `RevertDetectedEvent` -- Types: `EventType` -- Utilities: `createClaimIdentity`, `hashEvent`, `compareEvents`, `mergeEventStreams` +Key exports: +- Interface: `EvidenceEvent` +- Types: `EventType`, `EvidenceLayer`, `PolicyDimension` +- Utilities: `createClaimIdentity`, `createEventIdentity`, `compareEvents` -## `@var-ia/ingestion` +### `@var-ia/ingestion` Wikimedia API adapters. Fetches revision history and parses wikitext. @@ -23,9 +75,9 @@ import { RevisionFetcher } from "@var-ia/ingestion"; const fetcher = new RevisionFetcher({ wiki: "en.wikipedia.org" }); ``` -Includes: `RevisionFetcher`, `WikitextParser` +Key exports: `RevisionFetcher`, `WikitextParser`, `MediaWikiClient` -## `@var-ia/analyzers` +### `@var-ia/analyzers` Deterministic analyzers for section diffs, citation tracking, revert detection, and template analysis. @@ -35,7 +87,9 @@ import { SectionDiffer, CitationTracker, RevertDetector, TemplateAnalyzer } from All L1 analyzers share a common interface — receive a pair of revisions and produce an array of `EvidenceEvent`. -## `@var-ia/interpreter` +Key exports: `SectionDiffer`, `CitationTracker`, `RevertDetector`, `TemplateAnalyzer` + +### `@var-ia/interpreter` Pluggable model adapter interface for L2. @@ -44,11 +98,13 @@ import type { ModelAdapter } from "@var-ia/interpreter"; import { createOpenAIAdapter } from "@var-ia/interpreter/openai"; ``` -## `@var-ia/cli` +Key exports: `ModelAdapter` (interface), `createOpenAIAdapter` + +### `@var-ia/cli` The `wikihistory` CLI tool. Combines ingestion, analysis, and output. -## `@var-ia/persistence` +### `@var-ia/persistence` SQLite storage adapter (uses `bun:sqlite`). @@ -57,6 +113,8 @@ import { EventStore } from "@var-ia/persistence"; const store = new EventStore("varia.db"); ``` -## `@var-ia/eval` +Key exports: `EventStore` + +### `@var-ia/eval` Evaluation harness for measuring analyzer and model accuracy against L3 ground truth. diff --git a/docs/tutorials/citation-churn.md b/docs/tutorials/citation-churn.md index 87dbbee..96b77e1 100644 --- a/docs/tutorials/citation-churn.md +++ b/docs/tutorials/citation-churn.md @@ -20,15 +20,48 @@ wikihistory claim --input ./covid-citations.json --type citation_added,citation_ ### 3. Track citation churn -TODO: Use `wikihistory export` to produce a churn report showing which citations appear, disappear, and reappear across revisions. +```bash +wikihistory export --input ./citations-only.json --format csv --output ./churn-report.csv +``` + +This produces a CSV with citation URLs, add/remove timestamps, and revision IDs — ready for spreadsheet analysis. ## Use case: identifying biased sourcing -TODO: Describe how citation churn patterns can reveal coordinated editing or shifts in cited evidence. +Citation churn patterns reveal source instability. A citation added in one edit and removed in the next signals contested sourcing. Citation replacement (journal swapped for a news article) indicates editors updating their evidence base. Churn concentrated on one page section may point to a content dispute. + +## Example output + +```json +{ + "eventId": "cite001a", + "eventType": "citation_added", + "fromRevisionId": 1280090010, + "toRevisionId": 1280090100, + "section": "Vaccine efficacy", + "before": "", + "after": "{{cite journal |last1=Smith |title=...}}", + "timestamp": "2024-11-20T10:00:00Z", + "layer": "observed" +} +``` + +```json +{ + "eventId": "cite001b", + "eventType": "citation_removed", + "fromRevisionId": 1280090100, + "toRevisionId": 1280090200, + "section": "Vaccine efficacy", + "before": "{{cite journal |last1=Smith |title=...}}", + "after": "", + "timestamp": "2024-11-21T14:30:00Z", + "layer": "observed" +} +``` -## TODO +## Notes -- Add churn analysis script or workflow -- Add example of a citation being added then removed in a subsequent edit -- Add section on grouping citations by source type (journal, news, government) -- Add note about the L1/L2 boundary — Varia tracks the citation change, not the quality of the source +- Use `--format csv` to produce spreadsheet-importable churn reports +- Group by source domain to see which types of sources churn most +- Varia tracks the citation change, not source quality — that's outside the boundary diff --git a/docs/tutorials/dispute-timeline.md b/docs/tutorials/dispute-timeline.md index 083e1c4..ee1b8a1 100644 --- a/docs/tutorials/dispute-timeline.md +++ b/docs/tutorials/dispute-timeline.md @@ -20,7 +20,11 @@ wikihistory claim --input ./gw-revisions.json --type revert_detected --output ./ ### 3. Cross-reference with talk page -TODO: Fetch talk page revision history and correlate revert clusters with discussion periods. +```bash +wikihistory analyze --page "Talk:Global_warming" --wiki "en.wikipedia.org" --limit 100 --output ./gw-talk.json +``` + +Talk pages are fetched by prefixing the page title with `Talk:`. Correlate the revert timestamps from step 2 with talk page revision timestamps to identify dispute intensity. ### 4. Build the timeline @@ -30,11 +34,42 @@ wikihistory visualize --input ./gw-reverts.json --output ./gw-dispute-timeline.h ## Use case: editorial dispute analysis -TODO: Describe how revert frequency, participant diversity, and talk page activity together indicate dispute intensity. +Revert clusters combined with talk page activity indicate dispute intensity. High revert frequency with little talk page discussion suggests unilateral editing or edit-warring. Reverts accompanied by long talk threads indicate active deliberation. Varia's deterministic events let you quantify both dimensions from the same L1 pipeline. + +## Example output + +```json +{ + "eventId": "revert001a", + "eventType": "revert_detected", + "fromRevisionId": 1280100100, + "toRevisionId": 1280100200, + "section": "Scientific consensus", + "before": "The scientific consensus is settled", + "after": "The scientific consensus is disputed", + "revertedBy": "UserA", + "timestamp": "2024-11-18T09:00:00Z", + "layer": "observed" +} +``` + +```json +{ + "eventId": "revert001b", + "eventType": "revert_detected", + "fromRevisionId": 1280100200, + "toRevisionId": 1280100300, + "section": "Scientific consensus", + "before": "The scientific consensus is disputed", + "after": "The scientific consensus is settled", + "revertedBy": "UserB", + "timestamp": "2024-11-18T09:30:00Z", + "layer": "observed" +} +``` -## TODO +## Notes -- Add talk page ingestion workflow -- Add correlation analysis between reverts and talk page threads -- Add example output showing a dispute timeline with annotated revert clusters -- Add section on privacy considerations (usernames in output) +- Talk pages use the same API; prefix the page title with `Talk:` +- Consider rate limits when fetching both article and talk revisions +- Usernames in output should be handled per your privacy policy diff --git a/docs/tutorials/fandom-canon.md b/docs/tutorials/fandom-canon.md index 33caaac..67d868f 100644 --- a/docs/tutorials/fandom-canon.md +++ b/docs/tutorials/fandom-canon.md @@ -32,10 +32,26 @@ wikihistory watch --page "Darth_Vader" --wiki "starwars.fandom.com" --interval 3 ## Use case: preserving canonicity -TODO: Describe how to monitor canon disputes over time, compare claim stability across different Fandom communities. +Fandom canon pages drift as new media releases or retcons earlier material. Character backstories, power levels, timelines, and faction alignments are frequently updated. Varia tracks these changes as `claim_added`, `claim_modified`, and `claim_removed` events, letting you see exactly which lore-critical statement changed, when, and by whom. Compare claim stability across pages to see which characters or settings have the most contested canon. + +## Example output + +```json +{ + "eventId": "canon001a", + "eventType": "claim_added", + "fromRevisionId": 1280012345, + "toRevisionId": 1280016789, + "section": "Powers and abilities", + "before": "", + "after": "Darth Vader's midichlorian count is over 20,000", + "timestamp": "2024-12-01T08:15:00Z", + "layer": "observed" +} +``` -## TODO +## Notes -- Add example output for a canon-relevant event -- Add note about Fandom API differences from Wikimedia -- Add section on multi-page monitoring (tracking a topic across pages) +- Fandom uses the same MediaWiki API as Wikipedia — `--wiki starwars.fandom.com` works +- Fandom wiki APIs may have different rate limits +- For multi-page monitoring, use a config file with `wikihistory cron` diff --git a/docs/tutorials/wikipedia-history.md b/docs/tutorials/wikipedia-history.md index 6329a1c..5a887ac 100644 --- a/docs/tutorials/wikipedia-history.md +++ b/docs/tutorials/wikipedia-history.md @@ -42,9 +42,41 @@ wikihistory visualize --input ./earth-events.json --output ./earth-timeline.html - [Monitor citation churn on medical pages](citation-churn.md) - [Build a dispute timeline](dispute-timeline.md) -## TODO +## Example output + +`wikihistory analyze` produces a stream of events. Here are two example events from the Earth page: + +```json +{ + "eventId": "wiki001a", + "eventType": "section_added", + "fromRevisionId": 1280110001, + "toRevisionId": 1280110100, + "section": "Geology", + "before": "", + "after": "== Geology ==\nEarth's crust consists of tectonic plates...", + "timestamp": "2024-11-25T12:00:00Z", + "layer": "observed" +} +``` + +```json +{ + "eventId": "wiki001b", + "eventType": "citation_added", + "fromRevisionId": 1280110100, + "toRevisionId": 1280110200, + "section": "Atmosphere", + "before": "", + "after": "{{cite web |title=Earth Fact Sheet |url=...}}", + "timestamp": "2024-11-25T14:00:00Z", + "layer": "observed" +} +``` + +## Troubleshooting -- Add expected output examples for each step -- Add section on filtering events by type -- Add section on configuring wiki source (other language Wikipedias) -- Add troubleshooting tips for rate limits +- **Rate limits**: add `--delay 1000` between requests +- **Large pages**: start with `--limit 5` to test +- **Other wikis**: use `--wiki de.wikipedia.org` for German Wikipedia +- **Output too large**: use `--format ndjson` for line-delimited output From 3012f11e4c8d3300468143a965d299b65be93cfc Mon Sep 17 00:00:00 2001 From: Kanav Jain Date: Thu, 14 May 2026 14:51:46 -0500 Subject: [PATCH 008/183] docs: rewrite CLI reference and tutorials to match actual wikihistory interface - Updated cli.md with real positional args, flags, and option tables - Rewrote 4 tutorials with correct command syntax and EvidenceEvent schema - Removed all fictional --page, --input, --output, --limit, --wiki flags --- docs/cli.md | 139 ++++++++++++++++++++++------ docs/tutorials/citation-churn.md | 48 +++++++--- docs/tutorials/dispute-timeline.md | 58 ++++++++---- docs/tutorials/fandom-canon.md | 46 ++++++--- docs/tutorials/wikipedia-history.md | 66 +++++++++---- 5 files changed, 267 insertions(+), 90 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index 1193376..5c60d30 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -2,75 +2,158 @@ ## `wikihistory analyze` -Fetch revisions and run all analyzers. +Analyze full edit history of a page. ```bash -wikihistory analyze --page [options] +wikihistory analyze [page] [options] ``` -**Options:** | Flag | Default | Description | |---|---|---| -| `--page` | required | Page title | -| `--wiki` | `en.wikipedia.org` | Wiki domain | -| `--limit` | `10` | Number of revisions | -| `--output` | stdout | Output file path | -| `--format` | `json` | Output format (json, ndjson) | +| `page` | required (positional) | Page title | +| `-d, --depth ` | `detailed` | Analysis depth: `brief`, `detailed`, `forensic` | +| `--from ` | — | Start revision ID | +| `--to ` | — | End revision ID | +| `--since ` | — | Re-observe from ISO timestamp | +| `-c, --cache` | off | Cache revisions in SQLite (`~/.wikihistory/varia.db`) | +| `--pages-file ` | — | Batch file of page titles (one per line) | +| `--router` | off | Use local open-weight models via Ollama | +| `-m, --model

` | — | Model provider: `openai`, `anthropic`, `deepseek`, `local`, `byok` | +| `--api ` | `en.wikipedia.org` | MediaWiki API base URL | +| `--cache-dir ` | `~/.wikihistory` | Cache directory path | ## `wikihistory claim` -Extract claim-level events from analysis output. +Track a specific claim across revisions. ```bash -wikihistory claim --input --output +wikihistory claim [options] ``` -## `wikihistory export` +| Flag | Default | Description | +|---|---|---| +| `page` | required (positional) | Page title | +| `-t, --text ` | required | Claim text to track | +| `-c, --cache` | off | Cache revisions in SQLite | +| `-m, --model

` | — | Model provider | +| `--api ` | `en.wikipedia.org` | MediaWiki API base URL | + +## `wikihistory cron` -Export event data to a portable format. +One-shot re-observation for cron. ```bash -wikihistory export --input --format --output +wikihistory cron [options] ``` -**Formats:** `json`, `csv`, `sqlite` +| Flag | Default | Description | +|---|---|---| +| `pages-file` | required (positional) | Batch file of page titles (one per line) | +| `-i, --interval ` | from last observation | Lookback window in hours | +| `--notify-slack` | off | Send Slack notification on changes | +| `--notify-email` | off | Send email notification on changes | +| `--notify-webhook ` | — | Send generic webhook POST on changes | +| `--api ` | `en.wikipedia.org` | MediaWiki API base URL | -## `wikihistory watch` +## `wikihistory diff` -Watch a page for new revisions in real time. +Cross-wiki comparison (2+ wikis). ```bash -wikihistory watch --page --interval +wikihistory diff [options] ``` -## `wikihistory cron` +| Flag | Default | Description | +|---|---|---| +| `topic` | required (positional) | Page title to compare across wikis | +| `--wiki-a ` | required | First wiki API URL | +| `--wiki-b ` | required | Second wiki API URL | +| `--wiki-c ` | optional | Third wiki API URL | +| `-d, --depth ` | `detailed` | Analysis depth | +| `-m, --model

` | — | Model provider | + +## `wikihistory eval` + +Run the evaluation harness. + +```bash +wikihistory eval [options] +``` -Schedule recurring analysis of a page set. +| Flag | Default | Description | +|---|---|---| +| `--page ` | — | Run only for a specific page | +| `--ground-truth <p\|b>` | — | Validate against L3 ground truth | +| `--l2` | off | Run L2 quality benchmarks | + +## `wikihistory export` + +Export analysis as structured data. ```bash -wikihistory cron --config <cronfile> --db <sqlite-path> +wikihistory export <page> [options] ``` +| Flag | Default | Description | +|---|---|---| +| `page` | required (positional) | Page title | +| `-f, --format <fmt>` | `json` | Output format: `json`, `csv`, `ndjson` | +| `--bundle` | off | Export as signed evidence bundle (SHA-256) | +| `--manifest` | off | Export as replay manifest with all hashes | +| `-m, --model <p>` | — | Model provider | +| `--api <url>` | `en.wikipedia.org` | MediaWiki API base URL | + ## `wikihistory visualize` -Render a timeline of events. +Export evidence graph as a diagram. ```bash -wikihistory visualize --input <file> --output <file> +wikihistory visualize <page> [options] ``` -## `wikihistory diff` +| Flag | Default | Description | +|---|---|---| +| `page` | required (positional) | Page title | +| `-f, --format <fmt>` | `mermaid` | Output format: `mermaid`, `dot` | +| `--all` | off | Show all event types (default: claim events only) | +| `--api <url>` | `en.wikipedia.org` | MediaWiki API base URL | -Compare two analysis snapshots. +## `wikihistory watch` + +Live polling daemon for new edits. ```bash -wikihistory diff --before <file> --after <file> +wikihistory watch <page> [options] ``` -## `wikihistory eval` +| Flag | Default | Description | +|---|---|---| +| `page` | required (positional) | Page title | +| `-s, --section <name>` | — | Watch a specific section only | +| `-i, --interval <ms>` | `60000` | Poll interval in milliseconds | +| `--api <url>` | `en.wikipedia.org` | MediaWiki API base URL | -Run the evaluation harness against ground truth. +## `wikihistory mcp` + +Start MCP server for AI agent integration. ```bash -wikihistory eval --input <events> --ground-truth <file> +wikihistory mcp ``` + +## Global options + +These options apply to most commands: + +| Flag | Description | +|---|---| +| `--api <url>` | MediaWiki API base URL (default: `en.wikipedia.org`) | +| `--cache-dir <path>` | Cache directory path (default: `~/.wikihistory`) | +| `--api-key <token>` | API key for bearer token auth | +| `--api-user <user>` | Username for basic auth | +| `--api-password <pass>` | Password for basic auth | +| `--model-api-key <key>` | API key for model provider | +| `--model-name <name>` | Model name override | +| `--model-endpoint <url>` | API endpoint override | +| `--temperature <n>` | Model temperature (default: 0.1) | +| `--prompt <text>` | Override system prompt for interpretation | diff --git a/docs/tutorials/citation-churn.md b/docs/tutorials/citation-churn.md index 96b77e1..bf3c308 100644 --- a/docs/tutorials/citation-churn.md +++ b/docs/tutorials/citation-churn.md @@ -9,23 +9,23 @@ Track citation additions and removals on a high-traffic medical Wikipedia page t ### 1. Analyze a medical page ```bash -wikihistory analyze --page "COVID-19" --limit 100 --output ./covid-citations.json +wikihistory analyze "COVID-19" --depth detailed -c ``` -### 2. Filter citation events +### 2. Export citation data as CSV ```bash -wikihistory claim --input ./covid-citations.json --type citation_added,citation_removed --output ./citations-only.json +wikihistory export "COVID-19" --format csv ``` -### 3. Track citation churn +This produces a CSV with citation URLs, add/remove timestamps, and revision IDs — ready for spreadsheet analysis. + +### 3. Pipe to file ```bash -wikihistory export --input ./citations-only.json --format csv --output ./churn-report.csv +wikihistory export "COVID-19" --format csv > churn-report.csv ``` -This produces a CSV with citation URLs, add/remove timestamps, and revision IDs — ready for spreadsheet analysis. - ## Use case: identifying biased sourcing Citation churn patterns reveal source instability. A citation added in one edit and removed in the next signals contested sourcing. Citation replacement (journal swapped for a news article) indicates editors updating their evidence base. Churn concentrated on one page section may point to a content dispute. @@ -34,7 +34,7 @@ Citation churn patterns reveal source instability. A citation added in one edit ```json { - "eventId": "cite001a", + "eventId": "d1e3f5a7b9c2048a", "eventType": "citation_added", "fromRevisionId": 1280090010, "toRevisionId": 1280090100, @@ -42,13 +42,23 @@ Citation churn patterns reveal source instability. A citation added in one edit "before": "", "after": "<ref>{{cite journal |last1=Smith |title=...}}</ref>", "timestamp": "2024-11-20T10:00:00Z", - "layer": "observed" + "layer": "observed", + "deterministicFacts": [ + { + "fact": "Citation added in section Vaccine efficacy", + "provenance": { + "analyzer": "citation-tracker", + "version": "0.3.1", + "inputHashes": [] + } + } + ] } ``` ```json { - "eventId": "cite001b", + "eventId": "2b4d6f8a0c1e3059", "eventType": "citation_removed", "fromRevisionId": 1280090100, "toRevisionId": 1280090200, @@ -56,12 +66,22 @@ Citation churn patterns reveal source instability. A citation added in one edit "before": "<ref>{{cite journal |last1=Smith |title=...}}</ref>", "after": "", "timestamp": "2024-11-21T14:30:00Z", - "layer": "observed" + "layer": "observed", + "deterministicFacts": [ + { + "fact": "Citation removed in section Vaccine efficacy", + "provenance": { + "analyzer": "citation-tracker", + "version": "0.3.1", + "inputHashes": [] + } + } + ] } ``` ## Notes -- Use `--format csv` to produce spreadsheet-importable churn reports -- Group by source domain to see which types of sources churn most -- Varia tracks the citation change, not source quality — that's outside the boundary +- Use `--format csv` or `--format ndjson` on `wikihistory export` for spreadsheet-importable churn reports. +- Group by source domain to see which types of sources churn most. +- Varia tracks the citation change, not source quality — that's outside the boundary. diff --git a/docs/tutorials/dispute-timeline.md b/docs/tutorials/dispute-timeline.md index ee1b8a1..b3dc50b 100644 --- a/docs/tutorials/dispute-timeline.md +++ b/docs/tutorials/dispute-timeline.md @@ -9,29 +9,31 @@ Combine article revision history with talk page activity to build a timeline of ### 1. Analyze the article ```bash -wikihistory analyze --page "Global_warming" --limit 100 --output ./gw-revisions.json +wikihistory analyze "Global_warming" --depth detailed -c ``` -### 2. Get revert events +### 2. Analyze the talk page + +Talk pages are fetched by prefixing the page title with `Talk:`: ```bash -wikihistory claim --input ./gw-revisions.json --type revert_detected --output ./gw-reverts.json +wikihistory analyze "Talk:Global_warming" --depth detailed -c ``` -### 3. Cross-reference with talk page +### 3. Visualize the combined timeline ```bash -wikihistory analyze --page "Talk:Global_warming" --wiki "en.wikipedia.org" --limit 100 --output ./gw-talk.json +wikihistory visualize "Global_warming" --format mermaid --all ``` -Talk pages are fetched by prefixing the page title with `Talk:`. Correlate the revert timestamps from step 2 with talk page revision timestamps to identify dispute intensity. - -### 4. Build the timeline +### 4. Export for further analysis ```bash -wikihistory visualize --input ./gw-reverts.json --output ./gw-dispute-timeline.html +wikihistory export "Global_warming" --format ndjson > global-warming-events.ndjson ``` +Correlate revert timestamps with talk page revision timestamps to identify dispute intensity. + ## Use case: editorial dispute analysis Revert clusters combined with talk page activity indicate dispute intensity. High revert frequency with little talk page discussion suggests unilateral editing or edit-warring. Reverts accompanied by long talk threads indicate active deliberation. Varia's deterministic events let you quantify both dimensions from the same L1 pipeline. @@ -40,36 +42,56 @@ Revert clusters combined with talk page activity indicate dispute intensity. Hig ```json { - "eventId": "revert001a", + "eventId": "3c5e7f9a1b2d4068", "eventType": "revert_detected", "fromRevisionId": 1280100100, "toRevisionId": 1280100200, "section": "Scientific consensus", "before": "The scientific consensus is settled", "after": "The scientific consensus is disputed", - "revertedBy": "UserA", "timestamp": "2024-11-18T09:00:00Z", - "layer": "observed" + "layer": "observed", + "deterministicFacts": [ + { + "fact": "Edit reverted in section Scientific consensus", + "detail": "Content replaced with previous version", + "provenance": { + "analyzer": "revert-detector", + "version": "0.3.1", + "inputHashes": [] + } + } + ] } ``` ```json { - "eventId": "revert001b", + "eventId": "4d6f8a0b2c3e5179", "eventType": "revert_detected", "fromRevisionId": 1280100200, "toRevisionId": 1280100300, "section": "Scientific consensus", "before": "The scientific consensus is disputed", "after": "The scientific consensus is settled", - "revertedBy": "UserB", "timestamp": "2024-11-18T09:30:00Z", - "layer": "observed" + "layer": "observed", + "deterministicFacts": [ + { + "fact": "Edit reverted in section Scientific consensus", + "detail": "Content replaced with previous version", + "provenance": { + "analyzer": "revert-detector", + "version": "0.3.1", + "inputHashes": [] + } + } + ] } ``` ## Notes -- Talk pages use the same API; prefix the page title with `Talk:` -- Consider rate limits when fetching both article and talk revisions -- Usernames in output should be handled per your privacy policy +- Talk pages use the standard MediaWiki API; prefix the page title with `Talk:`. +- Consider caching with `-c` when fetching both article and talk revisions. +- Varia does not expose editor usernames in event output — the `revert_detected` event does not include a `revertedBy` field. If you need contributor attribution, use the MediaWiki API directly. diff --git a/docs/tutorials/fandom-canon.md b/docs/tutorials/fandom-canon.md index 67d868f..2854b87 100644 --- a/docs/tutorials/fandom-canon.md +++ b/docs/tutorials/fandom-canon.md @@ -8,50 +8,68 @@ Monitor how a Fandom wiki's canon pages change over time, tracking additions and ### 1. Configure a Fandom wiki +Fandom wikis use the same MediaWiki API. Point `--api` at the wiki's API endpoint: + +```bash +wikihistory analyze "Darth_Vader" --api https://starwars.fandom.com/api.php --depth detailed +``` + +### 2. Run analyzers with caching + ```bash -wikihistory analyze --page "Darth_Vader" --wiki "starwars.fandom.com" --limit 20 +wikihistory analyze "Darth_Vader" --api https://starwars.fandom.com/api.php --depth detailed -c ``` -### 2. Run analyzers +### 3. Export results ```bash -wikihistory analyze --page "Darth_Vader" --wiki "starwars.fandom.com" --limit 50 --output ./canon-events.json +wikihistory export "Darth_Vader" --api https://starwars.fandom.com/api.php --format json ``` -### 3. Examine claim changes +### 4. Track a specific claim ```bash -wikihistory claim --input ./canon-events.json --output ./canon-claims.json +wikihistory claim "Darth_Vader" --text "midichlorian count is over 20,000" --api https://starwars.fandom.com/api.php -c ``` -### 4. Watch for new edits +### 5. Watch for new edits ```bash -wikihistory watch --page "Darth_Vader" --wiki "starwars.fandom.com" --interval 300 +wikihistory watch "Darth_Vader" --api https://starwars.fandom.com/api.php --interval 60000 ``` ## Use case: preserving canonicity -Fandom canon pages drift as new media releases or retcons earlier material. Character backstories, power levels, timelines, and faction alignments are frequently updated. Varia tracks these changes as `claim_added`, `claim_modified`, and `claim_removed` events, letting you see exactly which lore-critical statement changed, when, and by whom. Compare claim stability across pages to see which characters or settings have the most contested canon. +Fandom canon pages drift as new media releases or retcons earlier material. Character backstories, power levels, timelines, and faction alignments are frequently updated. Varia tracks these changes as `claim_first_seen`, `claim_removed`, `claim_reworded`, `claim_softened`, and `claim_strengthened` events, letting you see exactly which lore-critical statement changed and when. Compare claim stability across pages to see which characters or settings have the most contested canon. ## Example output ```json { - "eventId": "canon001a", - "eventType": "claim_added", + "eventId": "f7a2d0e3c1b84906", + "eventType": "claim_first_seen", "fromRevisionId": 1280012345, "toRevisionId": 1280016789, "section": "Powers and abilities", "before": "", "after": "Darth Vader's midichlorian count is over 20,000", "timestamp": "2024-12-01T08:15:00Z", - "layer": "observed" + "layer": "observed", + "deterministicFacts": [ + { + "fact": "New claim first appeared: Darth Vader's midichlorian count is over 20,000", + "provenance": { + "analyzer": "claim-differ", + "version": "0.3.1", + "inputHashes": [] + } + } + ] } ``` ## Notes -- Fandom uses the same MediaWiki API as Wikipedia — `--wiki starwars.fandom.com` works -- Fandom wiki APIs may have different rate limits -- For multi-page monitoring, use a config file with `wikihistory cron` +- Fandom wikis use the standard MediaWiki API — pass the API URL with `--api`. +- Fandom wiki APIs may have different rate limits; use `-c` to cache revisions. +- For multi-page monitoring, use a pages file with `wikihistory cron`. diff --git a/docs/tutorials/wikipedia-history.md b/docs/tutorials/wikipedia-history.md index 5a887ac..a09096f 100644 --- a/docs/tutorials/wikipedia-history.md +++ b/docs/tutorials/wikipedia-history.md @@ -2,7 +2,7 @@ ## Goal -Use Varia to analyze the revision history of a Wikipedia page and see what changed over the last 50 edits. +Use Varia to analyze the revision history of a Wikipedia page and see what changed. ## Steps @@ -15,25 +15,39 @@ bun add -g @var-ia/cli ### 2. Analyze ```bash -wikihistory analyze --page "Earth" --limit 50 --output ./earth-events.json +wikihistory analyze "Earth" --depth detailed -c +``` + +With a model for interpretations: + +```bash +wikihistory analyze "Earth" --depth detailed -c -m openai ``` ### 3. View results +Events print to stdout. For structured export: + ```bash -cat ./earth-events.json | bunx json +wikihistory export "Earth" ``` ### 4. Understand the events -- **Section events** — sections were added, removed, or renamed -- **Citation events** — citations were added or removed -- **Revert events** — someone reverted an edit +- **Section events** — sections were reorganized (`section_reorganized`) +- **Citation events** — citations were added, removed, or replaced (`citation_added`, `citation_removed`, `citation_replaced`) +- **Revert events** — someone reverted an edit (`revert_detected`) ### 5. Visualize ```bash -wikihistory visualize --input ./earth-events.json --output ./earth-timeline.html +wikihistory visualize "Earth" --format mermaid +``` + +To see all event types (not just claim events): + +```bash +wikihistory visualize "Earth" --format mermaid --all ``` ## Next steps @@ -48,21 +62,31 @@ wikihistory visualize --input ./earth-events.json --output ./earth-timeline.html ```json { - "eventId": "wiki001a", - "eventType": "section_added", + "eventId": "a3f5c2e1b7d409fa", + "eventType": "section_reorganized", "fromRevisionId": 1280110001, "toRevisionId": 1280110100, "section": "Geology", "before": "", "after": "== Geology ==\nEarth's crust consists of tectonic plates...", "timestamp": "2024-11-25T12:00:00Z", - "layer": "observed" + "layer": "observed", + "deterministicFacts": [ + { + "fact": "Section Geology added with 3 paragraphs", + "provenance": { + "analyzer": "section-differ", + "version": "0.3.1", + "inputHashes": [] + } + } + ] } ``` ```json { - "eventId": "wiki001b", + "eventId": "8e12b4f1a6d3c097", "eventType": "citation_added", "fromRevisionId": 1280110100, "toRevisionId": 1280110200, @@ -70,13 +94,23 @@ wikihistory visualize --input ./earth-events.json --output ./earth-timeline.html "before": "", "after": "<ref>{{cite web |title=Earth Fact Sheet |url=...}}</ref>", "timestamp": "2024-11-25T14:00:00Z", - "layer": "observed" + "layer": "observed", + "deterministicFacts": [ + { + "fact": "Citation added in section Atmosphere", + "provenance": { + "analyzer": "citation-tracker", + "version": "0.3.1", + "inputHashes": [] + } + } + ] } ``` ## Troubleshooting -- **Rate limits**: add `--delay 1000` between requests -- **Large pages**: start with `--limit 5` to test -- **Other wikis**: use `--wiki de.wikipedia.org` for German Wikipedia -- **Output too large**: use `--format ndjson` for line-delimited output +- **Rate limits**: Use `-c` to cache revisions and avoid re-fetching. +- **Large pages**: Analyze a range with `--from` and `--to` revision IDs. +- **Other wikis**: Use `--api` to point to a different MediaWiki API (e.g., `--api https://de.wikipedia.org/w/api.php`). +- **Output too large**: Use `wikihistory export "Earth" --format ndjson` for line-delimited output. From be709267fa6eebeb4a418ea1f2824a80d9c0c4b6 Mon Sep 17 00:00:00 2001 From: Kanav Jain <kanavj@gmail.com> Date: Thu, 14 May 2026 14:51:57 -0500 Subject: [PATCH 009/183] fix: tighten types to match evidence-graph schema, wire dead components to DOM - Added FactProvenance, DeterministicFact, PolicyDimension, DiscussionType types - Wired 6 unused components (SourceChangeTable, WordingDiffCard, RevertClusterDisplay, EditVelocityChart, TalkPageTimeline, SchemaViewer) to DOM containers - Added dashboard-row2 grid layout and component styles --- README.md | 4 ++++ index.html | 6 ++++++ src/main.ts | 30 ++++++++++++++++++++++++++++++ src/styles/main.css | 33 +++++++++++++++++++++++++++++++++ src/types.ts | 39 ++++++++++++++++++++++++++++++++++++--- 5 files changed, 109 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9213a19..3f4a201 100644 --- a/README.md +++ b/README.md @@ -18,3 +18,7 @@ Open `http://localhost:5173`. Sample data loads automatically. Drag a JSONL file - Vite + vanilla TypeScript - No framework dependencies - DOM-based rendering with CSS custom properties + +--- + +Built and maintained by [NextConsensus](https://nextconsensus.com). diff --git a/index.html b/index.html index 0338557..f318007 100644 --- a/index.html +++ b/index.html @@ -23,6 +23,12 @@ <h1>Varia</h1> <div id="panel-citations" class="panel"></div> <div id="panel-certainty" class="panel"></div> </div> + <div id="dashboard-row2"> + <div id="panel-disputes" class="panel"></div> + <div id="panel-wording" class="panel"></div> + <div id="panel-source" class="panel"></div> + <div id="panel-schema" class="panel"></div> + </div> </div> <script type="module" src="/src/main.ts"></script> </body> diff --git a/src/main.ts b/src/main.ts index ad2dc12..c556ea2 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,6 +6,12 @@ import { CitationChurnChart } from "./citations/CitationChurnChart.js"; import { CertaintyTimeline } from "./language/CertaintyTimeline.js"; import { ImportUpload } from "./export/ImportUpload.js"; import { TimelineExport } from "./export/TimelineExport.js"; +import { SchemaViewer } from "./export/SchemaViewer.js"; +import { SourceChangeTable } from "./citations/SourceChangeTable.js"; +import { WordingDiffCard } from "./language/WordingDiffCard.js"; +import { RevertClusterDisplay } from "./disputes/RevertClusterDisplay.js"; +import { EditVelocityChart } from "./disputes/EditVelocityChart.js"; +import { TalkPageTimeline } from "./disputes/TalkPageTimeline.js"; import "./styles/main.css"; let allEvents: EvidenceEvent[] = []; @@ -22,6 +28,24 @@ const diffView = new DiffView(document.getElementById("panel-diff")!); const citationChart = new CitationChurnChart(document.getElementById("panel-citations")!); const certaintyTimeline = new CertaintyTimeline(document.getElementById("panel-certainty")!); +const schemaViewer = new SchemaViewer(document.getElementById("panel-schema")!); +const sourceChangeTable = new SourceChangeTable(document.getElementById("panel-source")!); +const wordingDiffCard = new WordingDiffCard(document.getElementById("panel-wording")!); + +const disputesContainer = document.getElementById("panel-disputes")!; +const revertSection = document.createElement("div"); +const velocitySection = document.createElement("div"); +const talkSection = document.createElement("div"); +velocitySection.style.marginTop = "1rem"; +talkSection.style.marginTop = "1rem"; +disputesContainer.appendChild(revertSection); +disputesContainer.appendChild(velocitySection); +disputesContainer.appendChild(talkSection); + +const revertClusterDisplay = new RevertClusterDisplay(revertSection); +const editVelocityChart = new EditVelocityChart(velocitySection); +const talkPageTimeline = new TalkPageTimeline(talkSection); + const filter = new EventFilter({ container: document.getElementById("filter-area")!, allTypes: [], @@ -68,6 +92,12 @@ function updateAll(events: EvidenceEvent[]): void { timelineView.setData(events); citationChart.setData(events); certaintyTimeline.setData(events); + schemaViewer.setData(events as unknown as Record<string, unknown>[]); + sourceChangeTable.setData(events); + wordingDiffCard.setData(events); + revertClusterDisplay.setData(events); + editVelocityChart.setData(events); + talkPageTimeline.setData(events); } function applyFilters(activeTypes: Set<EventType>): void { diff --git a/src/styles/main.css b/src/styles/main.css index a9efd57..1501876 100644 --- a/src/styles/main.css +++ b/src/styles/main.css @@ -117,6 +117,30 @@ body { grid-row: 3 / 4; } +#dashboard-row2 { + display: grid; + grid-template-columns: 1fr 1fr 1fr 1fr; + gap: 1rem; + margin-top: 1rem; +} + +#panel-disputes { + grid-column: 1 / 3; +} + +#panel-wording { + grid-column: 3 / 4; +} + +#panel-source { + grid-column: 4 / 5; +} + +#panel-schema { + grid-column: 1 / 5; + grid-row: 2 / 3; +} + @media (max-width: 900px) { #dashboard { grid-template-columns: 1fr; @@ -139,6 +163,15 @@ body { #panel-certainty { grid-row: auto; } + #dashboard-row2 { + grid-template-columns: 1fr; + } + #panel-disputes, + #panel-wording, + #panel-source, + #panel-schema { + grid-column: 1 / 2; + } } /* --- Buttons & Inputs --- */ diff --git a/src/types.ts b/src/types.ts index 5294a4f..dc51b19 100644 --- a/src/types.ts +++ b/src/types.ts @@ -29,11 +29,44 @@ export type EventType = | "talk_reply_added" | "template_parameter_changed"; +export interface FactProvenance { + analyzer: string; + version: string; + inputHashes: string[]; +} + +export interface DeterministicFact { + fact: string; + detail?: string; + provenance?: FactProvenance; +} + +export type PolicyDimension = + | "verifiability" + | "npov" + | "blp" + | "due_weight" + | "protection" + | "edit_warring" + | "notability" + | "copyright" + | "civility"; + +export type DiscussionType = + | "notability_challenge" + | "sourcing_dispute" + | "neutrality_concern" + | "content_deletion" + | "content_addition" + | "naming_dispute" + | "procedural" + | "other"; + export interface ModelInterpretation { semanticChange: string; confidence: number; - policyDimension?: string; - discussionType?: string; + policyDimension?: PolicyDimension; + discussionType?: DiscussionType; } export interface EvidenceEvent { @@ -45,7 +78,7 @@ export interface EvidenceEvent { section: string; before: string; after: string; - deterministicFacts: { fact: string; detail?: string }[]; + deterministicFacts: DeterministicFact[]; modelInterpretation?: ModelInterpretation; layer: EvidenceLayer; timestamp: string; From 963c9d7f1971ba427175f6c75a180ec3e91a5e2a Mon Sep 17 00:00:00 2001 From: Kanav Jain <kanavj@gmail.com> Date: Thu, 14 May 2026 15:40:55 -0500 Subject: [PATCH 010/183] ci: add GitHub Pages deployment workflow - Set Vite base to /varia-ui/ for Pages asset paths - Added GitHub Actions workflow for build + deploy on push to main --- .github/workflows/deploy.yml | 32 ++++++++++++++++++++++++++++++++ vite.config.ts | 1 + 2 files changed, 33 insertions(+) create mode 100644 .github/workflows/deploy.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..e150e94 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,32 @@ +name: Deploy to GitHub Pages + +on: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - run: bun install + - run: bun run build + - uses: actions/configure-pages@v5 + - uses: actions/upload-pages-artifact@v3 + with: + path: dist + - id: deploy + uses: actions/deploy-pages@v4 diff --git a/vite.config.ts b/vite.config.ts index 9221db8..1011221 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,6 +2,7 @@ import { defineConfig } from "vite"; export default defineConfig({ root: ".", + base: "/varia-ui/", build: { outDir: "dist", }, From 071bdb5ad175464eea868eb45ac0e01572d998bc Mon Sep 17 00:00:00 2001 From: Kanav Jain <kanavj@gmail.com> Date: Thu, 14 May 2026 15:46:36 -0500 Subject: [PATCH 011/183] revert: remove GitHub Pages deployment --- .github/workflows/deploy.yml | 32 -------------------------------- vite.config.ts | 1 - 2 files changed, 33 deletions(-) delete mode 100644 .github/workflows/deploy.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml deleted file mode 100644 index e150e94..0000000 --- a/.github/workflows/deploy.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Deploy to GitHub Pages - -on: - push: - branches: [main] - workflow_dispatch: - -permissions: - contents: read - pages: write - id-token: write - -concurrency: - group: pages - cancel-in-progress: false - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: oven-sh/setup-bun@v2 - with: - bun-version: latest - - run: bun install - - run: bun run build - - uses: actions/configure-pages@v5 - - uses: actions/upload-pages-artifact@v3 - with: - path: dist - - id: deploy - uses: actions/deploy-pages@v4 diff --git a/vite.config.ts b/vite.config.ts index 1011221..9221db8 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,7 +2,6 @@ import { defineConfig } from "vite"; export default defineConfig({ root: ".", - base: "/varia-ui/", build: { outDir: "dist", }, From d9c0d2b94bf19fd7d85b11f061913592869bdf04 Mon Sep 17 00:00:00 2001 From: Kanav Jain <kanavj@gmail.com> Date: Thu, 14 May 2026 15:50:35 -0500 Subject: [PATCH 012/183] feat: static docs site with markdown build and GitHub Pages deploy --- .github/workflows/deploy.yml | 33 ++++ .gitignore | 2 + assets/style.css | 252 ++++++++++++++++++++++++++++ build.mjs | 178 ++++++++++++++++++++ docs/frontier-use-cases.md | 310 +++++++++++++++++++++++++++++++++++ docs/index.md | 1 + package.json | 12 ++ 7 files changed, 788 insertions(+) create mode 100644 .github/workflows/deploy.yml create mode 100644 .gitignore create mode 100644 assets/style.css create mode 100644 build.mjs create mode 100644 docs/frontier-use-cases.md create mode 100644 package.json diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..27e5a73 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,33 @@ +name: Deploy to GitHub Pages + +on: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci + - run: npm run build + - uses: actions/configure-pages@v5 + - uses: actions/upload-pages-artifact@v3 + with: + path: dist + - id: deploy + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b947077 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ diff --git a/assets/style.css b/assets/style.css new file mode 100644 index 0000000..e0dde86 --- /dev/null +++ b/assets/style.css @@ -0,0 +1,252 @@ +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + +:root { + --sidebar-w: 260px; + --bg: #fff; + --fg: #18181b; + --fg-muted: #52525b; + --sidebar-bg: #0f0d1e; + --sidebar-fg: #c7c7d8; + --sidebar-fg-muted: #7c7c9a; + --accent: #6366f1; + --accent-dim: #4f46e5; + --border: #e4e4e7; + --code-bg: #f4f4f5; + --code-fg: #18181b; + --radius: 6px; + --font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + --mono: 'SF Mono', 'Fira Code', 'Fira Mono', Menlo, Consolas, monospace; +} + +html { font-size: 16px; scroll-behavior: smooth; } +body { + font-family: var(--font); + color: var(--fg); + background: var(--bg); + line-height: 1.7; + -webkit-font-smoothing: antialiased; +} + +/* Layout */ +.layout { display: flex; min-height: 100vh; } + +/* Sidebar */ +.sidebar { + width: var(--sidebar-w); + min-width: var(--sidebar-w); + background: var(--sidebar-bg); + color: var(--sidebar-fg); + display: flex; + flex-direction: column; + position: fixed; + top: 0; + left: 0; + bottom: 0; + z-index: 40; + overflow-y: auto; + padding: 28px 24px 24px; +} + +.sidebar-header { margin-bottom: 32px; } +.brand { + font-size: 1.5rem; + font-weight: 700; + color: #fff; + text-decoration: none; + letter-spacing: -0.02em; +} +.brand:hover { color: var(--accent); } +.tagline { + font-size: 0.8125rem; + color: var(--sidebar-fg-muted); + margin-top: 6px; + line-height: 1.5; +} + +/* Navigation */ +.sidebar-nav { flex: 1; display: flex; flex-direction: column; gap: 2px; } + +.nav-link { + display: block; + padding: 6px 12px; + border-radius: var(--radius); + color: var(--sidebar-fg); + text-decoration: none; + font-size: 0.875rem; + transition: background 0.15s, color 0.15s; + margin: 0 -12px; +} +.nav-link:hover { background: rgba(255,255,255,0.06); color: #fff; } +.nav-link.active { + background: rgba(99,102,241,0.15); + color: var(--accent); + font-weight: 600; +} + +.nav-section { + font-size: 0.6875rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--sidebar-fg-muted); + padding: 16px 12px 4px; +} +.nav-section:first-child { padding-top: 0; } + +.nav-group { display: none; } +.nav-group.open { display: block; } +.nav-group .nav-link { padding-left: 24px; font-size: 0.8125rem; } + +/* Sidebar footer */ +.sidebar-footer { + margin-top: 24px; + padding-top: 20px; + border-top: 1px solid rgba(255,255,255,0.08); + display: flex; + flex-direction: column; + gap: 6px; +} +.sidebar-link { + color: var(--sidebar-fg-muted); + text-decoration: none; + font-size: 0.8125rem; + transition: color 0.15s; +} +.sidebar-link:hover { color: #fff; } + +/* Content area */ +.content { + margin-left: var(--sidebar-w); + flex: 1; + min-width: 0; + padding: 48px 56px 96px; + max-width: 780px; +} + +/* Mobile menu toggle */ +.menu-toggle { display: none; } +.menu-btn { + display: none; + flex-direction: column; + gap: 5px; + cursor: pointer; + padding: 8px; + margin-bottom: 16px; + width: 36px; +} +.menu-btn span { + display: block; + height: 2px; + background: var(--fg); + border-radius: 2px; + transition: transform 0.2s, opacity 0.2s; +} +.menu-overlay { display: none; } + +/* Typography */ +.content h1 { font-size: 2rem; font-weight: 700; letter-spacing: -0.03em; margin-bottom: 0.5em; line-height: 1.3; } +.content h2 { font-size: 1.375rem; font-weight: 600; margin: 2.5em 0 0.6em; padding-bottom: 0.3em; border-bottom: 1px solid var(--border); } +.content h3 { font-size: 1.125rem; font-weight: 600; margin: 1.8em 0 0.4em; } +.content h4 { font-size: 1rem; font-weight: 600; margin: 1.5em 0 0.3em; } + +.content p { margin: 0.8em 0; } +.content a { color: var(--accent-dim); text-decoration: none; } +.content a:hover { text-decoration: underline; } +.content strong { font-weight: 600; color: #09090b; } + +.content ul, .content ol { padding-left: 1.5em; margin: 0.8em 0; } +.content li { margin: 0.25em 0; } +.content li > p { margin: 0.2em 0; } + +.content blockquote { + border-left: 3px solid var(--accent); + margin: 1.2em 0; + padding: 0.5em 1em; + color: var(--fg-muted); + background: #fafafa; + border-radius: 0 var(--radius) var(--radius) 0; +} +.content blockquote p { margin: 0.3em 0; } + +.content hr { border: none; border-top: 1px solid var(--border); margin: 2em 0; } + +/* Code */ +.content code { + font-family: var(--mono); + font-size: 0.875em; + background: var(--code-bg); + color: var(--code-fg); + padding: 0.15em 0.4em; + border-radius: 4px; + font-weight: 500; +} + +.content pre { + background: #18181b; + color: #e4e4e7; + padding: 16px 20px; + border-radius: var(--radius); + overflow-x: auto; + margin: 1.2em 0; + font-size: 0.875rem; + line-height: 1.6; +} +.content pre code { + background: none; + color: inherit; + padding: 0; + font-size: inherit; + font-weight: 400; +} + +/* Tables */ +.content table { + width: 100%; + border-collapse: collapse; + margin: 1.2em 0; + font-size: 0.9375rem; +} +.content th { + text-align: left; + font-weight: 600; + padding: 8px 12px; + border-bottom: 2px solid var(--border); + color: var(--fg-muted); + font-size: 0.8125rem; + text-transform: uppercase; + letter-spacing: 0.05em; +} +.content td { + padding: 8px 12px; + border-bottom: 1px solid var(--border); + vertical-align: top; +} + +/* Images */ +.content img { max-width: 100%; border-radius: var(--radius); } + +/* Responsive */ +@media (max-width: 768px) { + :root { --sidebar-w: 240px; } + + .sidebar { + transform: translateX(-100%); + transition: transform 0.25s ease; + } + .menu-toggle:checked ~ .layout .sidebar { + transform: translateX(0); + } + .menu-toggle:checked ~ .layout .menu-overlay { + display: block; + position: fixed; + inset: 0; + background: rgba(0,0,0,0.4); + z-index: 30; + } + + .content { margin-left: 0; padding: 24px 20px 64px; max-width: 100%; } + .menu-btn { display: flex; } + + .content h1 { font-size: 1.625rem; } + .content h2 { font-size: 1.25rem; } +} diff --git a/build.mjs b/build.mjs new file mode 100644 index 0000000..09467bc --- /dev/null +++ b/build.mjs @@ -0,0 +1,178 @@ +import { readFile, writeFile, mkdir, readdir, rm, cp } from 'node:fs/promises'; +import { join, dirname, basename } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { marked } from 'marked'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const DOCS_DIR = join(__dirname, 'docs'); +const DIST_DIR = join(__dirname, 'dist'); +const ASSETS_DIR = join(__dirname, 'assets'); + +const NAV = [ + { title: 'Home', slug: 'index' }, + { title: 'Quick Start', slug: 'quickstart' }, + { title: 'Install', slug: 'install' }, + { title: 'Concepts', slug: 'concepts' }, + { title: 'Schema', slug: 'schema' }, + { title: 'CLI', slug: 'cli' }, + { title: 'SDK', slug: 'sdk' }, + { title: 'Boundary', slug: 'boundary' }, + { title: 'Frontier Use Cases', slug: 'frontier-use-cases' }, + { + title: 'Tutorials', + slug: null, + children: [ + { title: 'Wikipedia History', slug: 'tutorials/wikipedia-history' }, + { title: 'Fandom Canon', slug: 'tutorials/fandom-canon' }, + { title: 'Citation Churn', slug: 'tutorials/citation-churn' }, + { title: 'Dispute Timeline', slug: 'tutorials/dispute-timeline' }, + ], + }, +]; + +function resolveTitle(slug) { + for (const item of NAV) { + if (item.slug === slug) return item.title; + if (item.children) { + for (const child of item.children) { + if (child.slug === slug) return child.title; + } + } + } + return basename(slug); +} + +function slugHref(slug) { + return slug === 'index' ? '/' : `/${slug}/`; +} + +function renderNav(currentSlug) { + function link(item) { + const isActive = item.slug === currentSlug; + const cls = isActive ? 'nav-link active' : 'nav-link'; + return `<a href="${slugHref(item.slug)}" class="${cls}">${item.title}</a>`; + } + + let html = ''; + for (const item of NAV) { + if (item.children) { + const parentActive = item.children.some(c => c.slug === currentSlug); + html += `<div class="nav-section">${item.title}</div>`; + html += `<div class="nav-group${parentActive ? ' open' : ''}">`; + for (const child of item.children) { + html += link(child); + } + html += '</div>'; + } else { + html += link(item); + } + } + return html; +} + +function wrapHTML(title, content, currentSlug) { + return `<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>${title} — Varia + + + + + + +

+ + +
+ + ${content} +
+
+ +`; +} + +function rewriteLink(href) { + if (!href) return href; + if (href.startsWith('http') || href.startsWith('/') || href.startsWith('#')) return href; + href = href.replace(/\.md$/, '/'); + if (href === 'index/') return '/'; + return href; +} + +async function collectFiles(dir, base = '') { + const entries = await readdir(dir, { withFileTypes: true }); + const files = []; + for (const entry of entries) { + const fp = join(dir, entry.name); + const rel = base ? `${base}/${entry.name}` : entry.name; + if (entry.isDirectory()) { + files.push(...await collectFiles(fp, rel)); + } else if (entry.name.endsWith('.md')) { + files.push({ path: fp, slug: rel.replace(/\.md$/, '') }); + } + } + return files; +} + +async function build() { + await rm(DIST_DIR, { recursive: true, force: true }); + await mkdir(DIST_DIR, { recursive: true }); + + const files = await collectFiles(DOCS_DIR); + + const renderer = new marked.Renderer(); + renderer.link = function ({ href, title, text }) { + const h = rewriteLink(href); + const t = title ? ` title="${title}"` : ''; + return `${text}`; + }; + renderer.image = function ({ href, title, text }) { + const h = rewriteLink(href); + const t = title ? ` title="${title}"` : ''; + return `${text}`; + }; + + marked.setOptions({ gfm: true, breaks: false }); + + for (const file of files) { + const raw = await readFile(file.path, 'utf-8'); + const body = marked.parse(raw, { renderer }); + const h1Match = raw.match(/^#\s+(.+)/m); + const title = h1Match ? h1Match[1] : resolveTitle(file.slug); + const html = wrapHTML(title, body, file.slug); + + if (file.slug === 'index') { + await writeFile(join(DIST_DIR, 'index.html'), html); + } else { + const outDir = join(DIST_DIR, file.slug); + await mkdir(outDir, { recursive: true }); + await writeFile(join(outDir, 'index.html'), html); + } + } + + await cp(ASSETS_DIR, DIST_DIR, { recursive: true }); + console.log(`Built ${files.length} pages to dist/`); +} + +build().catch(err => { + console.error(err); + process.exit(1); +}); diff --git a/docs/frontier-use-cases.md b/docs/frontier-use-cases.md new file mode 100644 index 0000000..39b10e1 --- /dev/null +++ b/docs/frontier-use-cases.md @@ -0,0 +1,310 @@ +# Frontier Use Cases for Claim-Provenance Infrastructure + +## Core Primitive + +Varia turns version history into: + +**claim + source + wording + placement + stability + time** + +It does not report what a document says now. It explains how a claim entered, changed, was sourced, challenged, moved, stabilized, weakened, or disappeared. + +--- + +## Retrieval & Search + +### AI Retrieval Provenance + +AI systems retrieve text as if every sentence is equally stable. They lack signals about what has been contested, recently changed, or stabilized through editorial scrutiny. + +Varia exposes per-claim metadata: + +```json +{ + "claim": "Entity X knew about the defect before launch", + "status": "included_with_attribution", + "first_seen": "2021-04-03", + "stability": "stable_after_regulatory_source_added", + "contestation": "previously_reverted_twice", + "placement": "body_not_lead", + "source_support": "regulatory_report_and_secondary_news" +} +``` + +This helps retrieval systems decide whether to present a claim directly, hedge it, attribute it, surface uncertainty, or deprioritize it. + +### Claim-Level Search + +Search across claim histories, not documents. + +Example queries Varia can support: + +- Claims about an entity that stabilized after a given date +- Claims removed as unsourced +- Claims supported by a specific source type +- Claims that moved from lead to body +- Claims that softened after external events + +This is a new search primitive over version-history data. + +### AI Evaluation & Temporal Leakage Detection + +Use claim histories to test AI systems for temporal leakage. + +Questions answerable with Varia data: + +- Was this claim public before the model's knowledge cutoff? +- Did the supporting source appear later? +- Did the wording exist at the time? +- Is the model using future knowledge? + +Varia provides temporal ground truth for benchmarking retrieval truthfulness, training-data audits, and knowledge-cutoff testing. + +--- + +## Claim History & Memory + +### Public Claim Memory + +Create a temporal knowledge graph for claims. + +Not: What does the article say now? +But: What has this claim said over time? + +Varia tracks across revisions: + +- Origin (first appearance) +- Source support evolution +- Wording changes +- Removals and restorations +- Disputes and challenges +- Current status + +### Knowable-at-the-Time Reconstruction + +Reconstruct what was publicly knowable at a specific date. + +Example: As of March 1, 2023, what sources supported a given claim? + +Varia's revision-level timestamping and deterministic extraction make this auditable, with no hindsight leakage. + +### Institutionalization of Knowledge + +Detect when a claim moves from rumor into durable public reference. + +Pattern Varia can surface: + +rumor → local reporting → national reporting → institutional source → article body → article lead → stable public reference + +Core question: When did this stop being chatter and become part of the durable public record? + +### Claim Survivorship + +Measure which claims survive scrutiny. + +Varia can classify claims by survival pattern: + +- Failed claim (removed and never restored) +- Temporary claim (appeared briefly) +- Stable claim (survives extended period unchanged) +- Stable only with attribution +- Stable only in body, not lead +- Repeatedly contested +- Removed under policy concern + +--- + +## Source & Evidence Integrity + +### Source Escalation & Degradation + +Track how source support changes across revisions. + +Escalation patterns Varia detects: + +unsourced → local source → national source → regulatory source → court ruling → academic analysis + +Degradation patterns: + +secondary source removed → primary source remains → citation-needed tag → claim removed + +### Article Voice & Attribution + +Distinguish who is making a claim — not just what the claim says. + +States Varia detects: + +- Direct assertion in article voice +- Attributed to a named source +- Attributed to critics or regulators +- Reported allegation +- Disputed claim +- Removed claim + +Example: "The company concealed the defect" versus "Regulators alleged that the company failed to disclose the defect" should not be treated as equivalent. + +### Semantic Intensity Drift + +Detect language getting stronger, weaker, broader, or narrower. + +Example shifts Varia can surface: + +- caused → contributed to +- concealed → failed to disclose +- proved → alleged +- will → may +- riot → protest → unrest + +### Misinformation & Circular Sourcing + +Track weak, false, or fringe claims as they move through public systems. + +Patterns Varia detects: + +- Claim introduced without source +- Reverted as unsourced +- Reintroduced with weak source +- Challenged and removed +- Returns in softened form + +Citogenesis detection: a claim originates on Wikipedia, gets cited by an external source, and Wikipedia later cites that source to verify the original claim — creating a self-referential loop. + +### Source Dependency Mapping + +Map which claims depend on which sources. + +If a source is corrected, discredited, or retracted, Varia can identify all affected claims across the knowledge base, enabling cascade analysis. + +--- + +## Editorial Mechanics + +### Editorial Consensus Mapping + +Show how a page or claim reaches a stable form. + +Signals Varia surfaces: + +- Talk-page discussion activity +- Revert cycles +- Policy references (BLP, NPOV, RS, UNDUE) +- Source replacement patterns +- Compromise wording emergence +- Page protection events +- Post-discussion stabilization + +### Prominence & Placement + +Track not just whether a claim exists, but where it appears. + +States Varia detects: lead, infobox, body, controversy section, footnote, caption, table, category, talk page only, removed. + +Meaningful transitions: body → lead (increased prominence), lead → body (reduced prominence), removed → restored (narrative resurrection). + +### Cross-Language Narrative Divergence + +Compare the same claim across language editions. + +Varia can surface differences in: wording, sources cited, prominence, stability timelines, and terminology across language versions of the same topic. + +--- + +## Change Monitoring + +### Public Reference Monitoring + +Monitor how an entity or topic changes in public reference sources. + +What Varia surfaces: + +- New claims added +- Claims moving into prominent positions +- Regulatory or institutional sources added or removed +- Policy templates applied (BLP, NPOV) +- Revert chains triggered +- Page protections activated +- Cross-language spread detected + +### Knowledge Volatility Metrics + +Create quantitative metrics from claim-history data: + +- Claim volatility over time +- Source churn rate +- Lead section instability +- Controversy intensity +- Cross-language divergence rate +- Edit-burst frequency + +### Change Summaries + +Generate structured summaries from claim-history analysis. + +Varia can produce outputs like: + +Three public claims changed materially this period. +1. One stabilized after regulatory sourcing. +2. One moved from body to lead. +3. One remains disputed after repeated reverts. + +Each statement links to the supporting evidence. + +--- + +## Domain Extensions + +### Policy, Regulation & Standards Tracking + +Apply the same engine to public rules and guidance documents. + +Changes Varia can detect: requirements added or weakened, mandatory → optional transitions, deadline extensions, scope narrowing, enforcement language removed. + +### Enterprise Knowledge Governance + +Apply claim provenance to internal knowledge bases. + +Detect across internal documentation: unsupported claims, stale procedures, contradictions between docs, obsolete product claims, policy drift, claims without sources. + +### Corporate Disclosure Tracking + +Track changes in publicly filed corporate documents. + +Varia can surface when commitments, risk factors, deadlines, or claims change in investor presentations, earnings materials, ESG reports, terms of service, or privacy policies. + +### Legal-Document History + +Apply the same engine to contracts, policies, and versioned legal documents. + +Detect: obligations added or narrowed, warranties softened, termination rights expanded, indemnity strengthened, compliance requirements removed. + +Example shifts: shall → may, all damages → direct damages, must notify within 24 hours → must notify without undue delay. + +--- + +## Universal Document-History + +### General Document-History Operating System + +The broadest application. For any versioned document system, Varia answers: + +- What changed? +- What claim or obligation did it affect? +- What source or evidence supported it? +- Was it challenged? +- Did it stabilize? +- What is its current status? + +This applies across: public wikis, policy drafts, standards documents, open-source documentation, collaborative prose, and any versioned text corpus. + +--- + +## Synthesis + +The frontier is not summarizing version history. + +The frontier is turning version history into claim-level provenance. + +The reusable primitive is: + +**claim + source + wording + placement + stability + time** + +That primitive can power use cases across retrieval, research, monitoring, governance, and knowledge management — without deciding what is true. diff --git a/docs/index.md b/docs/index.md index 631049d..1af3d05 100644 --- a/docs/index.md +++ b/docs/index.md @@ -19,6 +19,7 @@ Varia is a domain-neutral observability layer for public knowledge. It ingests r - [SDK / package reference](sdk.md) - [Tutorials](tutorials/wikipedia-history.md) - [Boundary](boundary.md) +- [Frontier use cases](frontier-use-cases.md) ## Open source diff --git a/package.json b/package.json new file mode 100644 index 0000000..2d425c0 --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "name": "varia-docs", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "build": "node build.mjs" + }, + "devDependencies": { + "marked": "^15.0.0" + } +} From e0f8b6575ed00af2183aabd0dfeddc0259bb9425 Mon Sep 17 00:00:00 2001 From: Kanav Jain Date: Thu, 14 May 2026 15:50:43 -0500 Subject: [PATCH 013/183] chore: add package-lock.json --- package-lock.json | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 package-lock.json diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..9976073 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,28 @@ +{ + "name": "varia-docs", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "varia-docs", + "version": "0.1.0", + "devDependencies": { + "marked": "^15.0.0" + } + }, + "node_modules/marked": { + "version": "15.0.12", + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", + "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", + "dev": true, + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + } + } +} From f91513e2d6647886f0d00c2267e24d9c1486f1f7 Mon Sep 17 00:00:00 2001 From: Kanav Jain Date: Thu, 14 May 2026 15:52:13 -0500 Subject: [PATCH 014/183] fix: enable Pages auto-configuration in deploy workflow --- .github/workflows/deploy.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 27e5a73..71b8d08 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -26,6 +26,8 @@ jobs: - run: npm ci - run: npm run build - uses: actions/configure-pages@v5 + with: + enablement: true - uses: actions/upload-pages-artifact@v3 with: path: dist From b7a129df2c6b492363ba5a0a6420e6dce396a7ad Mon Sep 17 00:00:00 2001 From: Kanav Jain Date: Thu, 14 May 2026 15:55:35 -0500 Subject: [PATCH 015/183] fix: use base path for GitHub Pages project site URLs --- build.mjs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/build.mjs b/build.mjs index 09467bc..fc7d868 100644 --- a/build.mjs +++ b/build.mjs @@ -7,6 +7,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); const DOCS_DIR = join(__dirname, 'docs'); const DIST_DIR = join(__dirname, 'dist'); const ASSETS_DIR = join(__dirname, 'assets'); +const BASE = process.env.BASE || '/varia-docs/'; const NAV = [ { title: 'Home', slug: 'index' }, @@ -43,7 +44,7 @@ function resolveTitle(slug) { } function slugHref(slug) { - return slug === 'index' ? '/' : `/${slug}/`; + return slug === 'index' ? BASE : `${BASE}${slug}/`; } function renderNav(currentSlug) { @@ -77,7 +78,7 @@ function wrapHTML(title, content, currentSlug) { ${title} — Varia - + @@ -86,7 +87,7 @@ function wrapHTML(title, content, currentSlug) {