Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@ hosts typed configuration as a modular layer over the same core.
integration suite in CI but remains a pre-1.0 production candidate. Cloudflare and FoundationDB
are private experimental workspace packages; shared core changes must keep them compiling, but
their semantics are not part of the default conformance claim.
- `@bjacobso/triplex-workbench` is a private browser application over the public core, config,
and derivation surfaces. It composes an app-lifetime Effect layer for a workforce demo,
evaluates hypothetical edits with `Derivation.Overlay`, and commits attributed transactions.
Its product UI and workforce model stay above core; no core or backend package depends on it.
- `@bjacobso/triplex-testkit` is the public home for reusable backend conformance helpers.
- `test/integration` owns tests that intentionally compose multiple publishable packages.
- `test/stress` owns opt-in performance and scale tests.
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,13 @@ hypothetical facts without mutating the source store or journal.
- [Compliance host demo](examples/compliance-host) — config, facts, feed catch-up, reconciliation,
hypothetical planning, and expiry-driven reopening in one standalone scenario

## Workbench

Run the workforce operations app with `pnpm workbench` and open <http://localhost:4174>.
Workbench combines spreadsheet, board, and graph views with eligibility explanations,
read-only change previews, and attributed transactions over a real browser-local Triplex database.
The demo resets on reload. See [`packages/workbench`](packages/workbench) for setup and scope.

## Explorer dashboard

Run the standalone Foldkit dashboard to inspect a real browser-local Triplex database:
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
"test:foundationdb:integration": "pnpm --filter @bjacobso/triplex-foundationdb test:integration",
"test:postgres:integration": "pnpm --filter @bjacobso/triplex-postgres test:integration",
"test:watch": "turbo watch test",
"typecheck": "tsc --noEmit -p tsconfig.alchemy.json && turbo run typecheck"
"typecheck": "tsc --noEmit -p tsconfig.alchemy.json && turbo run typecheck",
"workbench": "pnpm --filter @bjacobso/triplex-workbench dev"
},
"devDependencies": {
"@changesets/cli": "catalog:",
Expand Down
36 changes: 36 additions & 0 deletions packages/workbench/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Workbench

A workforce operations app over Triplex: spreadsheet views, linked objects, and explainable decisions.

```sh
pnpm install
pnpm --filter @bjacobso/triplex build
pnpm workbench
```

Open <http://localhost:4174>. The app seeds 12 workers, three organizations, and seven tasks in a
browser-local in-memory Triplex database. Reloading starts a fresh demo.

- Search, filter, sort, export CSV, and switch between table, I-9 board, and employer graph views.
- Click an eligibility cell to inspect source checks, the pinned rule definition, facts, and history.
- Click a state, employment status, or I-9 cell to preview an edit through `Derivation.Overlay`.
- Apply the preview as an attributed atomic transaction with a compare-and-retract precondition.
- Create a worker and explore linked organizations, tasks, the Datalog rule, and causal activity.

The app composes public `Triples` and `ConfigStore` services in one app-lifetime Effect layer.
Eligibility is a real Datalog derivation with source provenance and a committed config release.
For non-matches, the inspector shows the individual source checks; these are not a synthesized
negative derivation proof. Previewing does not mutate the source store. A stale edited source fact
causes the commit to fail atomically.

Workbench is a private application package above core. The existing domain-independent dashboard
remains available through `pnpm dashboard`. Workbench currently uses a fixed workforce model;
it does not yet offer arbitrary ontology authoring, durable storage, authentication, collaborative
editing, or automatic task reconciliation. Demo tasks are independent stored objects, so completing
an I-9 changes eligibility but does not automatically close its linked tasks.

```sh
pnpm --filter @bjacobso/triplex-workbench test
pnpm --filter @bjacobso/triplex-workbench typecheck
pnpm --filter @bjacobso/triplex-workbench build
```
22 changes: 22 additions & 0 deletions packages/workbench/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#176b50" />
<meta
name="description"
content="Workbench — a spreadsheet connected to your operational world, powered by Triplex."
/>
<title>Workbench · Workforce operations</title>
<link
rel="icon"
type="image/svg+xml"
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='8' fill='%23176b50'/%3E%3Cpath d='M7 9l4 15 5-11 5 11 4-15' stroke='white' stroke-width='2.5' fill='none'/%3E%3C/svg%3E"
/>
</head>
<body>
<div id="app"><div class="loading">Opening Workbench…</div></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
26 changes: 26 additions & 0 deletions packages/workbench/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "@bjacobso/triplex-workbench",
"version": "0.1.0",
"private": true,
"description": "An operational workspace over the Triplex knowledge graph.",
"type": "module",
"scripts": {
"dev": "vite --host 0.0.0.0 --port 4174",
"build": "vite build",
"typecheck": "tsc --noEmit",
"test": "vitest run",
"clean": "rm -rf dist"
},
"dependencies": {
"@bjacobso/triplex": "workspace:^",
"@fontsource-variable/ibm-plex-sans": "catalog:",
"@fontsource/ibm-plex-mono": "catalog:",
"effect": "catalog:"
},
"devDependencies": {
"@types/node": "catalog:",
"typescript": "catalog:",
"vite": "catalog:",
"vitest": "catalog:"
}
}
76 changes: 76 additions & 0 deletions packages/workbench/src/data.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { Effect, ManagedRuntime } from "effect";
import { describe, expect, it } from "vitest";
import {
applyChange,
createWorker,
initialize,
previewChange,
readWorkbench,
WorkbenchLayer,
} from "./data.js";

const makeRuntime = () => ManagedRuntime.make(WorkbenchLayer);
const withWorkspace = async (test: (runtime: ReturnType<typeof makeRuntime>) => Promise<void>) => {
const runtime = ManagedRuntime.make(WorkbenchLayer);
try {
await runtime.runPromise(initialize);
await test(runtime);
} finally {
await runtime.dispose();
}
};

describe("Workbench operational data", () => {
it("derives eligibility from stored facts and links worker tasks", async () => {
await withWorkspace(async (runtime) => {
const data = await runtime.runPromise(readWorkbench);
expect(data.workers).toHaveLength(12);
expect(data.evaluation.candidates).toHaveLength(7);
const bob = data.workers.find((worker) => worker.name === "Bob Martinez")!;
expect(bob.eligible).toBe(false);
expect(bob.tasks).toBe(2);
expect(data.evaluation.candidates[0]!.sources.length).toBe(4);
expect(data.definition.configSnapshot).toBe(data.release);
});
});
it("previews without writes, applies atomically, and rejects a stale preview", async () => {
await withWorkspace(async (runtime) => {
const before = await runtime.runPromise(readWorkbench);
const bob = before.workers.find((worker) => worker.name === "Bob Martinez")!;
const preview = await runtime.runPromise(previewChange(before, bob, "i9", "Complete"));
expect(preview.eligible).toBe(true);
const untouched = await runtime.runPromise(readWorkbench);
expect(untouched.workers.find((worker) => worker.id === bob.id)!.i9).toBe("Missing");
expect(untouched.journal).toHaveLength(before.journal.length);
const receipt = await runtime.runPromise(applyChange(preview));
const after = await runtime.runPromise(readWorkbench);
expect(after.workers.find((worker) => worker.id === bob.id)!.eligible).toBe(true);
expect(after.journal[0]!.txId).toBe(receipt.txId);
expect(after.journal[0]!.changes.map((change) => change.op).sort()).toEqual([
"assert",
"retract",
]);
expect(after.journal[0]!.configSnapshot).toBe(before.release);
const stale = await runtime.runPromise(applyChange(preview).pipe(Effect.result));
expect(stale._tag).toBe("Failure");
expect((await runtime.runPromise(readWorkbench)).journal).toHaveLength(after.journal.length);
});
});
it("creates a linked worker and validates input before writing", async () => {
await withWorkspace(async (runtime) => {
const id = await runtime.runPromise(createWorker("Alex Morgan", "Analyst", "org:acme", "CA"));
const data = await runtime.runPromise(readWorkbench);
expect(data.workers.find((worker) => worker.id === id)).toMatchObject({
name: "Alex Morgan",
employer: "org:acme",
i9: "Missing",
eligible: false,
});
const invalid = await runtime.runPromise(
createWorker(" ", "Analyst", "org:acme", "CA").pipe(Effect.result),
);
expect(invalid._tag).toBe("Failure");
expect((await runtime.runPromise(readWorkbench)).workers).toHaveLength(13);
});
});
});
Loading
Loading