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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ This is about _our_ history. Genuinely foreign input — a third-party server's
- **`Stack` is the invariant layer; adapters are storage engines.** Validation, `_config` protection, ID rules, migration policy, and permission checks live in `packages/core` so every adapter inherits them. Don't reimplement an invariant inside an adapter. (The one documented exception: the `_config` query exclusion must live in each adapter's own query predicate.)
- **Package prefixes declare the contract**: `adapter-*` = full `StackAdapter`, `record-adapter-*` = `StackRecordAdapter`, `blob-adapter-*` = `StackBlobAdapter`.
- **Optional capabilities are optional methods**, checked for truthiness at the call site with a documented fallback — never a boolean in `capabilities`.
- **Shared SQLite logic goes in `@haverstack/sqlite-shared`**, not duplicated across the two adapters.
- **Shared SQLite logic goes in `@haverstack/sqlite-shared`**, not duplicated across adapters. It is `private` and bundled into its consumers at build time, so it is never a `dependencies` entry and adding an export to it does not widen any package's public API.
- **Wire behavior is pinned by `@haverstack/conformance-fixtures`** — pure data, consumed by both `adapter-api` and server implementations. Changing the wire contract means updating fixtures, spec, and implementation together.

## Tests
Expand Down
6 changes: 4 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,9 @@ Note that this is about _our_ history, not about inputs we genuinely receive. Ha

**Optional adapter capabilities follow one pattern**: an optional interface method, checked for truthiness at the call site, with a described fallback when absent — never a boolean flag in `capabilities`. `combineAdapters()` forwards an optional method only when the underlying part actually implements it.

**Logic shared between the two SQLite adapters goes in `@haverstack/sqlite-shared`**, an internal (unpublished-as-public-API) package. Keeping schema DDL, the cursor codec, and the CRUD logic in one place is what keeps the two engines from silently drifting and keeps a cursor minted by one decodable by the other. Only genuinely engine-specific code (WASM init vs. `DatabaseSync`, pragma setup, lifecycle) belongs in the adapter packages.
**Logic shared between SQLite-backed adapters goes in `@haverstack/sqlite-shared`**, an internal package. Keeping schema DDL, the cursor codec, and the CRUD logic in one place is what keeps engines from silently drifting and keeps a cursor minted by one decodable by another. Only genuinely engine-specific code (database construction, pragma setup, lifecycle) belongs in the adapter packages.

The package is `private` and **bundled into its consumers at build time** — `record-adapter-sqlite` builds with `tsup` and inlines it, so nothing installing that adapter from the registry resolves `@haverstack/sqlite-shared`. Two consequences when working here: adding an export to `sqlite-shared` does not expand any package's public API, and a new SQLite adapter reuses it by living in this repository rather than by installing it. Adding a `dependencies` entry on it would undo that — it belongs in `devDependencies`.

**Wire-format behavior is pinned by shared fixtures.** `@haverstack/conformance-fixtures` is pure data — no test framework, no adapter, no server. Two independent consumers exercise the same fixtures: `adapter-api`'s tests, and any server implementation. A change to the wire contract updates the fixtures, the spec, and the implementation together.

Expand Down Expand Up @@ -152,7 +154,7 @@ docs/
commons/ # Schema Commons — shared, app-neutral record types
packages/
core/ # Stack, ScopedStack, types, schema, validation, MemoryAdapter
sqlite-shared/ # Internal: shared SQL logic for SQLite-backed adapters
sqlite-shared/ # Internal: shared SQL logic, bundled into consumers, not published
record-adapter-sqlite/ # Node native SQLite (node:sqlite), FTS5, WAL
blob-adapter-disk/ # Content-addressed blobs on disk
adapter-local/ # Convenience: SQLite records + disk blobs
Expand Down
2 changes: 2 additions & 0 deletions docs/spec/adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ All adapters support the full Record API. Performance guarantees differ; correct

**`@haverstack/sqlite-shared`** is an internal, non-public package holding everything a SQLite-backed record adapter needs that isn't specific to one binding — schema DDL, `WHERE`/`ORDER` building, the cursor codec, row mappers, the FTS5 sanitizer and indexing strategy, the storage-ownership lock, and (via a small `SqlExecutor` interface normalizing a binding's call convention) the actual CRUD/query/version/type/association/token logic itself. An adapter implements only what's genuinely engine-specific: database construction, pragma/WAL setup, and lifecycle. `record-adapter-sqlite` is its only consumer today; the split exists so a second SQLite engine inherits the behavior rather than reimplementing it, and so a cursor minted by one is decodable by another.

**It is bundled into its consumers rather than published.** "Non-public" is enforced, not merely intended: the package is `private`, and `record-adapter-sqlite` inlines it at build time, so a consumer installing that adapter from the registry never resolves `@haverstack/sqlite-shared` and cannot depend on it. `SqlExecutor` and the `Shared*Logic` classes are therefore internal collaborators of the adapters in this repository, not an extension point — a second SQLite engine inherits them by living here, not by installing them. Reversing that (publishing it so third-party adapters can build on `SqlExecutor`) is a deliberate decision to make it public API with the stability obligations that implies, not a packaging tweak.

`SqlExecutor` is synchronous. Every SQLite binding in scope executes queries in-process without yielding, and the shared logic's explicit `BEGIN`/`COMMIT` sequences depend on that — an engine reached over a network (D1, libsql over HTTP) does not fit this interface without making it async throughout.

SQLite-backed adapters enable foreign-key enforcement (`PRAGMA foreign_keys = ON`) so that operations like `associate()` against a nonexistent record fail loudly (`StackNotFoundError`) instead of silently creating an orphan row.
Expand Down
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@
"publish:blob-adapter-disk": "pnpm --filter @haverstack/blob-adapter-disk publish --access public",
"publish:api": "pnpm --filter @haverstack/adapter-api publish --access public",
"publish:wire-types": "pnpm --filter @haverstack/wire-types publish --access public",
"publish:commons": "pnpm --filter @haverstack/commons publish --access public"
"publish:commons": "pnpm --filter @haverstack/commons publish --access public",
"publish:record-adapter-sqlite": "pnpm --filter @haverstack/record-adapter-sqlite publish --access public",
"publish:conformance-fixtures": "pnpm --filter @haverstack/conformance-fixtures publish --access public",
"publish:all": "pnpm run publish:core && pnpm run publish:wire-types && pnpm run publish:blob-adapter-disk && pnpm run publish:record-adapter-sqlite && pnpm run publish:commons && pnpm run publish:conformance-fixtures && pnpm run publish:adapter-local && pnpm run publish:api",
"verify:pack": "node scripts/verify-pack.mjs"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
Expand Down
6 changes: 3 additions & 3 deletions packages/adapter-api/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@haverstack/adapter-api",
"version": "0.6.0",
"version": "0.7.0",
"description": "Remote server adapter for Haverstack",
"type": "module",
"exports": {
Expand All @@ -22,8 +22,8 @@
"lint": "eslint src tests"
},
"dependencies": {
"@haverstack/core": "workspace:*",
"@haverstack/wire-types": "workspace:*"
"@haverstack/core": "workspace:^",
"@haverstack/wire-types": "workspace:^"
},
"devDependencies": {
"@haverstack/conformance-fixtures": "workspace:*",
Expand Down
8 changes: 4 additions & 4 deletions packages/adapter-local/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@haverstack/adapter-local",
"version": "0.6.0",
"version": "0.7.0",
"description": "Local (SQLite + disk) stack adapter for Haverstack",
"type": "module",
"engines": {
Expand Down Expand Up @@ -39,9 +39,9 @@
"lint": "eslint src tests"
},
"dependencies": {
"@haverstack/core": "workspace:*",
"@haverstack/record-adapter-sqlite": "workspace:*",
"@haverstack/blob-adapter-disk": "workspace:*"
"@haverstack/core": "workspace:^",
"@haverstack/record-adapter-sqlite": "workspace:^",
"@haverstack/blob-adapter-disk": "workspace:^"
},
"devDependencies": {
"@types/node": "^22.0.0",
Expand Down
4 changes: 2 additions & 2 deletions packages/blob-adapter-disk/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@haverstack/blob-adapter-disk",
"version": "0.6.0",
"version": "0.7.0",
"description": "Disk blob adapter for Haverstack",
"type": "module",
"exports": {
Expand Down Expand Up @@ -36,7 +36,7 @@
"lint": "eslint src tests"
},
"dependencies": {
"@haverstack/core": "workspace:*"
"@haverstack/core": "workspace:^"
},
"devDependencies": {
"@types/node": "^22.0.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/commons/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"lint": "eslint src tests"
},
"dependencies": {
"@haverstack/core": "workspace:*"
"@haverstack/core": "workspace:^"
},
"devDependencies": {
"@types/node": "^22.0.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/conformance-fixtures/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"lint": "eslint src"
},
"dependencies": {
"@haverstack/wire-types": "workspace:*"
"@haverstack/wire-types": "workspace:^"
},
"devDependencies": {
"@types/node": "^22.0.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@haverstack/core",
"version": "0.8.0",
"version": "0.9.0",
"description": "Core library for Haverstack — portable personal data stack",
"type": "module",
"exports": {
Expand Down
7 changes: 4 additions & 3 deletions packages/record-adapter-sqlite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,18 @@
],
"scripts": {
"prepublishOnly": "pnpm run build",
"build": "tsc -p tsconfig.build.json",
"build": "tsup",
"test": "vitest run",
"typecheck": "tsc --noEmit",
"lint": "eslint src tests"
},
"dependencies": {
"@haverstack/core": "workspace:*",
"@haverstack/sqlite-shared": "workspace:*"
"@haverstack/core": "workspace:^"
},
"devDependencies": {
"@haverstack/sqlite-shared": "workspace:*",
"@types/node": "^22.0.0",
"tsup": "^8.5.1",
"typescript": "^5.5.0",
"vitest": "^2.0.0"
}
Expand Down
24 changes: 24 additions & 0 deletions packages/record-adapter-sqlite/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { defineConfig } from 'tsup';

/**
* @haverstack/sqlite-shared is bundled into this package's output rather
* than shipped as a dependency. It is an internal package — it carries no
* API stability promise and is not published — so a consumer installing
* this adapter from the registry must not need to resolve it.
*
* @haverstack/core stays external: it is a real published peer, and
* inlining it would give this package its own private copy of the error
* classes, breaking `instanceof` against the caller's copy.
*/
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm'],
target: 'node22',
dts: true,
sourcemap: true,
clean: true,
splitting: false,
treeshake: true,
external: ['@haverstack/core', '@haverstack/core/adapter'],
noExternal: ['@haverstack/sqlite-shared'],
});
8 changes: 4 additions & 4 deletions packages/sqlite-shared/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@haverstack/sqlite-shared",
"version": "0.1.0",
"description": "Engine-independent SQL building blocks shared between Haverstack's SQLite-backed record adapters. Internal package, not a StackAdapter implementation.",
"description": "Internal shared logic for SQLite-backed Haverstack record adapters. Not published — bundled into its consumers. No API stability promise.",
"type": "module",
"exports": {
".": {
Expand All @@ -26,18 +26,18 @@
"internal"
],
"scripts": {
"prepublishOnly": "pnpm run build",
"build": "tsc -p tsconfig.build.json",
"test": "vitest run",
"typecheck": "tsc --noEmit",
"lint": "eslint src tests"
},
"dependencies": {
"@haverstack/core": "workspace:*"
"@haverstack/core": "workspace:^"
},
"devDependencies": {
"@types/node": "^22.0.0",
"typescript": "^5.5.0",
"vitest": "^2.0.0"
}
},
"private": true
}
4 changes: 2 additions & 2 deletions packages/wire-types/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@haverstack/wire-types",
"version": "0.6.0",
"version": "0.7.0",
"description": "HTTP wire types and serialization for Haverstack",
"type": "module",
"exports": {
Expand All @@ -22,7 +22,7 @@
"lint": "eslint src tests"
},
"dependencies": {
"@haverstack/core": "workspace:*"
"@haverstack/core": "workspace:^"
},
"devDependencies": {
"@types/node": "^22.0.0",
Expand Down
Loading
Loading