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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 12 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,18 @@ Configuration lives at exactly one of two levels: in the package (`eslint.config

## Packages

| Package | What it is |
| -------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`graph-dag`](packages/core/graph/dag/README.md) | Model-independent dependency-graph primitives over a node set and a `needs` accessor: layers, ancestors, descendants, cycles, transitive reduction, critical path. |
| [`graph-schema`](packages/core/graph/schema/README.md) | The process model: typed identifier spaces, `defineModel` with compile-time reference checking, structural validation. |
| [`graph-projection`](packages/core/graph/projection/README.md) | Projections of the model into a flowchart, swimlane, state machine and sequence diagram, plus derived hand-offs. |
| [`graph-domain`](packages/core/graph/domain/README.md) | Reasoning over the model and a run overlay: control graph, illegal cycles, three-valued status, ready set, blocked-by, impact, what-if and critical path. |
| [`graph-layout`](packages/core/graph/layout/README.md) | Layered layout of a projected flowchart, swimlane or state machine into positions in the model's `views` shape. |
| [`graph-source-contract`](packages/core/graph/source-contract/README.md) | The `GraphSource` port as an oRPC contract: versioned, paged and streamed sync of bound activities, participants, transitions and run state, resumable by cursor, with an in-memory fake and a conformance kit. |
| [`graph-source-github-issues`](packages/adapters/graph/source-github-issues/README.md) | The GitHub issues adapter for the `GraphSource` port: issues as bound activities, `blockedBy` as transitions, sub-issues, repositories, organisations and assignees, across organisations and repositories. |
| [`graph-store-contract`](packages/core/graph/store-contract/README.md) | The persistence port as an oRPC contract: save, load and delete models, set run state, upsert items from a source, structural ready, blocked-chain and impact queries, and a change feed; with an in-memory fake and a conformance kit. |
| [`process-graph`](packages/targets/process-graph/README.md) | The primary entry point: re-exports the schema, projection and domain APIs flat and the DAG API as the `dag` namespace. |
| Package | What it is |
| -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`graph-dag`](packages/core/graph/dag/README.md) | Model-independent dependency-graph primitives over a node set and a `needs` accessor: layers, ancestors, descendants, cycles, transitive reduction, critical path. |
| [`graph-schema`](packages/core/graph/schema/README.md) | The process model: typed identifier spaces, `defineModel` with compile-time reference checking, structural validation. |
| [`graph-projection`](packages/core/graph/projection/README.md) | Projections of the model into a flowchart, swimlane, state machine and sequence diagram, plus derived hand-offs. |
| [`graph-domain`](packages/core/graph/domain/README.md) | Reasoning over the model and a run overlay: control graph, illegal cycles, three-valued status, ready set, blocked-by, impact, what-if and critical path. |
| [`graph-layout`](packages/core/graph/layout/README.md) | Layered layout of a projected flowchart, swimlane or state machine into positions in the model's `views` shape. |
| [`graph-source-contract`](packages/core/graph/source-contract/README.md) | The `GraphSource` port as an oRPC contract: versioned, paged and streamed sync of bound activities, participants, transitions and run state, resumable by cursor, with an in-memory fake and a conformance kit. |
| [`graph-source-github-issues`](packages/adapters/graph/source-github-issues/README.md) | The GitHub issues adapter for the `GraphSource` port: issues as bound activities, `blockedBy` as transitions, sub-issues, repositories, organisations and assignees, across organisations and repositories. |
| [`graph-store-contract`](packages/core/graph/store-contract/README.md) | The persistence port as an oRPC contract: save, load and delete models, set run state, upsert items from a source, structural ready, blocked-chain and impact queries, and a change feed; with an in-memory fake and a conformance kit. |
| [`graph-store-sqlite`](packages/adapters/graph/store-sqlite/README.md) | The store port on SQLite through Drizzle: one table definition, committed drizzle-kit migrations applied the same way in every runtime, recursive SQL for the structural queries, against Drizzle's async SQLite database so any driver plugs in. |
| [`process-graph`](packages/targets/process-graph/README.md) | The primary entry point: re-exports the schema, projection and domain APIs flat and the DAG API as the `dag` namespace. |

## Conventions

Expand Down
1 change: 1 addition & 0 deletions packages/adapters/graph/store-sqlite/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
src/generated/
1 change: 1 addition & 0 deletions packages/adapters/graph/store-sqlite/AGENTS.md
1 change: 1 addition & 0 deletions packages/adapters/graph/store-sqlite/CLAUDE.md
39 changes: 39 additions & 0 deletions packages/adapters/graph/store-sqlite/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# @exadev/process-graph-store-sqlite

[![GitHub](https://img.shields.io/badge/GitHub-181717?logo=github&logoColor=white)](https://github.com/ExaDev/process-graph) [![npm](https://img.shields.io/badge/npm-CB3837?logo=npm&logoColor=white)](https://www.npmjs.com/package/@exadev/process-graph-store-sqlite) [![Release](https://img.shields.io/github/v/release/ExaDev/process-graph)](https://github.com/ExaDev/process-graph/releases/latest) [![CI](https://img.shields.io/github/actions/workflow/status/ExaDev/process-graph/ci.yml?branch=main)](https://github.com/ExaDev/process-graph/actions)

The `GraphStore` port from `@exadev/process-graph-store-contract` on SQLite through [Drizzle](https://orm.drizzle.team) (`drizzle-orm` 1.0.0-rc.4). It is written against Drizzle's async SQLite database type, so any Drizzle SQLite driver plugs in: `@exadev/process-graph-sqlite-node` supplies `node:sqlite`, `@exadev/process-graph-sqlite-wasm` supplies SQLite in a browser worker on OPFS. `src/` imports nothing Node-only.

```ts
import { createGraphStoreRouter } from "@exadev/process-graph-store-contract";
import { createSqliteGraphStore } from "@exadev/process-graph-store-sqlite";
import { createRouterClient } from "@orpc/server";

const store = createRouterClient(
createGraphStoreRouter(createSqliteGraphStore({ db })),
);
```

## Schema

`src/tables.ts` is the one table definition. Every record a model declares is a row keyed by model id and record id, with a `position` column keeping the declared order; enumerated columns take their values from graph-schema's own vocabulary, and nested values are JSON text. Rows never reach the contract: `src/rows.ts` flattens a model into rows and reassembles it, parsing the result with graph-schema's `modelSchema`, so what leaves the store is checked to be a model. A unit test derives the row schemas with `drizzle-orm/zod` and asserts they agree with the model schema on every shared scalar field.

Each transition row records whether graph-domain's `loopTransitions` counts it as a decision's loop edge, so the structural queries can leave loop edges out without loading the model. No foreign keys are declared, because SQLite enforces them only on connections that enable them; the store writes and deletes a model's rows together in one transaction instead.

## Migrations

The SQL migrations under `drizzle/` are generated by drizzle-kit from `src/tables.ts` and committed: once data exists they are the schema's history. After changing the tables, run `pnpm run migrations:generate` and commit the new folder.

The build embeds them in the package (`scripts/embed-migrations.ts`, parsed by drizzle's own `readMigrationFiles`; the generated module is build output and never committed), and `migrate` applies the missing ones in one transaction, recording each in drizzle's `__drizzle_migrations` journal by the same name and hash drizzle's migrators use. The store migrates on its first operation. Drizzle's own migrators cannot serve every runtime: `drizzle-orm/migrator` reads the folder through `node:fs`, which a browser worker does not have, and `migrateAsync` reads its journal in object mode, which the `sqlite-proxy` session refuses. A unit test checks that `migrate` builds the same schema and journal as drizzle's `node-sqlite` migrator reading the committed folder, and that each finds nothing left to apply after the other. The decision and the evidence are in [#22](https://github.com/ExaDev/process-graph/issues/22).

## Writes and queries

Every write runs `checkStored` from the contract before it commits, takes the next value of the store-wide revision sequence, rewrites the model's rows in one transaction and publishes the change to the store's `ChangeFeed` once it has committed. A failed check or an aborted signal rolls the transaction back.

`query.ready`, `query.blockedChain` and `query.impact` are single `WITH RECURSIVE` queries through Drizzle's `sql` template over `transitions`, `elements` and `overlay`, with the contract's semantics (control elements passed through, loop edges left out, traversal stopping at done work). The walks cap distances at the model's element count, which is longer than any shortest path and ends the recursion on a cycle no decision closes.

Transactions are opened with plain `BEGIN IMMEDIATE`, `COMMIT` and `ROLLBACK` statements rather than Drizzle's `transaction`, whose callback has to be synchronous on a synchronous driver and asynchronous on an async one. Each store operation therefore runs with the connection to itself: by default the store serialises its own operations, and a host that shares the database across contexts passes `exclusive` to hold its cross-context lock as well, and `feed` to bridge the other contexts' changes.

## Tests

The contract's conformance kit runs against the store over `node:sqlite` in memory, and further tests cover reopening a database file, the migrations and the row schemas.
8 changes: 8 additions & 0 deletions packages/adapters/graph/store-sqlite/drizzle.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { defineConfig } from "drizzle-kit";

/** drizzle-kit reads the one table definition and writes each migration as SQL under `drizzle/`, which is committed: once data exists, the migrations are the schema's history and cannot be rebuilt from the schema alone. */
export default defineConfig({
dialect: "sqlite",
schema: "./src/tables.ts",
out: "./drizzle",
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
CREATE TABLE `actions` (
`model_id` text NOT NULL,
`id` text NOT NULL,
`position` integer NOT NULL,
`name` text NOT NULL,
CONSTRAINT `actions_pk` PRIMARY KEY(`model_id`, `id`)
);
--> statement-breakpoint
CREATE TABLE `artefacts` (
`model_id` text NOT NULL,
`id` text NOT NULL,
`position` integer NOT NULL,
`name` text NOT NULL,
`deliverable` integer,
CONSTRAINT `artefacts_pk` PRIMARY KEY(`model_id`, `id`)
);
--> statement-breakpoint
CREATE TABLE `elements` (
`model_id` text NOT NULL,
`id` text NOT NULL,
`position` integer NOT NULL,
`kind` text NOT NULL,
`label` text,
`actor` text,
`action` text,
`source` text,
`affects` text,
`consumes` text,
`produces` text,
`parent` text,
`entry` text,
`exit` text,
`role` text,
`from_actor` text,
`to_actor` text,
`mode` text,
`event` text,
`extensions` text,
CONSTRAINT `elements_pk` PRIMARY KEY(`model_id`, `id`)
);
--> statement-breakpoint
CREATE TABLE `events` (
`model_id` text NOT NULL,
`id` text NOT NULL,
`position` integer NOT NULL,
`name` text NOT NULL,
`payload` text,
CONSTRAINT `events_pk` PRIMARY KEY(`model_id`, `id`)
);
--> statement-breakpoint
CREATE TABLE `fragments` (
`model_id` text NOT NULL,
`id` text NOT NULL,
`position` integer NOT NULL,
`operator` text NOT NULL,
`operands` text NOT NULL,
CONSTRAINT `fragments_pk` PRIMARY KEY(`model_id`, `id`)
);
--> statement-breakpoint
CREATE TABLE `models` (
`id` text PRIMARY KEY,
`revision` integer NOT NULL,
`views` text
);
--> statement-breakpoint
CREATE TABLE `overlay` (
`model_id` text NOT NULL,
`element` text NOT NULL,
`status` text NOT NULL,
CONSTRAINT `overlay_pk` PRIMARY KEY(`model_id`, `element`)
);
--> statement-breakpoint
CREATE TABLE `participants` (
`model_id` text NOT NULL,
`id` text NOT NULL,
`position` integer NOT NULL,
`label` text NOT NULL,
`kind` text NOT NULL,
CONSTRAINT `participants_pk` PRIMARY KEY(`model_id`, `id`)
);
--> statement-breakpoint
CREATE TABLE `projections` (
`model_id` text NOT NULL,
`position` integer NOT NULL,
`id` text NOT NULL,
`notation` text NOT NULL,
`subject` text,
`scenario` text,
CONSTRAINT `projections_pk` PRIMARY KEY(`model_id`, `position`)
);
--> statement-breakpoint
CREATE TABLE `sequences` (
`name` text PRIMARY KEY,
`value` integer NOT NULL
);
--> statement-breakpoint
CREATE TABLE `transitions` (
`model_id` text NOT NULL,
`id` text NOT NULL,
`position` integer NOT NULL,
`source` text NOT NULL,
`target` text NOT NULL,
`trigger` text,
`guard` text,
`condition` text,
`effect` text,
`loop` integer NOT NULL,
CONSTRAINT `transitions_pk` PRIMARY KEY(`model_id`, `id`)
);
--> statement-breakpoint
CREATE INDEX `transitions_by_target` ON `transitions` (`model_id`,`target`);--> statement-breakpoint
CREATE INDEX `transitions_by_source` ON `transitions` (`model_id`,`source`);
Loading
Loading