factcircuit
FactCircuit: an auditable claim-and-evidence verification loop with immutable provenance, staged checks, bounded retrieval, and reproducible evaluation.
Explore data through linked tables, charts, maps and reproducible workflows.
Translation pending
DataTypeScript · JavaScriptGitHub @danylaksonoOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Interactive visual analytics, from data to direction.
Translation pending
Taken from the repository README
Loading the full README. The saved excerpt is shown below.
Interactive visual analytics, from data to direction.
ALUR is a browser-based workspace for inspecting, exploring and understanding data through coordinated tables, charts, maps and visual workflows. Load data, compare distributions, filter and select across linked views, calculate fields, build reproducible processing pipelines and ask the analysis copilot for help.Read the full README ↗Interactive visual analytics, from data to direction.
ALUR is a browser-based workspace for inspecting, exploring and understanding data through coordinated tables, charts, maps and visual workflows. Load data, compare distributions, filter and select across linked views, calculate fields, build reproducible processing pipelines and ask the analysis copilot for help.
Maps are one analytical view, not the product boundary: ALUR uses MapLibre when geography helps answer the question, while the interactive table, charts and DuckDB workflow remain equally important. Everything runs locally in the browser. The copilot is bring-your-own-key — add an OpenRouter API key in Settings; it is stored only in your browser's localStorage.
| Layer | Technology |
|---|---|
| Analytical engine | DuckDB-Wasm (in-browser SQL + spatial, plus h3 fetched on first use) |
| Linked views | TanStack Table + custom SVG charts + MapLibre GL JS |
| Workflow editor | XYFlow (React Flow) |
| State management | Zustand |
| AI assistant | LLM-powered chat with tool calling |
| UI framework | React 18 + Tailwind CSS |
| Build tool | Vite |
Everything runs client-side. No backend, no server, no installation.
npm install
npm run dev # Start dev server on localhost:5173
npm run build # Production build
npm run preview # Preview production build
npm run test # Run the test suite
Netlify is the sole production deployment at alur-app.netlify.app. It builds the site with npm run build and publishes dist; the included netlify.toml records those settings in the repository.
Coordinated visual-analytics shell: the main canvas provides geographic context when available, while charts, layers and the copilot live in a collapsible left panel and the workflow, table and SQL views share a resizable bottom drawer.
┌──────────────────────────────────────────────────────────┐
│ Header: logo · Add data · New project · Settings (BYOK) │
├──┬───────────────┬───────────────────────────────────────┤
│R │ Left panel │ │
│a │ (collapsible) │ MapLibre GL (full-bleed) │
│i │ · Layers + │ │
│l │ Style │ Legend overlay Selection overlay │
│ │ · Charts │ │
│ │ · Copilot │ │
├──┴───────────────┴───────────────────────────────────────┤
│ Bottom drawer (collapsed ▲ / resizable / maximizable) │
│ Workflow (node canvas + palette) · Table · SQL │
└──────────────────────────────────────────────────────────┘
│ │ │
└───────────────┼────────────────────┘
│
┌──────┴──────┐
│ Zustand │ Single store for all state
│ Store │
└──────┬──────┘
│
┌──────┴──────┐
│ DuckDB-Wasm│ Spatial queries, filtering,
│ │ profiling, dot generation
└─────────────┘
GROUP BY, or geometry dissolve), Allocate (spend a budget or capacity down a ranked list), Join (spatial predicate or attribute key), Operation (a named, reusable group of steps you saved), Visualisation (style recipe), Output (map preview or file export)Summarise computes count, count distinct, sum, average, median, min and max over any number of group keys. Sum, average and median cast to a number; min and max do not, so they still work on dates and text. Merging each group's geometry is optional, and keeps the summary mappable.
Allocate answers "who gets served before the money runs out". It accumulates a column in priority order and either flags each row within/over the limit, drops the rows past it, or gives the row straddling it a partial share. Partitioning gives each group its own limit rather than sharing one.
Score combines several numeric columns into one weighted ranking. Each column is normalised across the whole result — min–max, z-score or percentile rank — before weighting, so columns on different scales combine safely, and weights are shares of their total so they need not sum to 1. Alongside the score and its rank it emits one column per criterion recording what that criterion contributed, and those contributions sum exactly to the score.
ST_GeneratePoints creates random dots within polygons, proportional to a numeric fieldAll styles are compiled to native MapLibre expressions — no hand-written JSON.
The Score panel in the left rail is where weights become arguable rather than declared. Moving a weight re-ranks the list underneath it against live data, each row carries a stacked bar of its criterion contributions, and a sensitivity strip reports how far the ranking moves when each weight is nudged — so a criterion that barely disturbs the top of the list is visibly not worth arguing over. Any model built there can be handed to the workflow as a Score node, which is what makes the result reproducible and feeds the rest of the pipeline.
A WHERE clause answers what survived; it cannot answer why a particular row did not, because the statement that produces the answer destroys the evidence. The Filter node's named conditions mode takes a list of labelled conditions instead of one anonymous clause, and writes on every row which ones it failed — alur_excluded, alur_excluded_by and alur_excluded_count. Conditions are hard (can remove a row) or soft (only marks it), so near-misses stay visible instead of being lost. Set the outcome to keep them, marked and nothing is removed at all.
The reasons are plain text, so they group in a Summarise node, colour a map through the category renderer and export to CSV without anything downstream needing to know they are special.
Inside the node, a constraint funnel measures each condition against the real upstream rows and reports both what it removes on its own and what it removes that the earlier conditions had not already. The gap is the useful part: a condition that removes thousands alone but nothing in sequence does not bind, which no surviving row count would ever tell you.
A workflow that reads as fifteen anonymous Attribute nodes is one nobody can follow a month later. Select a run of steps and save them as an operation with a name and fill-in-the-blank values: it joins the node palette and reads as one step on the canvas.
Blanks are found rather than declared — write {{amount}} or {{column}} anywhere in a step's configuration and the save dialog offers them to be labelled and typed. Values are a number, a column, or one of a list, and each is validated before any SQL is built; free text is deliberately not offered, because a value is interpolated into the query.
ALUR ships no domain vocabulary of its own. Retrofit(−20% on Gcons2023) is something you author out of generic nodes, and it travels in your project file for whoever you share it with.
Project files record the workflow, styling, cohorts and report — but not the data, which stays on your machine. ALUR keeps a copy of every file you load in the browser's private storage, so reopening a project or recovering a crashed session re-attaches its data without asking you to find each file again. Where a file is not held — a different machine, or storage the browser has reclaimed — the existing relink prompt asks for exactly that file and checks it matches before accepting it.
Nothing leaves your device. Settings shows how much is cached and clears it, and the cache stays inside a fixed budget by dropping least-recently-used files.
_alur_feature_id assigned on layer creation, used for cross-component linkingData Input Filter Analysis Output
┌──────────┐ ┌──────────┐ ┌──────────────┐ ┌──────────┐
│ wards │ ──► │ need >= 10│ ──► │ ST_Buffer │ ──► │ Map │
│ .parquet │ │ │ │ (500m) │ │ Preview │
└──────────┘ └──────────┘ └──────────────┘ └──────────┘
This compiles to a single DuckDB query:
WITH step1 AS (SELECT * FROM "wards"),
step2 AS (SELECT * FROM step1 WHERE need >= 10),
step3 AS (SELECT *, ST_Buffer("geometry", 500) AS geom_buffered FROM step2)
SELECT * FROM step3 LIMIT 5000;
src/
├── store/
│ └── useStore.ts # Zustand store (nodes, edges, mapLayers, visualAnalytics, chat)
├── types/
│ ├── visualisation.ts # LayerVisualisation union, LegendSpec
│ └── visualAnalytics.ts # VisualFilter, LayerFeatureSelection, summaries
├── utils/
│ ├── workflowEngine.ts # Node graph → chained-CTE SQL compiler
│ ├── mapStyleCompiler.ts # Visualisation → MapLibre expressions
│ ├── classification.ts # Profiling, classification, vis builders
│ ├── scoreModel.ts # Weighted multi-criteria score → SQL
│ ├── aggregationSql.ts # Group-by measures, running-total allocation, top-N
│ ├── filterPredicates.ts # Named conditions → exclusion columns and funnel SQL
│ ├── workflowFragments.ts # Saved operations: parameters, validation, expansion
│ ├── variantLineage.ts # Scenario tree, per-branch parameter diffs, assumptions
│ ├── visualFilterSql.ts # VisualFilter → DuckDB WHERE clauses
│ ├── visualisationResolver.ts # Workflow config → LayerVisualisation
│ ├── scenarioComparison.ts # Variant results → ComparisonSpec
│ ├── storyDiff.ts # Claim matching between two stories
│ ├── fieldCalculator.ts # Table-view expression parser
│ └── toolDefinitions.ts # LLM tool schemas
├── services/
│ ├── duckdb.ts # DuckDB-Wasm init + query interface
│ ├── visualAnalyticsService.ts # Filtered rows, profiles, summaries, temporal range
│ ├── comparisonService.ts # Comparison alignment and denominators
│ ├── scoreService.ts # Live scoring, ranking and weight sensitivity
│ ├── filterFunnelService.ts # What each condition in a filter actually removes
│ ├── sourceCache.ts # Loaded files kept in OPFS so projects reopen with their data
│ ├── layerMaterialization.ts # Workflow result → layer or dataset
│ ├── workflowRun.ts # Registers a run's output across the store
│ ├── projectService.ts # Project manifest save / load / migrate
│ └── storyService.ts # Story export, parse and disclosure
├── components/
│ ├── Visualisation/ # Style editor, cohorts, filters, KPIs, selection
│ ├── Compare/ # Compare workspace + shared evidence views
│ ├── Explain/ # Report workspace, story viewer, diff, export
│ ├── Variants/ # Scenario variants panel
│ ├── Charts/ · Map/ · Flow/ # Chart panel, MapLibre view, workflow nodes
│ ├── shell/ # AppShell, rail, panel, drawer, command palette
│ ├── Chat.tsx # LLM agent with tool execution (BYOK)
│ └── DataTable.tsx # Attribute table with profiling
└── App.tsx # DuckDB init + AppShell mount
379 tests across 46 files, covering the visualisation pipeline, workflow SQL generation, store actions, comparison and story services, and a full workflow smoke test. Some of the more load-bearing ones:
| Test file | Focus |
|---|---|
visualisationIntegration.test.ts | End-to-end pipeline: vis kinds, resolver, branching, interaction state, feature IDs, clustering, cleanup |
workflowEngine.test.ts | SQL generation, joins, visualisation propagation, terminal-node attribution |
scoreModel.test.ts | Weighted score compilation: weights, direction, normalisation, missing values |
filterPredicates.test.ts | Named conditions: NULL handling, hard vs soft, exclusion columns, funnel counts |
workflowFragments.test.ts | Saved operations: parameter validation, expansion, rewiring, graph immutability |
variantLineage.test.ts | Scenario tree, per-branch diffs, orphans and cycles, assumption capture |
comparisonService.test.ts | Comparison alignment, denominators and warnings |
useStore.test.ts | Layer state, layout preferences, scenario variants |
storyDiff.test.ts | Claim matching and divergence reasons between two stories |
sampleWorkflowSmoke.test.tsx | Full workflow sequence + UI mounting + chat tools |
npm test # Run once
npm run test:watch # Watch mode
npm run test:e2e # Playwright
docs/improvement-plan.md is the current plan. Composite scoring, the numeric GROUP BY and allocation nodes, filter provenance, named reusable operations, equal-area hexbins, resumable projects and scenario lineage have shipped; wider copilot coverage has not.
Still deferred, and not in that plan: