A memory for your world.
Small experiments in software you can inspect, explain, and build on. Triplex keeps the facts. These projects explore the language and models around them.
diff --git a/docs/snippets/home/layers.ts b/docs/snippets/home/layers.ts
deleted file mode 100644
index 3d0259e..0000000
--- a/docs/snippets/home/layers.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { Layer } from "effect";
-import { KvTriples } from "@triplex-build/triplex";
-import { ConfigStore } from "@triplex-build/triplex/config";
-import { PgTriples } from "@triplex-build/triplex-postgres";
-import { SqliteTriples } from "@triplex-build/triplex-sqlite";
-
-// The same program runs against any of these. Only the provided layer changes.
-export const InMemory = ConfigStore.layer.pipe(Layer.provideMerge(KvTriples.layer));
-
-export const Sqlite = ConfigStore.layer.pipe(
- Layer.provideMerge(SqliteTriples.layer({ filename: "app.db" })),
-);
-
-export const Postgres = ConfigStore.layer.pipe(
- Layer.provideMerge(PgTriples.layerFromUrl(process.env["DATABASE_URL"] ?? "")),
-);
diff --git a/docs/snippets/home/site-safety/app.ts b/docs/snippets/home/site-safety/app.ts
new file mode 100644
index 0000000..2884563
--- /dev/null
+++ b/docs/snippets/home/site-safety/app.ts
@@ -0,0 +1,168 @@
+/**
+ * The homepage site-safety application, top to bottom: model, rules, a command,
+ * audit questions, derived work, storage, and HTTP. The homepage shows each
+ * region in order, and `run.ts` executes it in CI to produce every output.
+ */
+import { createServer } from "node:http";
+
+import { NodeHttpServer } from "@effect/platform-node";
+import { Effect, Layer } from "effect";
+import { HttpRouter } from "effect/http";
+import { EntityId, KvTriples, Triples, type TemporalBasis } from "@triplex-build/triplex";
+import {
+ Attribute,
+ ConfigStore,
+ ContentId,
+ EntityType,
+ GraphConstraint,
+ type InMemoryConfigStore,
+} from "@triplex-build/triplex/config";
+import * as Derivation from "@triplex-build/triplex/derivation";
+import { EntityHttp, HttpAuthorizationAllowAll } from "@triplex-build/triplex-http";
+import { PgTriples } from "@triplex-build/triplex-postgres";
+import { SqliteTriples } from "@triplex-build/triplex-sqlite";
+
+export const MAR_2 = Date.UTC(2026, 2, 2);
+export const maria = EntityId.make("worker:maria");
+export const harbor = EntityId.make("site:harbor");
+
+// #region model
+// Attributes are global facts. Entity types say how each one is used.
+export const Site = EntityType.make("Site", {
+ attributes: { name: Attribute.use(Attribute.text(":site/name"), { required: true }) },
+});
+
+export const Worker = EntityType.make("Worker", {
+ attributes: {
+ name: Attribute.use(Attribute.text(":worker/name"), { required: true }),
+ trainedFor: Attribute.use(Attribute.ref(":worker/trained-for", Site), { cardinality: "many" }),
+ },
+});
+
+const placement = {
+ worker: Attribute.use(Attribute.ref(":placement/worker", Worker), { required: true }),
+ site: Attribute.use(Attribute.ref(":placement/site", Site), { required: true }),
+};
+export const Placement = EntityType.make("Placement", { attributes: placement });
+
+// The next version of the rules: every placement needs a supervisor.
+export const PlacementV2 = EntityType.make("Placement", {
+ attributes: {
+ ...placement,
+ supervisor: Attribute.use(Attribute.text(":placement/supervisor"), { required: true }),
+ },
+});
+// #endregion model
+
+// #region release
+// Publishing a model creates an immutable, content-addressed release and moves `live` to it.
+export const publish = (label: string, types: ReadonlyArray<{ nodes: typeof Site.nodes }>) =>
+ Effect.gen(function* () {
+ const config = yield* ConfigStore.ConfigStore;
+ const objects = yield* Effect.all(types.map((type) => type.nodes));
+ return yield* config.commit({ label, objects: objects.flat(), ref: "live" });
+ });
+// #endregion release
+
+// #region command
+// One command is one atomic transaction: who acted, which command, under which rules.
+export const placeWorker = (
+ release: InMemoryConfigStore.ConfigSnapshot,
+ placement: EntityId,
+ worker: EntityId,
+ site: EntityId,
+) =>
+ Effect.gen(function* () {
+ const triples = yield* Triples;
+ const assert = { op: "assert", entityId: placement, entityType: "Placement" } as const;
+ return yield* triples.transact(
+ [
+ { ...assert, ...Placement.worker.assertion(worker, { validFrom: MAR_2 }) },
+ { ...assert, ...Placement.site.assertion(site, { validFrom: MAR_2 }) },
+ ],
+ {
+ actor: "user:dana",
+ commandId: `create:${placement}`, // unique: a retry can never apply twice
+ correlationId: "request:7f3a",
+ configSnapshot: release.id, // the rules this write answered to
+ enforce: GraphConstraint.enforcement(yield* GraphConstraint.collect(release.root)),
+ },
+ );
+ });
+// #endregion command
+
+// #region ask
+// Who placed Maria, under which rules, and was she trained, as known then and now?
+export const audit = Effect.gen(function* () {
+ const triples = yield* Triples;
+ const config = yield* ConfigStore.ConfigStore;
+ const receipt = yield* triples.transactionByCommand("create:placement:maria-harbor");
+ if (receipt === null || !ContentId.isContentId(receipt.configSnapshot)) {
+ return yield* Effect.die("placement was not pinned to a release");
+ }
+
+ const trained = (basis: TemporalBasis) =>
+ triples
+ .match({ entityId: maria, attribute: Worker.trainedFor.key }, basis)
+ .pipe(Effect.map((facts) => facts.length > 0));
+
+ return {
+ actor: receipt.actor,
+ governedBy: (yield* config.snapshotById(receipt.configSnapshot))?.label,
+ liveToday: (yield* config.resolveRef("live"))?.label,
+ trainedAsKnownThen: yield* trained({ validAt: MAR_2, recordedAt: receipt.instant }),
+ trainedAsKnownNow: yield* trained({ validAt: MAR_2 }),
+ };
+});
+// #endregion ask
+
+// #region derive
+// Open work is a query: a placed worker without current training for that site.
+export const openWork = (configSnapshot: string, validAt: number) =>
+ Effect.gen(function* () {
+ const training = yield* Derivation.make({
+ name: "task.site-safety-training",
+ query: {
+ find: ["?worker", "?site"],
+ where: [
+ ["?placement", Placement.worker.key, "?worker"],
+ ["?placement", Placement.site.key, "?site"],
+ ["not", ["?worker", Worker.trainedFor.key, "?site"]],
+ ],
+ },
+ identity: ["?worker", "?site"],
+ configSnapshot,
+ });
+ const { candidates, nextTemporalBoundary } = yield* Derivation.evaluate(
+ yield* Triples,
+ training,
+ { basis: { validAt } },
+ );
+ return {
+ open: candidates.map((task) => task.result),
+ recheckAt: nextTemporalBoundary === undefined ? null : new Date(nextTemporalBoundary),
+ };
+ });
+// #endregion derive
+
+// #region run
+// The same program runs on any storage. Only the layer changes.
+export const InMemory = ConfigStore.layer.pipe(Layer.provideMerge(KvTriples.layer));
+export const Sqlite = ConfigStore.layer.pipe(
+ Layer.provideMerge(SqliteTriples.layer({ filename: "app.db" })),
+);
+export const Postgres = ConfigStore.layer.pipe(
+ Layer.provideMerge(PgTriples.layerFromUrl("postgres://localhost/app")),
+);
+
+// The `live` release is also a REST API with OpenAPI docs and as-of reads.
+export const Api = EntityHttp.layer({
+ docs: true,
+ exposure: { collections: { Worker: "workers", Placement: "placements" } },
+}).pipe(Layer.provide(HttpAuthorizationAllowAll)); // bring your own auth in production
+
+export const Server = HttpRouter.serve(Api).pipe(
+ Layer.provide(Sqlite),
+ Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })),
+);
+// #endregion run
diff --git a/docs/snippets/home/site-safety/model.ts b/docs/snippets/home/site-safety/model.ts
deleted file mode 100644
index 0dfa0d8..0000000
--- a/docs/snippets/home/site-safety/model.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-import { Effect } from "effect";
-import { EntityId } from "@triplex-build/triplex";
-import { Attribute, ConfigStore, EntityType } from "@triplex-build/triplex/config";
-
-// Attributes own global identity and value type.
-export const SiteName = Attribute.text(":site/name");
-export const Site = EntityType.make("Site", {
- attributes: { name: Attribute.use(SiteName, { required: true, unique: true }) },
-});
-
-export const WorkerName = Attribute.text(":worker/name");
-export const TrainedFor = Attribute.ref(":worker/trained-for", Site);
-export const Worker = EntityType.make("Worker", {
- attributes: {
- name: Attribute.use(WorkerName, { required: true }),
- trainedFor: Attribute.use(TrainedFor, { cardinality: "many" }),
- },
-});
-
-export const PlacementWorker = Attribute.ref(":placement/worker", Worker);
-export const PlacementSite = Attribute.ref(":placement/site", Site);
-export const Placement = EntityType.make("Placement", {
- attributes: {
- worker: Attribute.use(PlacementWorker, { required: true }),
- site: Attribute.use(PlacementSite, { required: true }),
- },
-});
-
-// hr-2026.2 changes the rules: every placement now needs a supervisor.
-export const PlacementSupervisor = Attribute.text(":placement/supervisor");
-export const PlacementV2 = EntityType.make("Placement", {
- attributes: {
- worker: Attribute.use(PlacementWorker, { required: true }),
- site: Attribute.use(PlacementSite, { required: true }),
- supervisor: Attribute.use(PlacementSupervisor, { required: true }),
- },
-});
-
-export const maria = EntityId.make("worker:maria");
-export const harbor = EntityId.make("site:harbor");
-export const placement = EntityId.make("placement:maria-harbor");
-
-// Entity types own usage rules. Publishing them creates an immutable release.
-export const publish = (label: string, placementType: typeof Placement | typeof PlacementV2) =>
- Effect.gen(function* () {
- const config = yield* ConfigStore.ConfigStore;
- return yield* config.commit({
- label,
- objects: [...(yield* Site.nodes), ...(yield* Worker.nodes), ...(yield* placementType.nodes)],
- ref: "live",
- });
- });
diff --git a/docs/snippets/home/site-safety/open-work.ts b/docs/snippets/home/site-safety/open-work.ts
deleted file mode 100644
index 6d9322b..0000000
--- a/docs/snippets/home/site-safety/open-work.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import { Effect } from "effect";
-import { Triples } from "@triplex-build/triplex";
-import * as Derivation from "@triplex-build/triplex/derivation";
-import { PlacementSite, PlacementWorker, TrainedFor } from "./model.ts";
-
-// Obligation: every placed worker holds current training for that site.
-export const siteSafety = (configSnapshot: string) =>
- Derivation.make({
- name: "task.site-safety-training",
- query: {
- find: ["?worker", "?site"],
- where: [
- ["?placement", PlacementWorker.key, "?worker"],
- ["?placement", PlacementSite.key, "?site"],
- ["not", ["?worker", TrainedFor.key, "?site"]],
- ],
- },
- identity: ["?worker", "?site"],
- configSnapshot,
- });
-
-const day = (instant: number) => new Date(instant).toISOString().slice(0, 10);
-
-// Which training tasks should be open on a given day, and when will that change?
-export const openWorkOn = (configSnapshot: string, validAt: number) =>
- Effect.gen(function* () {
- const triples = yield* Triples;
- const definition = yield* siteSafety(configSnapshot);
- const { candidates, nextTemporalBoundary } = yield* Derivation.evaluate(triples, definition, {
- basis: { validAt },
- });
- return {
- on: day(validAt),
- open: candidates.map((task) => task.result),
- recheckOn: nextTemporalBoundary === undefined ? null : day(nextTemporalBoundary),
- };
- });
diff --git a/docs/snippets/home/site-safety/output/audit.json b/docs/snippets/home/site-safety/output/audit.json
new file mode 100644
index 0000000..ff0fffc
--- /dev/null
+++ b/docs/snippets/home/site-safety/output/audit.json
@@ -0,0 +1,7 @@
+{
+ "actor": "user:dana",
+ "governedBy": "hr-2026.1",
+ "liveToday": "hr-2026.2",
+ "trainedAsKnownThen": true,
+ "trainedAsKnownNow": false
+}
diff --git a/docs/snippets/home/site-safety/output/http.json b/docs/snippets/home/site-safety/output/http.json
new file mode 100644
index 0000000..5728769
--- /dev/null
+++ b/docs/snippets/home/site-safety/output/http.json
@@ -0,0 +1,12 @@
+{
+ "asKnownThen": {
+ "id": "worker:maria",
+ "type": "Worker",
+ "attributes": { ":worker/name": "Maria", ":worker/trained-for": ["site:harbor"] }
+ },
+ "asKnownNow": {
+ "id": "worker:maria",
+ "type": "Worker",
+ "attributes": { ":worker/name": "Maria" }
+ }
+}
diff --git a/docs/snippets/home/site-safety/output/open-work.json b/docs/snippets/home/site-safety/output/open-work.json
index 3d01a3f..b7e75fe 100644
--- a/docs/snippets/home/site-safety/output/open-work.json
+++ b/docs/snippets/home/site-safety/output/open-work.json
@@ -1,13 +1,11 @@
-[
- {
- "on": "2026-03-03",
+{
+ "2026-03-03": {
"open": [{ "?worker": "worker:maria", "?site": "site:harbor" }],
- "recheckOn": "2026-03-05"
+ "recheckAt": "2026-03-05T00:00:00.000Z"
},
- { "on": "2026-04-01", "open": [], "recheckOn": "2026-06-01" },
- {
- "on": "2026-06-01",
+ "2026-04-01": { "open": [], "recheckAt": "2026-06-01T00:00:00.000Z" },
+ "2026-06-01": {
"open": [{ "?worker": "worker:maria", "?site": "site:harbor" }],
- "recheckOn": null
+ "recheckAt": null
}
-]
+}
diff --git a/docs/snippets/home/site-safety/output/stack.json b/docs/snippets/home/site-safety/output/stack.json
new file mode 100644
index 0000000..3243e26
--- /dev/null
+++ b/docs/snippets/home/site-safety/output/stack.json
@@ -0,0 +1 @@
+{ "model": 22, "release": 7, "command": 24, "ask": 20, "derive": 26, "run": 17, "total": 116 }
diff --git a/docs/snippets/home/site-safety/output/timeline.md b/docs/snippets/home/site-safety/output/timeline.md
index 61e8ebd..c1c8c86 100644
--- a/docs/snippets/home/site-safety/output/timeline.md
+++ b/docs/snippets/home/site-safety/output/timeline.md
@@ -1,8 +1,8 @@
-| Position | Actor | Command | Changes (+ assert, − retract; valid time) |
-| -------- | ---------------------- | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
-| 1 | `triplex/config-store` | — | Release `hr-2026.1`; `live` moved to it |
-| 2 | `user:hr` | `onboard:maria` | + `:site/name` `"Harbor"` from 2026-01-05
+ `:worker/name` `"Maria"` from 2026-01-05 |
-| 3 | `user:trainer` | `training:record:maria-harbor` | + `:worker/trained-for` `site:harbor` [2026-03-01, 2026-06-01) |
-| 4 | `user:dana` | `placement:create:maria-harbor` | + `:placement/worker` `worker:maria` from 2026-03-02
+ `:placement/site` `site:harbor` from 2026-03-02 |
-| 5 | `triplex/config-store` | — | Release `hr-2026.2`; `live` moved to it |
-| 6 | `user:auditor` | `training:correct:maria-harbor` | − `:worker/trained-for` `site:harbor` [2026-03-01, 2026-06-01)
+ `:worker/trained-for` `site:harbor` [2026-03-05, 2026-06-01) |
+| Position | Actor | Command | Changes (+ assert, − retract; valid time) |
+| -------- | ---------------------- | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
+| 1 | `triplex/config-store` | — | Release `hr-2026.1`; `live` moved to it |
+| 2 | `user:hr` | `onboard:maria` | + `:site/name` `"Harbor"` from 2026-01-05
+ `:site/name` `"Pier"` from 2026-01-05
+ `:worker/name` `"Maria"` from 2026-01-05 |
+| 3 | `user:trainer` | `training:record:maria-harbor` | + `:worker/trained-for` `site:harbor` [2026-03-01, 2026-06-01) |
+| 4 | `user:dana` | `create:placement:maria-harbor` | + `:placement/worker` `worker:maria` from 2026-03-02
+ `:placement/site` `site:harbor` from 2026-03-02 |
+| 5 | `triplex/config-store` | — | Release `hr-2026.2`; `live` moved to it |
+| 6 | `user:auditor` | `training:correct:maria-harbor` | − `:worker/trained-for` `site:harbor` [2026-03-01, 2026-06-01)
+ `:worker/trained-for` `site:harbor` [2026-03-05, 2026-06-01) |
diff --git a/docs/snippets/home/site-safety/output/what-we-knew.json b/docs/snippets/home/site-safety/output/what-we-knew.json
deleted file mode 100644
index 0e35098..0000000
--- a/docs/snippets/home/site-safety/output/what-we-knew.json
+++ /dev/null
@@ -1 +0,0 @@
-{ "knownAtDecision": true, "knownToday": false }
diff --git a/docs/snippets/home/site-safety/output/which-rules.json b/docs/snippets/home/site-safety/output/which-rules.json
deleted file mode 100644
index 6561f41..0000000
--- a/docs/snippets/home/site-safety/output/which-rules.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "actor": "user:dana",
- "correlationId": "request:7f3a",
- "governedBy": "hr-2026.1",
- "liveToday": "hr-2026.2"
-}
diff --git a/docs/snippets/home/site-safety/record.ts b/docs/snippets/home/site-safety/record.ts
deleted file mode 100644
index cbd7045..0000000
--- a/docs/snippets/home/site-safety/record.ts
+++ /dev/null
@@ -1,113 +0,0 @@
-import { Effect } from "effect";
-import { Triples } from "@triplex-build/triplex";
-import { GraphConstraint, type InMemoryConfigStore } from "@triplex-build/triplex/config";
-import { Placement, Site, TrainedFor, Worker, harbor, maria, placement } from "./model.ts";
-
-export const JAN_5 = Date.UTC(2026, 0, 5);
-export const MAR_1 = Date.UTC(2026, 2, 1);
-export const MAR_2 = Date.UTC(2026, 2, 2);
-export const MAR_5 = Date.UTC(2026, 2, 5);
-export const JUN_1 = Date.UTC(2026, 5, 1);
-
-export const onboard = Effect.gen(function* () {
- const triples = yield* Triples;
- return yield* triples.transact(
- [
- {
- op: "assert",
- entityId: harbor,
- entityType: "Site",
- ...Site.attributes.name.assertion("Harbor", { validFrom: JAN_5 }),
- },
- {
- op: "assert",
- entityId: maria,
- entityType: "Worker",
- ...Worker.attributes.name.assertion("Maria", { validFrom: JAN_5 }),
- },
- ],
- { actor: "user:hr", commandId: "onboard:maria" },
- );
-});
-
-// Evidence holds for a window of business time: [validFrom, validTo).
-export const recordTraining = Effect.gen(function* () {
- const triples = yield* Triples;
- return yield* triples.transact(
- [
- {
- op: "assert",
- entityId: maria,
- entityType: "Worker",
- attribute: TrainedFor.key,
- value: TrainedFor.encode(harbor),
- validFrom: MAR_1,
- validTo: JUN_1,
- },
- ],
- { actor: "user:trainer", commandId: "training:record:maria-harbor" },
- );
-});
-
-// #region place
-// One application command is one atomic transaction that records who acted,
-// which command it was, and the release whose rules it was checked against.
-export const placeWorker = (release: InMemoryConfigStore.ConfigSnapshot) =>
- Effect.gen(function* () {
- const triples = yield* Triples;
- const rules = yield* GraphConstraint.collect(release.root);
- return yield* triples.transact(
- [
- {
- op: "assert",
- entityId: placement,
- entityType: "Placement",
- ...Placement.attributes.worker.assertion(maria, { validFrom: MAR_2 }),
- },
- {
- op: "assert",
- entityId: placement,
- entityType: "Placement",
- ...Placement.attributes.site.assertion(harbor, { validFrom: MAR_2 }),
- },
- ],
- {
- actor: "user:dana",
- commandId: "placement:create:maria-harbor", // unique: a retry can never apply twice
- correlationId: "request:7f3a",
- configSnapshot: release.id, // the rules this write was governed by
- enforce: GraphConstraint.enforcement(rules), // required/unique/ref checks, atomically
- },
- );
- });
-// #endregion place
-
-// A correction never deletes history. It retracts the recorded belief and
-// asserts what is now known, in one attributed transaction.
-export const correctTraining = Effect.gen(function* () {
- const triples = yield* Triples;
- const [wrong] = yield* triples.match(
- { entityId: maria, attribute: TrainedFor.key },
- { validAt: MAR_1 },
- );
- if (wrong === undefined) return yield* Effect.die("no training evidence to correct");
- return yield* triples.transact(
- [
- { op: "retract", id: wrong.id },
- {
- op: "assert",
- entityId: maria,
- entityType: "Worker",
- attribute: TrainedFor.key,
- value: TrainedFor.encode(harbor),
- validFrom: MAR_5,
- validTo: JUN_1,
- },
- ],
- {
- actor: "user:auditor",
- commandId: "training:correct:maria-harbor",
- causationId: "audit:2026-q1",
- },
- );
-});
diff --git a/docs/snippets/home/site-safety/retry.ts b/docs/snippets/home/site-safety/retry.ts
deleted file mode 100644
index d5ad62d..0000000
--- a/docs/snippets/home/site-safety/retry.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { Effect } from "effect";
-import { Triples } from "@triplex-build/triplex";
-import type { InMemoryConfigStore } from "@triplex-build/triplex/config";
-import { placeWorker } from "./record.ts";
-
-// A retried command must never apply twice. The duplicate is rejected with the
-// original transaction, so the caller can answer with the first receipt.
-export const placeWorkerOnce = (release: InMemoryConfigStore.ConfigSnapshot) =>
- placeWorker(release).pipe(
- Effect.map((result) => result.position),
- Effect.catchTag("CommandAlreadyCommittedError", (duplicate) =>
- Effect.gen(function* () {
- const triples = yield* Triples;
- const receipt = yield* triples.transactionByCommand(duplicate.commandId);
- return receipt?.position;
- }),
- ),
- );
diff --git a/docs/snippets/home/site-safety/run.ts b/docs/snippets/home/site-safety/run.ts
index 5d9a098..cd2ca32 100644
--- a/docs/snippets/home/site-safety/run.ts
+++ b/docs/snippets/home/site-safety/run.ts
@@ -9,16 +9,27 @@ import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { Effect, Layer } from "effect";
-import { EntityId, KvTriples, Triples, type TransactionChange } from "@triplex-build/triplex";
-import { ConfigStore, GraphConstraint } from "@triplex-build/triplex/config";
+import { HttpRouter } from "effect/http";
+import { EntityId, Triples, type TransactionChange } from "@triplex-build/triplex";
+import { ConfigStore } from "@triplex-build/triplex/config";
import { ContentId } from "@triplex-build/triplex/content";
-import { Placement, PlacementV2, harbor, maria, publish } from "./model.ts";
-import { JUN_1, MAR_2, correctTraining, onboard, placeWorker, recordTraining } from "./record.ts";
-import { openWorkOn } from "./open-work.ts";
-import { placeWorkerOnce } from "./retry.ts";
-import { governingRules } from "./which-rules.ts";
-import { wasMariaTrained } from "./what-we-knew.ts";
+import {
+ Api,
+ InMemory,
+ MAR_2,
+ Placement,
+ PlacementV2,
+ Site,
+ Worker,
+ audit,
+ harbor,
+ maria,
+ openWork,
+ placeWorker,
+ publish,
+} from "./app.ts";
+import { JUN_1, correctTraining, onboard, pier, placeWorkerOnce, recordTraining } from "./story.ts";
const day = (instant: number) => new Date(instant).toISOString().slice(0, 10);
@@ -34,47 +45,56 @@ const describe = (change: TransactionChange) => {
// Recorded time comes from the wall clock; keep each step on a distinct instant.
const tick = Effect.sleep("5 millis");
+// Serve the `Api` layer in process and read Maria as of March 2, as known then and now.
+const readOverHttp = (recordedAt: number) =>
+ Effect.gen(function* () {
+ const database = yield* Effect.context
();
+ const { handler, dispose } = HttpRouter.toWebHandler(
+ Api.pipe(Layer.provide(Layer.succeedContext(database))),
+ { disableLogger: true },
+ );
+ const read = (query: string) =>
+ Effect.promise(async () => {
+ const url = `http://triplex.local/api/rest/latest/workers/${maria}?validAt=${MAR_2}${query}`;
+ const response = await handler(new Request(url));
+ if (!response.ok) throw new Error(`${url} → ${response.status} ${await response.text()}`);
+ return response.json();
+ });
+ const asKnownThen = yield* read(`&recordedAt=${recordedAt}`);
+ const asKnownNow = yield* read("");
+ yield* Effect.promise(dispose);
+ return { asKnownThen, asKnownNow };
+ });
+
const scenario = Effect.gen(function* () {
const triples = yield* Triples;
- const release = yield* publish("hr-2026.1", Placement);
+ const release = yield* publish("hr-2026.1", [Site, Worker, Placement]);
yield* tick;
yield* onboard;
yield* tick;
yield* recordTraining;
yield* tick;
- const placed = yield* placeWorker(release.snapshot);
+ const placed = yield* placeWorker(
+ release.snapshot,
+ EntityId.make("placement:maria-harbor"),
+ maria,
+ harbor,
+ );
yield* tick;
const retried = yield* placeWorkerOnce(release.snapshot);
- if (retried !== placed.position)
+ if (retried !== placed.position) {
throw new Error("a retried command must return its first receipt");
+ }
yield* tick;
- const next = yield* publish("hr-2026.2", PlacementV2);
+ const next = yield* publish("hr-2026.2", [Site, Worker, PlacementV2]);
yield* tick;
- // The same kind of command, checked against today's rules, is rejected atomically.
- const rejected = yield* triples
- .transact(
- [
- {
- op: "assert",
- entityId: EntityId.make("placement:maria-pier"),
- entityType: "Placement",
- ...PlacementV2.attributes.worker.assertion(maria, { validFrom: MAR_2 }),
- },
- {
- op: "assert",
- entityId: EntityId.make("placement:maria-pier"),
- entityType: "Placement",
- ...PlacementV2.attributes.site.assertion(harbor, { validFrom: MAR_2 }),
- },
- ],
- {
- actor: "user:dana",
- commandId: "placement:create:maria-pier",
- configSnapshot: next.snapshot.id,
- enforce: GraphConstraint.enforcement(yield* GraphConstraint.collect(next.snapshot.root)),
- },
- )
- .pipe(Effect.flip);
+ // The same command under today's rules is rejected atomically.
+ const rejected = yield* placeWorker(
+ next.snapshot,
+ EntityId.make("placement:maria-pier"),
+ maria,
+ pier,
+ ).pipe(Effect.flip);
yield* tick;
yield* correctTraining;
@@ -99,32 +119,40 @@ const scenario = Effect.gen(function* () {
const snapshot = release.snapshot.id;
return {
timeline: `${rows.join("\n")}\n`,
- "what-we-knew": yield* wasMariaTrained,
- "which-rules": yield* governingRules,
+ audit: yield* audit,
rejected: {
error: rejected._tag,
...(rejected._tag === "ConstraintViolationError"
? { violations: [...new Set(rejected.violations.map((violation) => violation.message))] }
: {}),
},
- "open-work": [
- yield* openWorkOn(snapshot, Date.UTC(2026, 2, 3)),
- yield* openWorkOn(snapshot, Date.UTC(2026, 3, 1)),
- yield* openWorkOn(snapshot, JUN_1),
- ],
+ "open-work": {
+ [day(Date.UTC(2026, 2, 3))]: yield* openWork(snapshot, Date.UTC(2026, 2, 3)),
+ [day(Date.UTC(2026, 3, 1))]: yield* openWork(snapshot, Date.UTC(2026, 3, 1)),
+ [day(JUN_1)]: yield* openWork(snapshot, JUN_1),
+ },
+ http: yield* readOverHttp(placed.instant),
};
});
-const AppLayer = ConfigStore.layer.pipe(Layer.provideMerge(KvTriples.layer));
-const outputs = await Effect.runPromise(scenario.pipe(Effect.provide(AppLayer)));
+const outputs = await Effect.runPromise(scenario.pipe(Effect.provide(InMemory)));
+
+// How long each layer of the stack is, counted from the regions the homepage shows.
+const source = await readFile(resolve(import.meta.dirname, "app.ts"), "utf8");
+const stack: Record = {};
+for (const [, name, body] of source.matchAll(/#region (\w+)\n([\s\S]*?)\/\/ #endregion/g)) {
+ stack[name!] = body!.split("\n").filter((line) => line.trim() !== "").length;
+}
+stack["total"] = Object.values(stack).reduce((sum, lines) => sum + lines, 0);
const check = process.argv.includes("--check");
const files: Record = {
+ "stack.json": JSON.stringify(stack),
"timeline.md": outputs.timeline,
- "what-we-knew.json": JSON.stringify(outputs["what-we-knew"]),
- "which-rules.json": JSON.stringify(outputs["which-rules"]),
+ "audit.json": JSON.stringify(outputs.audit),
"rejected.json": JSON.stringify(outputs.rejected),
"open-work.json": JSON.stringify(outputs["open-work"]),
+ "http.json": JSON.stringify(outputs.http),
};
// Format exactly as the repository formatter would, so `pnpm format` never fights `--check`.
diff --git a/docs/snippets/home/site-safety/serve.ts b/docs/snippets/home/site-safety/serve.ts
new file mode 100644
index 0000000..fc74362
--- /dev/null
+++ b/docs/snippets/home/site-safety/serve.ts
@@ -0,0 +1,6 @@
+import { NodeRuntime } from "@effect/platform-node";
+import { Layer } from "effect";
+import { Server } from "./app.ts";
+
+// Serves whichever release `live` points at in app.db, with docs at /api/rest/latest/docs.
+Layer.launch(Server).pipe(NodeRuntime.runMain);
diff --git a/docs/snippets/home/site-safety/story.ts b/docs/snippets/home/site-safety/story.ts
new file mode 100644
index 0000000..9f1823f
--- /dev/null
+++ b/docs/snippets/home/site-safety/story.ts
@@ -0,0 +1,100 @@
+/**
+ * The rest of the homepage story: onboarding, training evidence, a retried
+ * command, and the auditor's correction. `run.ts` plays these in order.
+ */
+import { Effect } from "effect";
+import { EntityId, Triples } from "@triplex-build/triplex";
+import type { InMemoryConfigStore } from "@triplex-build/triplex/config";
+import { Site, Worker, harbor, maria, placeWorker } from "./app.ts";
+
+export const JAN_5 = Date.UTC(2026, 0, 5);
+export const MAR_1 = Date.UTC(2026, 2, 1);
+export const MAR_5 = Date.UTC(2026, 2, 5);
+export const JUN_1 = Date.UTC(2026, 5, 1);
+export const pier = EntityId.make("site:pier");
+
+export const onboard = Effect.gen(function* () {
+ const triples = yield* Triples;
+ const since = { validFrom: JAN_5 };
+ return yield* triples.transact(
+ [
+ {
+ op: "assert",
+ entityId: harbor,
+ entityType: "Site",
+ ...Site.name.assertion("Harbor", since),
+ },
+ { op: "assert", entityId: pier, entityType: "Site", ...Site.name.assertion("Pier", since) },
+ {
+ op: "assert",
+ entityId: maria,
+ entityType: "Worker",
+ ...Worker.name.assertion("Maria", since),
+ },
+ ],
+ { actor: "user:hr", commandId: "onboard:maria" },
+ );
+});
+
+// Evidence holds for a window of business time: [validFrom, validTo).
+export const recordTraining = Effect.gen(function* () {
+ const triples = yield* Triples;
+ return yield* triples.transact(
+ [
+ {
+ op: "assert",
+ entityId: maria,
+ entityType: "Worker",
+ ...Worker.trainedFor.assertion(harbor, { validFrom: MAR_1 }),
+ validTo: JUN_1,
+ },
+ ],
+ { actor: "user:trainer", commandId: "training:record:maria-harbor" },
+ );
+});
+
+// #region retry
+// A retried command must never apply twice. The duplicate is rejected with the
+// original transaction, so the caller can answer with the first receipt.
+export const placeWorkerOnce = (release: InMemoryConfigStore.ConfigSnapshot) =>
+ placeWorker(release, EntityId.make("placement:maria-harbor"), maria, harbor).pipe(
+ Effect.map((result) => result.position),
+ Effect.catchTag("CommandAlreadyCommittedError", (duplicate) =>
+ Effect.gen(function* () {
+ const triples = yield* Triples;
+ const receipt = yield* triples.transactionByCommand(duplicate.commandId);
+ return receipt?.position;
+ }),
+ ),
+ );
+// #endregion retry
+
+// #region correct
+// A correction never deletes history. It retracts the recorded belief and
+// asserts what is now known, in one attributed transaction.
+export const correctTraining = Effect.gen(function* () {
+ const triples = yield* Triples;
+ const [wrong] = yield* triples.match(
+ { entityId: maria, attribute: Worker.trainedFor.key },
+ { validAt: MAR_1 },
+ );
+ if (wrong === undefined) return yield* Effect.die("no training evidence to correct");
+ return yield* triples.transact(
+ [
+ { op: "retract", id: wrong.id },
+ {
+ op: "assert",
+ entityId: maria,
+ entityType: "Worker",
+ ...Worker.trainedFor.assertion(harbor, { validFrom: MAR_5 }),
+ validTo: JUN_1,
+ },
+ ],
+ {
+ actor: "user:auditor",
+ commandId: "training:correct:maria-harbor",
+ causationId: "audit:2026-q1",
+ },
+ );
+});
+// #endregion correct
diff --git a/docs/snippets/home/site-safety/what-we-knew.ts b/docs/snippets/home/site-safety/what-we-knew.ts
deleted file mode 100644
index 32e488d..0000000
--- a/docs/snippets/home/site-safety/what-we-knew.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { Effect } from "effect";
-import { Triples, type TemporalBasis } from "@triplex-build/triplex";
-import { TrainedFor, maria } from "./model.ts";
-import { MAR_2 } from "./record.ts";
-
-// Was Maria trained for Harbor on the day she started there?
-export const wasMariaTrained = Effect.gen(function* () {
- const triples = yield* Triples;
- const decision = yield* triples.transactionByCommand("placement:create:maria-harbor");
- if (decision === null) return yield* Effect.die("placement was never recorded");
-
- const trained = (basis: TemporalBasis) =>
- triples
- .match({ entityId: maria, attribute: TrainedFor.key }, basis)
- .pipe(Effect.map((facts) => facts.length > 0));
-
- return {
- // What the database believed at the instant Dana made the placement.
- knownAtDecision: yield* trained({ validAt: MAR_2, recordedAt: decision.instant }),
- // What the database knows now, after the auditor's correction.
- knownToday: yield* trained({ validAt: MAR_2 }),
- };
-});
diff --git a/docs/snippets/home/site-safety/which-rules.ts b/docs/snippets/home/site-safety/which-rules.ts
deleted file mode 100644
index 5b2202c..0000000
--- a/docs/snippets/home/site-safety/which-rules.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { Effect } from "effect";
-import { Triples } from "@triplex-build/triplex";
-import { ConfigStore, ContentId } from "@triplex-build/triplex/config";
-
-// Who placed Maria, and under which version of the rules?
-export const governingRules = Effect.gen(function* () {
- const triples = yield* Triples;
- const config = yield* ConfigStore.ConfigStore;
- const receipt = yield* triples.transactionByCommand("placement:create:maria-harbor");
- const pinned = receipt?.configSnapshot;
- if (pinned === undefined || !ContentId.isContentId(pinned)) {
- return yield* Effect.die("placement was not pinned to a release");
- }
-
- return {
- actor: receipt?.actor,
- correlationId: receipt?.correlationId,
- governedBy: (yield* config.snapshotById(pinned))?.label,
- liveToday: (yield* config.resolveRef("live"))?.label,
- };
-});
diff --git a/docs/snippets/tsconfig.json b/docs/snippets/tsconfig.json
index e1d4c12..f845482 100644
--- a/docs/snippets/tsconfig.json
+++ b/docs/snippets/tsconfig.json
@@ -14,7 +14,8 @@
"@triplex-build/triplex/derivation": ["packages/core/src/derivation/index.ts"],
"@triplex-build/triplex-postgres": ["packages/postgres/src/index.ts"],
"@triplex-build/triplex/internal": ["packages/core/src/internal.ts"],
- "@triplex-build/triplex-sql": ["packages/sql/src/index.ts"]
+ "@triplex-build/triplex-sql": ["packages/sql/src/index.ts"],
+ "@triplex-build/triplex-http": ["packages/http/src/index.ts"]
},
"allowImportingTsExtensions": true,
"types": ["node"]
diff --git a/e2e/docs-home.spec.ts b/e2e/docs-home.spec.ts
index 88445c2..cd19e87 100644
--- a/e2e/docs-home.spec.ts
+++ b/e2e/docs-home.spec.ts
@@ -28,16 +28,19 @@ for (const colorScheme of ["light", "dark"] as const) {
page.getByRole("heading", { name: "Reading this as a coding agent?" }),
).toBeVisible();
- // The journal table and every question are rendered from verified scenario outputs.
+ // The stack map, journal table, and every step are rendered from verified scenario outputs.
+ const stack = page.locator(".stack-map");
+ await expect(stack.getByRole("link")).toHaveCount(6);
+ await expect(stack).toContainText(/\d+ lines from schema to REST API/);
const timeline = page.locator(".triplex-home__timeline table");
await expect(timeline.locator("tbody tr")).toHaveCount(6);
- await expect(timeline).toContainText("placement:create:maria-harbor");
+ await expect(timeline).toContainText("create:placement:maria-harbor");
const examples = page.locator('.triplex-home__snippet div[class*="language-"]');
- await expect(examples).toHaveCount(8);
+ await expect(examples).toHaveCount(11);
for (const example of await examples.all()) {
await expect(example).toBeVisible();
}
- await expect(content).toContainText('"knownAtDecision": true');
+ await expect(content).toContainText('"trainedAsKnownThen": true');
await expect(content).toContainText('"governedBy": "hr-2026.1"');
await expect(page.getByRole("tab")).toHaveCount(0);
@@ -52,7 +55,7 @@ test("home examples remain visible on mobile without page overflow", async ({ pa
await page.setViewportSize({ width: 390, height: 844 });
await page.goto("/");
const examples = page.locator('.triplex-home__snippet div[class*="language-"]');
- await expect(examples).toHaveCount(8);
+ await expect(examples).toHaveCount(11);
for (const example of await examples.all()) {
await expect(example).toBeVisible();
}
@@ -91,7 +94,7 @@ test("agent-readable documentation is served as plain text", async ({ request })
const markdown = await guide.text();
expect(markdown).toMatch(/^# Modeling a back-office domain/);
// Code imports are resolved into fenced code rather than left as VitePress directives.
- expect(markdown).toContain('commandId: "placement:create:maria-harbor"');
+ expect(markdown).toContain("commandId: `create:${placement}`");
expect(markdown).not.toContain("<<<");
const full = await request.get("/llms-full.txt");
diff --git a/e2e/docs-home.spec.ts-snapshots/home-dark.png b/e2e/docs-home.spec.ts-snapshots/home-dark.png
index 13b5502..7d997c4 100644
Binary files a/e2e/docs-home.spec.ts-snapshots/home-dark.png and b/e2e/docs-home.spec.ts-snapshots/home-dark.png differ
diff --git a/e2e/docs-home.spec.ts-snapshots/home-light.png b/e2e/docs-home.spec.ts-snapshots/home-light.png
index 209b2cd..11814b3 100644
Binary files a/e2e/docs-home.spec.ts-snapshots/home-light.png and b/e2e/docs-home.spec.ts-snapshots/home-light.png differ