From b831a98bb9c750848fb455a20be505e7d5fafdfd Mon Sep 17 00:00:00 2001
From: Heliton Nordt <1625399+hnordt@users.noreply.github.com>
Date: Tue, 28 Jul 2026 12:17:12 -0300
Subject: [PATCH 01/20] chore: establish local package workspaces
---
AGENTS.md | 1 +
.../adr/0001-path-and-pathname-terminology.md | 90 +++++++++++++++++++
docs/adr/README.md | 49 ++++++++++
docs/design/0005-local-package-workspaces.md | 86 ++++++++++++++++++
docs/design/0006-sqlite-connection-package.md | 84 +++++++++++++++++
docs/design/README.md | 54 ++++++-----
package-lock.json | 29 +++++-
package.json | 6 +-
packages/kernel/package.json | 11 +++
packages/kernel/src/index.ts | 7 ++
packages/sqlite/package.json | 14 +++
packages/sqlite/src/index.ts | 28 ++++++
src/lib/kernel-package.test.ts | 6 ++
src/lib/server/db/index.ts | 18 +---
src/lib/server/db/open-database.test.ts | 21 +++++
15 files changed, 461 insertions(+), 43 deletions(-)
create mode 100644 docs/adr/0001-path-and-pathname-terminology.md
create mode 100644 docs/adr/README.md
create mode 100644 docs/design/0005-local-package-workspaces.md
create mode 100644 docs/design/0006-sqlite-connection-package.md
create mode 100644 packages/kernel/package.json
create mode 100644 packages/kernel/src/index.ts
create mode 100644 packages/sqlite/package.json
create mode 100644 packages/sqlite/src/index.ts
create mode 100644 src/lib/kernel-package.test.ts
create mode 100644 src/lib/server/db/open-database.test.ts
diff --git a/AGENTS.md b/AGENTS.md
index 7c6f7e2..187201c 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -197,6 +197,7 @@ Infer TypeScript types from Zod schemas instead of duplicating manual interfaces
- Prefer domain names over generic names such as `data`, `item`, `handler`, `manager`, `helper`, or `utils`.
- Prefer standard ECMAScript, Web Platform, Svelte, SvelteKit, and CSS APIs over project-specific wrappers.
- Do not extract a one-use helper unless it creates a real boundary, names a domain invariant, or materially reduces complexity.
+- Do not introduce a one-use local merely to restate a short, readable expression. Keep a local when it names a domain concept or boundary, avoids repeated or side-effectful evaluation, enables type narrowing or cleanup, or materially improves readability.
- Avoid unrelated renaming, formatting, file movement, or refactoring.
- Preserve public contracts unless the task explicitly changes them.
- Separate verified implementation from target architecture in code comments and documentation.
diff --git a/docs/adr/0001-path-and-pathname-terminology.md b/docs/adr/0001-path-and-pathname-terminology.md
new file mode 100644
index 0000000..209540f
--- /dev/null
+++ b/docs/adr/0001-path-and-pathname-terminology.md
@@ -0,0 +1,90 @@
+# Path and pathname terminology
+
+| Field | Value |
+| ------------- | ---------------------------------------------------- |
+| Status | Accepted |
+| Scope | Public APIs, configuration, and implementation names |
+| Decision date | 2026-07-27 |
+
+## Summary
+
+Hyperkernel uses `path` for a general location accepted by an API, and
+`pathname` only for the path component of a URL. The two names are not
+interchangeable.
+
+## Problem
+
+Location-like inputs can be filesystem paths, opaque identifiers accepted by an
+API, complete URLs, or one component of a URL. Calling each of them `path` or
+`pathname` without a rule makes public APIs harder to understand and obscures
+what inputs they accept.
+
+## Decision
+
+Use `path` when an API accepts a general location. It may be a filesystem path,
+an opaque location syntax documented by that API, or a complete `URL` when the
+underlying API accepts one.
+
+```ts
+const configurationPath = "./config/hyperkernel.json";
+const memoryPath = ":memory:";
+declare const resourcePath: URL;
+```
+
+Use `pathname` only when referring to the pathname component of a URL. It is a
+string such as `/workspaces/acme`, not the complete URL.
+
+```ts
+const url = new URL("https://example.test/workspaces/acme?tab=events");
+
+url.pathname; // "/workspaces/acme"
+url.href; // "https://example.test/workspaces/acme?tab=events"
+```
+
+Use `url` when an API specifically requires a complete URL and does not accept
+another kind of location.
+
+```ts
+function redirect(url: URL): Response {
+ return Response.redirect(url);
+}
+```
+
+This ADR governs names in programmatic APIs, configuration fields, types, and
+developer documentation. It does not prohibit ordinary prose such as "command
+execution path" where no location value is being named.
+
+## Examples
+
+| Value or concept | Name | Reason |
+| ---------------------------------------------------------------------- | ---------- | -------------------------------------------------------------- |
+| `./config/hyperkernel.json` supplied to a configuration reader | `path` | It is a filesystem location. |
+| `:memory:` supplied to a storage API | `path` | It is an API-specific location, not a URL pathname. |
+| `new URL("file:///var/lib/hyperkernel.db")` supplied to a resource API | `path` | The receiving API accepts a general location, including a URL. |
+| `/projects/hyperkernel` from `new URL(...).pathname` | `pathname` | It is precisely the URL pathname component. |
+| `https://example.test/projects/hyperkernel?view=events` | `url` | The complete URL includes origin and query string. |
+| A route matcher input derived from `request.url` | `pathname` | Routing commonly operates on only the URL pathname. |
+
+## Alternatives considered
+
+### Use `path` for every location-like value
+
+Rejected. It would obscure whether a value is a complete URL or only its path
+component.
+
+### Use `pathname` for all path values
+
+Rejected. `pathname` has a precise URL-specific meaning and does not describe
+filesystem paths or API-specific location syntax.
+
+## Consequences
+
+API names communicate valid input shapes without relying on implementation
+knowledge. A `pathname` must never be documented as a complete URL, and a
+`path` must not be assumed to be a URL pathname.
+
+## Status history
+
+| Date | Status | Note |
+| ---------- | -------- | ------------------------------------------------------ |
+| 2026-07-27 | Accepted | Global terminology chosen for current and future APIs. |
diff --git a/docs/adr/README.md b/docs/adr/README.md
new file mode 100644
index 0000000..4036a65
--- /dev/null
+++ b/docs/adr/README.md
@@ -0,0 +1,49 @@
+# Architecture decision records
+
+An Architecture Decision Record (ADR) documents an accepted, durable decision
+that guides Hyperkernel's implementation and public contracts. An ADR explains
+what was decided, why it was chosen, and the consequences that future work must
+respect.
+
+ADRs are not exploratory design documents. A decision can later be replaced,
+but it remains accepted until a newer ADR supersedes it.
+
+## Records
+
+| Record | Status |
+| ---------------------------------------------------------------------------- | -------- |
+| [0001: Path and pathname terminology](0001-path-and-pathname-terminology.md) | Accepted |
+
+## Relationship to design records
+
+[`../design/`](../design/README.md) contains ideas that progress through
+design stages. A design that is fully approved and tested may be promoted to
+an ADR when it establishes a durable rule, boundary, or public contract.
+
+The design record remains as the history and evidence for the decision. The
+resulting ADR links to that design record, and the design record links to the
+ADR. Not every completed design needs an ADR; create one only when the decision
+must guide future work beyond its original implementation.
+
+An ADR may also be created directly when a small, well-understood decision does
+not need a separate design process.
+
+## Status
+
+| Status | Meaning |
+| ---------- | ------------------------------------------------------------------ |
+| Accepted | The decision is active and guides current work. |
+| Superseded | A newer ADR replaced the decision; retain this record for history. |
+
+## Structure
+
+Each ADR contains:
+
+1. metadata with its status and decision date;
+2. a concise summary and the problem being decided;
+3. the decision and examples or rules needed to apply it;
+4. considered alternatives and consequences;
+5. links to any originating design record or replacement ADR.
+
+Number ADRs sequentially within this directory. The number is a stable identity
+and is not reused.
diff --git a/docs/design/0005-local-package-workspaces.md b/docs/design/0005-local-package-workspaces.md
new file mode 100644
index 0000000..500f450
--- /dev/null
+++ b/docs/design/0005-local-package-workspaces.md
@@ -0,0 +1,86 @@
+# Local package workspaces
+
+| Field | Value |
+| ------ | ---------------------------------------------------- |
+| Status | Development |
+| Scope | Repository architecture and local package boundaries |
+| Date | 2026-07-27 |
+
+## Summary
+
+Hyperkernel will use npm workspaces to develop the SvelteKit application and
+future reusable packages in one repository. Workspace packages are consumed by
+their package names locally before any publication to npm.
+
+## Problem and invariants
+
+The current repository is one SvelteKit application. It needs an incremental
+path toward a reusable kernel and UI packages without duplicating development
+tooling or making the kernel depend on SvelteKit.
+
+- The SvelteKit application remains the executable prototype during this step.
+- Formatting, linting, type-checking, and test tooling stay at the repository
+ root.
+- The kernel public API must not depend on SvelteKit routes, UI state, or
+ server-only SQLite implementation details.
+- No package is published or supported for external installation yet.
+
+## Decision
+
+The repository root is a private npm workspace coordinator named
+`hyperkernel-workspace`. It declares `packages/*` as workspaces.
+
+The initial workspaces are:
+
+- `packages/kernel`, named `@hyperkernel/kernel`, whose empty public entry point
+ establishes the future import boundary without moving any existing kernel
+ implementation; and
+- `packages/sqlite`, named `@hyperkernel/sqlite`, which extracts the existing
+ Node.js SQLite connection and baseline pragmas from the SvelteKit application.
+
+Both packages remain private until an explicit publication decision and
+packaging verification are complete.
+
+The existing root SvelteKit application stays in place for now. Moving it under
+`apps/` and introducing a UI package are later migrations, not prerequisites
+for validating the first workspace boundary.
+
+## Alternatives considered
+
+### Publish `src/lib` from the application
+
+Rejected. `src/lib` is the application's internal library. Publishing it would
+make application aliases and accidental SvelteKit dependencies part of a public
+package boundary.
+
+### Move the SvelteKit application and all packages at once
+
+Rejected. This would combine package resolution, build configuration, test
+paths, and code extraction into one hard-to-debug migration.
+
+### Use separate repositories immediately
+
+Rejected. Local npm workspaces provide the desired package-name imports while
+keeping the prototype changes atomic and easy to inspect.
+
+## Consequences and limitations
+
+`@hyperkernel/kernel` is locally addressable but intentionally exports no
+production API in this step. `@hyperkernel/sqlite` is Node-only and exposes the
+low-level connection API described in
+[`0006-sqlite-connection-package.md`](0006-sqlite-connection-package.md). A
+later step must select and verify distribution builds before either package can
+be published.
+
+## Evidence required for Evaluation
+
+- npm installs the workspace successfully from the repository root.
+- The root application continues to pass its existing checks.
+- The root application resolves both packages through their package names.
+- The SQLite connection behavior remains covered after extraction.
+
+## Status history
+
+| Date | Status | Note |
+| ---------- | ----------- | ------------------------------------------------- |
+| 2026-07-27 | Development | Chosen for the initial local workspace migration. |
diff --git a/docs/design/0006-sqlite-connection-package.md b/docs/design/0006-sqlite-connection-package.md
new file mode 100644
index 0000000..8675b09
--- /dev/null
+++ b/docs/design/0006-sqlite-connection-package.md
@@ -0,0 +1,84 @@
+# SQLite connection package
+
+| Field | Value |
+| ------ | ----------------------------- |
+| Status | Development |
+| Scope | SQLite infrastructure adapter |
+| Date | 2026-07-27 |
+
+## Summary
+
+`@hyperkernel/sqlite` provides the Node.js SQLite connection used by
+Hyperkernel applications. Its first public API is `openDatabase`, which opens
+a database and returns the underlying `DatabaseSync` instance.
+
+## Problem and invariants
+
+The SvelteKit prototype currently owns a single server-only SQLite connection
+and its baseline pragmas. That infrastructure must become reusable without
+making the kernel depend on SvelteKit or allowing SQLite to be presented as a
+database-agnostic contract.
+
+- The package is Node-only and uses the built-in `node:sqlite` module.
+- `openDatabase` opens an existing database or lets SQLite create a missing
+ file-backed database.
+- The caller owns and closes the returned `DatabaseSync` instance.
+- The adapter preserves the prototype's timeout, WAL, synchronous, and foreign
+ key settings.
+- Opening `:memory:` warns that data is lost when the process stops.
+- This low-level connection API is not a command, event, or authorization API.
+
+## Decision
+
+Expose `openDatabase(path = ":memory:", options = {})` from
+`@hyperkernel/sqlite`. `path` accepts a filesystem path, `:memory:`, or a URL
+accepted by `DatabaseSync`. The optional `options` argument currently accepts
+`timeout`. The function returns `DatabaseSync` directly.
+
+The name is deliberately short because the SQLite-specific package name
+supplies the missing context:
+
+```ts
+import { openDatabase } from "@hyperkernel/sqlite";
+```
+
+`@hyperkernel/db` is reserved for a future database-agnostic contract only if
+multiple adapters establish a real shared abstraction.
+
+## Alternatives considered
+
+### `createDatabase`
+
+Rejected. The operation also opens existing databases, so `create` would imply
+an inaccurate lifecycle guarantee.
+
+### `openSqliteDatabase`
+
+Rejected. It is correct but redundant at the package boundary.
+
+### Return a Hyperkernel-specific storage interface
+
+Deferred. The production kernel persistence contract does not yet exist. A
+premature abstraction would hide SQLite behavior before another adapter has
+shown which operations genuinely need to be shared.
+
+## Consequences and limitations
+
+Consumers receive a raw SQLite connection and can execute arbitrary SQL. This
+is appropriate for an infrastructure package but does not grant a supported
+path around the future kernel command boundary. The package is private and not
+yet prepared for npm publication.
+
+## Evidence required for Evaluation
+
+- The SvelteKit server connection uses the package rather than creating its own
+ `DatabaseSync` instance.
+- A test proves that `openDatabase` returns `DatabaseSync` and enables foreign
+ keys.
+- The application type-checks with the workspace dependency.
+
+## Status history
+
+| Date | Status | Note |
+| ---------- | ----------- | ----------------------------------------------------- |
+| 2026-07-27 | Development | Chosen for the first reusable infrastructure adapter. |
diff --git a/docs/design/README.md b/docs/design/README.md
index 4dd145e..e866499 100644
--- a/docs/design/README.md
+++ b/docs/design/README.md
@@ -1,14 +1,13 @@
# Design records
-Design records document significant Hyperkernel decisions whose reasoning,
-implementation maturity, and compatibility consequences must remain reviewable.
-They complement the public architecture in `README.md` and the canonical
-engineering rules in `AGENTS.md`.
+Design records capture Hyperkernel ideas while they are proposed, developed,
+and evaluated. They preserve the reasoning, evidence, and unresolved questions
+needed to reach a decision. They complement the public architecture in
+`README.md` and the canonical engineering rules in `AGENTS.md`.
-Use a design record for a kernel contract, a public extension contract, or
-another decision whose alternatives and long-term consequences matter. Do not
-create one for routine implementation details that are adequately explained by
-code and tests.
+Use a design record when a significant idea needs deliberate exploration before
+it becomes a durable decision. Do not create one for routine implementation
+details that are adequately explained by code and tests.
## Records
@@ -18,30 +17,37 @@ code and tests.
| [0002: Event-sourced persistence with SQLite](0002-event-sourced-persistence-with-sqlite.md) | Development |
| [0003: Web-platform-first frontend](0003-web-platform-first-frontend.md) | Development |
| [0004: Interface design philosophy](0004-interface-design-philosophy.md) | Development |
+| [0005: Local package workspaces](0005-local-package-workspaces.md) | Development |
+| [0006: SQLite connection package](0006-sqlite-connection-package.md) | Development |
## Authority
-`README.md` and `AGENTS.md` remain canonical. A design record elaborates those
-contracts but does not silently override them.
-
-- Draft, Development, and Evaluation records do not define stable contracts.
-- A Stable record may elaborate a canonical contract.
-- Once Stable, a record should receive only editorial clarifications.
-- A material change requires a new record. The previous record becomes Legacy
- and links to its replacement.
-- Spikes may provide evidence for a record but never define supported behavior.
+`README.md` and `AGENTS.md` remain canonical. A design record does not silently
+override them or establish a durable contract.
+
+- Draft, Development, and Evaluation records are ideas, not accepted decisions.
+- A Stable design is fully approved and tested for its intended scope.
+- When a Stable design establishes a durable rule, boundary, or public contract,
+ create an ADR in [`../adr/`](../adr/README.md) to record the accepted
+ decision. Keep the design record as the history and evidence, with links in
+ both directions.
+- Not every Stable design needs an ADR; routine or local decisions may remain
+ documented in code and tests.
+- A material change to an accepted decision requires a new ADR that supersedes
+ the earlier one.
+- Spikes may provide evidence for a design but never define supported behavior.
## Status
Design records advance through these statuses:
-| Status | Meaning |
-| ----------- | --------------------------------------------------------------------------------------------------------------------------------------- |
-| Draft | The problem and proposed decision are under discussion. Nothing is approved for implementation or support. |
-| Development | The design is chosen and implementation is underway. The contract may still change incompatibly. |
-| Evaluation | The implementation works end to end and is being evaluated through tests and concrete use. The contract is not yet stable. |
-| Stable | The contract is supported. Compatibility and replay guarantees apply, and material changes require a new design record. |
-| Legacy | Existing use and historical compatibility remain supported, but new use is prohibited. The record must identify its Stable replacement. |
+| Status | Meaning |
+| ----------- | -------------------------------------------------------------------------------------------------------------------------- |
+| Draft | The problem and proposed decision are under discussion. Nothing is approved for implementation or support. |
+| Development | The design is chosen and implementation is underway. The contract may still change incompatibly. |
+| Evaluation | The implementation works end to end and is being evaluated through tests and concrete use. The contract is not yet stable. |
+| Stable | The design is fully approved and tested. Create an ADR if it establishes a durable rule, boundary, or public contract. |
+| Legacy | The design no longer guides new work. Retain it as historical context and link to any successor design or ADR. |
A Draft that is abandoned before implementation remains in the repository with
a prominent note explaining that no decision was adopted.
diff --git a/package-lock.json b/package-lock.json
index 4ca406b..64489ff 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,15 +1,19 @@
{
- "name": "@hnordt/hyperkernel",
+ "name": "hyperkernel-workspace",
"version": "0.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
- "name": "@hnordt/hyperkernel",
+ "name": "hyperkernel-workspace",
"version": "0.0.1",
"license": "MIT",
+ "workspaces": [
+ "packages/*"
+ ],
"dependencies": {
"@fontsource-variable/inter": "^5.3.0",
+ "@hyperkernel/sqlite": "^0.0.1",
"zod": "^4.4.3"
},
"devDependencies": {
@@ -286,6 +290,14 @@
"url": "https://github.com/sponsors/nzakas"
}
},
+ "node_modules/@hyperkernel/kernel": {
+ "resolved": "packages/kernel",
+ "link": true
+ },
+ "node_modules/@hyperkernel/sqlite": {
+ "resolved": "packages/sqlite",
+ "link": true
+ },
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
@@ -4233,6 +4245,19 @@
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
+ },
+ "packages/kernel": {
+ "name": "@hyperkernel/kernel",
+ "version": "0.0.1",
+ "license": "MIT"
+ },
+ "packages/sqlite": {
+ "name": "@hyperkernel/sqlite",
+ "version": "0.0.1",
+ "license": "MIT",
+ "engines": {
+ "node": ">=24"
+ }
}
}
}
diff --git a/package.json b/package.json
index b367bec..d234b8d 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,10 @@
{
- "name": "@hnordt/hyperkernel",
+ "name": "hyperkernel-workspace",
"version": "0.0.1",
"private": true,
+ "workspaces": [
+ "packages/*"
+ ],
"description": "Open-source primitives for building reliable, auditable, self-hosted software platforms.",
"license": "MIT",
"repository": {
@@ -37,6 +40,7 @@
},
"dependencies": {
"@fontsource-variable/inter": "^5.3.0",
+ "@hyperkernel/sqlite": "^0.0.1",
"zod": "^4.4.3"
},
"devDependencies": {
diff --git a/packages/kernel/package.json b/packages/kernel/package.json
new file mode 100644
index 0000000..8734a76
--- /dev/null
+++ b/packages/kernel/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "@hyperkernel/kernel",
+ "version": "0.0.1",
+ "private": true,
+ "description": "Trusted kernel primitives for Hyperkernel applications.",
+ "license": "MIT",
+ "type": "module",
+ "exports": {
+ ".": "./src/index.ts"
+ }
+}
diff --git a/packages/kernel/src/index.ts b/packages/kernel/src/index.ts
new file mode 100644
index 0000000..8479b0c
--- /dev/null
+++ b/packages/kernel/src/index.ts
@@ -0,0 +1,7 @@
+/**
+ * Public kernel API.
+ *
+ * This package is intentionally empty while the initial workspace boundary is
+ * verified. Domain contracts will move here in later, separately reviewed steps.
+ */
+export {};
diff --git a/packages/sqlite/package.json b/packages/sqlite/package.json
new file mode 100644
index 0000000..18abbd0
--- /dev/null
+++ b/packages/sqlite/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "@hyperkernel/sqlite",
+ "version": "0.0.1",
+ "private": true,
+ "description": "SQLite connection primitives for Hyperkernel applications.",
+ "license": "MIT",
+ "type": "module",
+ "engines": {
+ "node": ">=24"
+ },
+ "exports": {
+ ".": "./src/index.ts"
+ }
+}
diff --git a/packages/sqlite/src/index.ts b/packages/sqlite/src/index.ts
new file mode 100644
index 0000000..b1a5523
--- /dev/null
+++ b/packages/sqlite/src/index.ts
@@ -0,0 +1,28 @@
+import { DatabaseSync } from "node:sqlite";
+
+/**
+ * Opens a SQLite database and applies Hyperkernel's baseline connection settings.
+ *
+ * The database is created by SQLite when `path` names a file that does not yet
+ * exist. Callers own the returned connection and must close it when finished.
+ */
+export function openDatabase(
+ path: string | URL = ":memory:",
+ options: { timeout?: number } = {},
+): DatabaseSync {
+ if (path === ":memory:") {
+ console.warn(
+ "Using an in-memory SQLite database. All data will be lost when the process stops.",
+ );
+ }
+
+ const database = new DatabaseSync(path, {
+ timeout: options.timeout ?? 5000,
+ });
+
+ database.exec("PRAGMA journal_mode = WAL");
+ database.exec("PRAGMA synchronous = NORMAL");
+ database.exec("PRAGMA foreign_keys = ON");
+
+ return database;
+}
diff --git a/src/lib/kernel-package.test.ts b/src/lib/kernel-package.test.ts
new file mode 100644
index 0000000..17b6df0
--- /dev/null
+++ b/src/lib/kernel-package.test.ts
@@ -0,0 +1,6 @@
+import { expect, test } from "vitest";
+import * as kernel from "@hyperkernel/kernel";
+
+test("resolves the kernel workspace through its package name", () => {
+ expect(Object.prototype.toString.call(kernel)).toBe("[object Module]");
+});
diff --git a/src/lib/server/db/index.ts b/src/lib/server/db/index.ts
index c627936..3bb3802 100644
--- a/src/lib/server/db/index.ts
+++ b/src/lib/server/db/index.ts
@@ -1,18 +1,4 @@
-import { DatabaseSync } from "node:sqlite";
+import { openDatabase } from "@hyperkernel/sqlite";
import { env } from "$env/dynamic/private";
-const dbUrl = env.DATABASE_URL ?? ":memory:";
-
-if (dbUrl === ":memory:") {
- console.warn(
- "Using an in-memory SQLite database. All data will be lost when the process stops.",
- );
-}
-
-export const db = new DatabaseSync(dbUrl, {
- timeout: 5000,
-});
-
-db.exec("PRAGMA journal_mode = WAL");
-db.exec("PRAGMA synchronous = NORMAL");
-db.exec("PRAGMA foreign_keys = ON");
+export const db = openDatabase(env.DATABASE_URL);
diff --git a/src/lib/server/db/open-database.test.ts b/src/lib/server/db/open-database.test.ts
new file mode 100644
index 0000000..eddc750
--- /dev/null
+++ b/src/lib/server/db/open-database.test.ts
@@ -0,0 +1,21 @@
+import { DatabaseSync } from "node:sqlite";
+import { expect, test, vi } from "vitest";
+import { openDatabase } from "@hyperkernel/sqlite";
+
+test("opens an in-memory SQLite database by default", () => {
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
+ const database = openDatabase(undefined);
+
+ try {
+ expect(database).toBeInstanceOf(DatabaseSync);
+ expect(warn).toHaveBeenCalledWith(
+ "Using an in-memory SQLite database. All data will be lost when the process stops.",
+ );
+ expect(database.prepare("PRAGMA foreign_keys").get()).toEqual({
+ foreign_keys: 1,
+ });
+ } finally {
+ database.close();
+ warn.mockRestore();
+ }
+});
From 4afa2213184bc603ca6d45d2ead1fd2cc36bedc3 Mon Sep 17 00:00:00 2001
From: Heliton Nordt <1625399+hnordt@users.noreply.github.com>
Date: Tue, 28 Jul 2026 12:46:28 -0300
Subject: [PATCH 02/20] Document SQLite connection defaults and ownership
strategy
---
packages/sqlite/src/index.ts | 16 ++++++++++++----
1 file changed, 12 insertions(+), 4 deletions(-)
diff --git a/packages/sqlite/src/index.ts b/packages/sqlite/src/index.ts
index b1a5523..5a97f09 100644
--- a/packages/sqlite/src/index.ts
+++ b/packages/sqlite/src/index.ts
@@ -5,10 +5,14 @@ import { DatabaseSync } from "node:sqlite";
*
* The database is created by SQLite when `path` names a file that does not yet
* exist. Callers own the returned connection and must close it when finished.
+ *
+ * @param path SQLite database filename. Defaults to `:memory:` when omitted.
+ * @param options SQLite connection options.
+ * @param options.timeout Maximum time, in milliseconds, to wait for a locked database. Defaults to `5000`.
*/
export function openDatabase(
path: string | URL = ":memory:",
- options: { timeout?: number } = {},
+ options = { timeout: 5000 },
): DatabaseSync {
if (path === ":memory:") {
console.warn(
@@ -16,9 +20,13 @@ export function openDatabase(
);
}
- const database = new DatabaseSync(path, {
- timeout: options.timeout ?? 5000,
- });
+ const database = new DatabaseSync(path, { timeout: options.timeout });
+
+ // TODO: Define single-server database ownership: prevent concurrent Hyperkernel
+ // instances from sharing a database, preferably through an exclusive lock. This
+ // protects the running database from external writes but is not a deployment security
+ // boundary. Reconcile it with native daily snapshots and incremental event export to
+ // S3; external tools such as Litestream may not access an exclusively locked database.
database.exec("PRAGMA journal_mode = WAL");
database.exec("PRAGMA synchronous = NORMAL");
From 0c0cab9eabe8728579444c08060d7a39bbbd0038 Mon Sep 17 00:00:00 2001
From: Heliton Nordt <1625399+hnordt@users.noreply.github.com>
Date: Tue, 28 Jul 2026 15:11:28 -0300
Subject: [PATCH 03/20] refactor: move UI components and SQLite tests into
packages
---
AGENTS.md | 3 +-
docs/design/0007-local-package-workspaces.md | 20 +++++++----
package-lock.json | 35 ++++++++-----------
package.json | 1 +
.../sqlite/src}/open-database.test.ts | 2 +-
packages/ui/package.json | 14 ++++++++
.../ui/src}/Canvas.svelte | 0
.../ui/src}/Heading.svelte | 0
.../ui/src}/Icon.svelte | 0
.../ui/src}/Icon.svelte.test.ts | 0
.../ui/src}/Paragraph.svelte | 0
.../ui/src}/Theme.svelte | 0
.../ui/src}/Window.svelte | 2 +-
packages/ui/src/index.ts | 6 ++++
src/routes/+layout.svelte | 3 +-
src/routes/+page.svelte | 4 +--
tsconfig.json | 22 +++++++++++-
vite.config.ts | 9 +++--
18 files changed, 82 insertions(+), 39 deletions(-)
rename {src/lib/server/db => packages/sqlite/src}/open-database.test.ts (92%)
create mode 100644 packages/ui/package.json
rename {src/lib/components => packages/ui/src}/Canvas.svelte (100%)
rename {src/lib/components => packages/ui/src}/Heading.svelte (100%)
rename {src/lib/components => packages/ui/src}/Icon.svelte (100%)
rename {src/lib/components => packages/ui/src}/Icon.svelte.test.ts (100%)
rename {src/lib/components => packages/ui/src}/Paragraph.svelte (100%)
rename {src/lib/components => packages/ui/src}/Theme.svelte (100%)
rename {src/lib/components => packages/ui/src}/Window.svelte (93%)
create mode 100644 packages/ui/src/index.ts
diff --git a/AGENTS.md b/AGENTS.md
index 6628c7d..4bcba8d 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -191,7 +191,8 @@ Hyperkernel currently requires Node.js 24 or later and npm 11 or later.
- `src/routes/` contains SvelteKit routes and transport/UI entry points.
- `src/lib/server/db/index.ts` owns the shared `DatabaseSync` connection.
- `src/lib/server/` is server-only and must never be imported by client code.
-- `src/lib/components/` contains reusable interface components.
+- `packages/ui/src/` contains reusable interface components exported through
+ `@hyperkernel/ui`.
- `docs/adr/` contains architecture decision records for repository-wide architecture and governance definitions.
- `docs/design/` contains numbered design records for significant contracts and decisions.
- `docs/spikes/` contains isolated architecture experiments. Spikes are not production modules, supported APIs, or proof that a contract is implemented.
diff --git a/docs/design/0007-local-package-workspaces.md b/docs/design/0007-local-package-workspaces.md
index 4a8b11d..ce2e72a 100644
--- a/docs/design/0007-local-package-workspaces.md
+++ b/docs/design/0007-local-package-workspaces.md
@@ -30,7 +30,7 @@ tooling or making the kernel depend on SvelteKit.
The repository root is a private npm workspace coordinator named
`hyperkernel-workspace`. It declares `packages/*` as workspaces.
-The initial workspaces are:
+The initial workspaces were:
- `packages/kernel`, named `@hyperkernel/kernel`, whose empty public entry point
establishes the future import boundary without moving any existing kernel
@@ -41,9 +41,14 @@ The initial workspaces are:
Both packages remain private until an explicit publication decision and
packaging verification are complete.
+A subsequent Experience-layer migration adds `packages/ui`, named
+`@hyperkernel/ui`. It owns the reusable Svelte components previously kept in
+the root application's `src/lib/components` directory and exposes them through
+one package entry point.
+
The existing root SvelteKit application stays in place for now. Moving it under
-`apps/` and introducing a UI package are later migrations, not prerequisites
-for validating the first workspace boundary.
+`apps/` remains a later migration and is not a prerequisite for validating the
+workspace boundaries.
## Alternatives considered
@@ -68,15 +73,17 @@ keeping the prototype changes atomic and easy to inspect.
`@hyperkernel/kernel` is locally addressable but intentionally exports no
production API in this step. `@hyperkernel/sqlite` is Node-only and exposes the
low-level connection API described in
-[`0008-sqlite-connection-package.md`](0008-sqlite-connection-package.md). A
-later step must select and verify distribution builds before either package can
+[`0008-sqlite-connection-package.md`](0008-sqlite-connection-package.md).
+`@hyperkernel/ui` is consumed directly from source by the root application.
+A later step must select and verify distribution builds before any package can
be published.
## Evidence required for Evaluation
- npm installs the workspace successfully from the repository root.
- The root application continues to pass its existing checks.
-- The root application resolves both packages through their package names.
+- The root application resolves its workspace dependencies through their
+ package names.
- The SQLite connection behavior remains covered after extraction.
## Status history
@@ -84,3 +91,4 @@ be published.
| Date | Status | Note |
| ---------- | ----------- | ------------------------------------------------- |
| 2026-07-27 | Development | Chosen for the initial local workspace migration. |
+| 2026-07-28 | Development | Added the local UI component package boundary. |
diff --git a/package-lock.json b/package-lock.json
index eb2c00b..19f5906 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,6 +14,7 @@
"dependencies": {
"@fontsource-variable/inter": "^5.3.0",
"@hyperkernel/sqlite": "^0.0.1",
+ "@hyperkernel/ui": "^0.0.1",
"zod": "^4.4.3"
},
"devDependencies": {
@@ -298,11 +299,14 @@
"resolved": "packages/sqlite",
"link": true
},
+ "node_modules/@hyperkernel/ui": {
+ "resolved": "packages/ui",
+ "link": true
+ },
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0",
@@ -313,7 +317,6 @@
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
@@ -324,7 +327,6 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
@@ -334,14 +336,12 @@
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
- "dev": true,
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.31",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
@@ -1200,7 +1200,6 @@
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.11.tgz",
"integrity": "sha512-LFuZUkjJ9iF7JZye/aG5XM0SFcQ5VyL0oVX4WJ9dc0Va3R3s0OauX1BESVCb+YN/ol8TAfqGDDAQsTG627Y5kw==",
- "dev": true,
"license": "MIT",
"peerDependencies": {
"acorn": "^8.9.0"
@@ -1355,7 +1354,6 @@
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
"integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
- "dev": true,
"license": "MIT"
},
"node_modules/@types/json-schema": {
@@ -1386,7 +1384,6 @@
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
- "dev": true,
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
@@ -1539,7 +1536,7 @@
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz",
"integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1793,7 +1790,6 @@
"version": "8.17.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
"integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==",
- "dev": true,
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
@@ -1833,7 +1829,6 @@
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz",
"integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==",
- "dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">= 0.4"
@@ -1853,7 +1848,6 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
"integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
- "dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">= 0.4"
@@ -1912,7 +1906,6 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -2019,7 +2012,6 @@
"version": "5.8.1",
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.1.tgz",
"integrity": "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==",
- "dev": true,
"license": "MIT"
},
"node_modules/es-errors": {
@@ -2210,7 +2202,6 @@
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
"integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==",
- "dev": true,
"license": "MIT"
},
"node_modules/espree": {
@@ -2248,7 +2239,6 @@
"version": "2.2.13",
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.13.tgz",
"integrity": "sha512-m8jH5hZgJE2RRUK/jjkGPcJEDAV+dYnZYFkosQaPTcE+Yw4xynXHOo6FUdwaWBtdR3b1MMa7wEDTSHeR2VWsGA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.15"
@@ -2938,7 +2928,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
"integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==",
- "dev": true,
"license": "MIT"
},
"node_modules/locate-path": {
@@ -2961,7 +2950,6 @@
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.5"
@@ -3627,7 +3615,6 @@
"version": "5.56.8",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.8.tgz",
"integrity": "sha512-PY8LOw7xP6c8IOiVqdo0sbbZVYhXRSfklOQLAUyGBKqjTX0wx/z4l/9J+PmBpmlLnxzEb1NqltxQ5/wZme/Cmg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/remapping": "^2.3.4",
@@ -3759,7 +3746,6 @@
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
"integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "^1.0.6"
@@ -4234,7 +4220,6 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz",
"integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==",
- "dev": true,
"license": "MIT"
},
"node_modules/zod": {
@@ -4258,6 +4243,14 @@
"engines": {
"node": ">=24"
}
+ },
+ "packages/ui": {
+ "name": "@hyperkernel/ui",
+ "version": "0.0.1",
+ "license": "MIT",
+ "peerDependencies": {
+ "svelte": "^5.56.8"
+ }
}
}
}
diff --git a/package.json b/package.json
index e62db3b..7ae4fb2 100644
--- a/package.json
+++ b/package.json
@@ -42,6 +42,7 @@
"dependencies": {
"@fontsource-variable/inter": "^5.3.0",
"@hyperkernel/sqlite": "^0.0.1",
+ "@hyperkernel/ui": "^0.0.1",
"zod": "^4.4.3"
},
"devDependencies": {
diff --git a/src/lib/server/db/open-database.test.ts b/packages/sqlite/src/open-database.test.ts
similarity index 92%
rename from src/lib/server/db/open-database.test.ts
rename to packages/sqlite/src/open-database.test.ts
index eddc750..950048c 100644
--- a/src/lib/server/db/open-database.test.ts
+++ b/packages/sqlite/src/open-database.test.ts
@@ -1,6 +1,6 @@
import { DatabaseSync } from "node:sqlite";
import { expect, test, vi } from "vitest";
-import { openDatabase } from "@hyperkernel/sqlite";
+import { openDatabase } from "./index.js";
test("opens an in-memory SQLite database by default", () => {
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
diff --git a/packages/ui/package.json b/packages/ui/package.json
new file mode 100644
index 0000000..d8abdfc
--- /dev/null
+++ b/packages/ui/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "@hyperkernel/ui",
+ "version": "0.0.1",
+ "private": true,
+ "description": "Reusable interface components for Hyperkernel applications.",
+ "license": "MIT",
+ "type": "module",
+ "exports": {
+ ".": "./src/index.ts"
+ },
+ "peerDependencies": {
+ "svelte": "^5.56.8"
+ }
+}
diff --git a/src/lib/components/Canvas.svelte b/packages/ui/src/Canvas.svelte
similarity index 100%
rename from src/lib/components/Canvas.svelte
rename to packages/ui/src/Canvas.svelte
diff --git a/src/lib/components/Heading.svelte b/packages/ui/src/Heading.svelte
similarity index 100%
rename from src/lib/components/Heading.svelte
rename to packages/ui/src/Heading.svelte
diff --git a/src/lib/components/Icon.svelte b/packages/ui/src/Icon.svelte
similarity index 100%
rename from src/lib/components/Icon.svelte
rename to packages/ui/src/Icon.svelte
diff --git a/src/lib/components/Icon.svelte.test.ts b/packages/ui/src/Icon.svelte.test.ts
similarity index 100%
rename from src/lib/components/Icon.svelte.test.ts
rename to packages/ui/src/Icon.svelte.test.ts
diff --git a/src/lib/components/Paragraph.svelte b/packages/ui/src/Paragraph.svelte
similarity index 100%
rename from src/lib/components/Paragraph.svelte
rename to packages/ui/src/Paragraph.svelte
diff --git a/src/lib/components/Theme.svelte b/packages/ui/src/Theme.svelte
similarity index 100%
rename from src/lib/components/Theme.svelte
rename to packages/ui/src/Theme.svelte
diff --git a/src/lib/components/Window.svelte b/packages/ui/src/Window.svelte
similarity index 93%
rename from src/lib/components/Window.svelte
rename to packages/ui/src/Window.svelte
index 14f8d1f..b1e8d7e 100644
--- a/src/lib/components/Window.svelte
+++ b/packages/ui/src/Window.svelte
@@ -1,7 +1,7 @@
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index b5b2508..3ac3314 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -1,7 +1,5 @@