docs: restructure architecture docs to flowgraph pattern

- Create decisions/ directory with 32 numbered ADRs (ADR-001 through ADR-032)
  extracted from inline DD/SD/ED/SE decision sections
- Create open-questions.md with 16 OQs organized by theme, cross-referenced
  to ADRs, with status tracking (resolved/open)
- Create README.md as architecture index with doc table, ADR table, and
  lifecycle status definitions (draft/reviewed/stable/deprecated)
- Replace inline decision sections in all spec docs with ADR reference tables
- Replace inline open questions with OQ references to centralized tracker
- Update frontmatter: metagraph-module.md, overview.md, sqlite-host.md → reviewed;
  schema-evolution.md and encrypted-data.md remain draft
- DD1-DD10 → ADR-009 through ADR-018
- D1-D8 → ADR-001 through ADR-008
- SD1-SD5 → ADR-019 through ADR-023 (SD5 folded into ADR-006/008)
- ED1-ED5 → ADR-023 through ADR-027
- SE1-SE5 → ADR-028 through ADR-032
This commit is contained in:
2026-05-29 07:19:03 +00:00
parent 6c3ed598db
commit 67ccfbf928
39 changed files with 1117 additions and 435 deletions

View File

@@ -1,6 +1,6 @@
---
status: draft
last_updated: 2026-05-28
status: reviewed
last_updated: 2026-05-29
---
# @alkdev/storage — Overview
@@ -69,64 +69,18 @@ the main `mod.ts` re-exports it. Importing from either `@alkdev/storage` or
## Design Decisions
### D1: Deno-first, JSR publishes, npm comes free
All design decisions are documented as ADRs in [decisions/](decisions/).
The package is published to JSR (`deno publish`). npm compatibility is automatic
via JSR's npm layer (`@jsr/alkdev__storage`). No separate dnt build step.
### D2: Metagraph over domain-specific tables
Instead of a table per domain concept (call graphs, ACL rules, task trees), we
define graph types with typed node and edge schemas. A "call graph" is a graph
type with specific node types (operation call, subcall) and edge types
(triggered, depends_on). An "ACL graph" is a graph type with node types
(account, resource) and edge types (can_read, can_write).
This trades some query convenience for generality. Domain-specific queries are
built on top of the graph query layer, not baked into table schemas.
### D3: Type.Module as the primary API surface
The `Type.Module()` construction API is the intended way to define graph type
definitions. The `Metagraph` Module provides base entries (`BaseNode`,
`BaseEdge`, `Config`); concrete graph types compose them via `Metagraph.Import()`
and `Type.Composite()`. The `SchemaBuilder` is removed.
This replaces the earlier fluent builder pattern. The Module format provides
native `Type.Ref()` for internal references, `Module.Import()` for cross-package
references, and JSON Schema `$defs` that map directly to DB storage.
### D4: Injectable clients, no module-level side effects
`createSqliteDatabase(client)` receives a pre-created client. Module-level side
effects (auto-connections, env-based configuration) are forbidden. This enables
testing with in-memory databases and containerized deployment patterns.
### D5: Drizzle + TypeBox (via drizzlebox) as the table definition pattern
Drizzle table definitions are the single source of truth for database schema.
`@alkdev/drizzlebox` generates TypeBox `Select*` and `Insert*` schemas from
Drizzle tables, enabling runtime validation without manual schema duplication.
### D6: Enumeration pattern — `as const` objects, not TypeScript enums
All enumerations use the `as const` object pattern (e.g.,
`GRAPH_STATUS = { Active: "active", ... } as const`) rather than TypeScript
`enum`. This avoids JSR slow-type issues and provides a consistent pattern
across the codebase. The TypeBox schemas use `Type.Union` of `Type.Literal`
values derived from the const object.
### D7: No comments in code
Per project convention across @alkdev packages, source files contain no inline
comments. Documentation lives in architecture docs and TypeBox schema
descriptions.
### D8: Common columns pattern
All tables share `id` (text PK), `metadata` (JSON text defaulting to `{}`),
`createdAt`, and `updatedAt` (integer timestamps in SQLite, will be timestamptz
in PG). This ensures every row has auditability and extensibility.
| ADR | Decision | Summary |
|-----|----------|---------|
| [001](decisions/001-deno-first-jsr-publishes.md) | Deno-first, JSR publishes | Published to JSR; npm comes free via `@jsr/alkdev__storage` |
| [002](decisions/002-metagraph-over-domain-tables.md) | Metagraph over domain-specific tables | 6 general-purpose tables serve all domains |
| [003](decisions/003-typebox-module-as-api-surface.md) | TypeBox Module as API surface | `Type.Module()` replaces `SchemaBuilder`; `Metagraph.Import()` + `Type.Composite()` |
| [004](decisions/004-injectable-clients-no-side-effects.md) | Injectable clients, no side effects | `createSqliteDatabase(client)` takes a pre-created client |
| [005](decisions/005-drizzle-plus-typebox-via-drizzlebox.md) | Drizzle + TypeBox via drizzlebox | Drizzle tables are single source of truth; drizzlebox generates TypeBox schemas |
| [006](decisions/006-enum-pattern-as-const-objects.md) | `as const` objects, not TypeScript enums | Avoids JSR slow-types; consistent pattern across codebase |
| [007](decisions/007-no-comments-in-code.md) | No comments in code | Documentation lives in architecture docs and TypeBox descriptions |
| [008](decisions/008-common-columns-pattern.md) | Common columns pattern | `id`, `metadata`, `createdAt`, `updatedAt` on every table |
## Dependencies
@@ -256,48 +210,14 @@ storage node attributes and operations call events), they should either:
## Open Questions
1. **Should `actors` be a node type or a standalone table?** Currently `actors`
is a standalone table in the SQLite host that isn't referenced by any
relation. If identity/authentication is a graph (ACL nodes based on
`@alkdev/operations`'s `Identity` interface), actors become node types. If
identity is a domain concept that needs special query patterns (auth lookups,
session joins), standalone tables may be better. Decision: defer until ACL
design, informed by `@alkdev/operations`'s `AccessControl` model.
Open questions are tracked in [open-questions.md](open-questions.md). Key
questions affecting this package:
2. **Should the repository layer be host-specific or host-agnostic?** A
host-agnostic repository (insert graph, find nodes by type) requires an
abstraction over Drizzle's query builder. A host-specific repository is
simpler but means duplicating query logic for PG. Decision: start
host-specific in SQLite, extract common patterns later.
3. **Encrypted data scope**: Should encryption be per-attribute, per-node, or
per-graph? Per-attribute (like hub's `client_secrets.value`) allows selective
encryption. Per-node encrypts the entire `attributes` blob. Per-graph is
overkill. Decision: per-attribute, modeled as an encrypted node type with a
dedicated attribute for the ciphertext.
4. **Key management scope**: `@alkdev/storage` should provide the
encryption/decryption primitives but NOT key management. The consuming
application provides the key ring. This keeps the storage package agnostic to
deployment-specific secret management.
5. **Schema evolution strategy**: When graph type schemas evolve (new node types,
changed attribute schemas), how are changes detected and data migrated?
TypeBox's `Value.Diff` can diff schemas-as-JSON to detect changes,
`Value.Cast` can migrate data shapes, and `Value.Check` can verify
compatibility. The `version` field on `graph_types` tracks breaking changes.
See [schema-evolution.md](./schema-evolution.md) for the full design.
6. **~~Should the repository layer live in `@alkdev/storage` or in a consumer
package?~~** Decision: the repository CRUD layer (host-specific typed
queries, schema validation before writes) belongs in `@alkdev/storage`. The
operations bridging layer (generating `OperationSpec`s from metagraph schemas)
belongs in a consumer or adapter package. These are separate concerns — CRUD
is a storage concern; call protocol integration is an application concern.
The repository layer in `@alkdev/storage` has **no dependency on
`@alkdev/operations`**. It performs typed inserts, finds, updates, and
deletes with schema validation. The consumer then wires these CRUD functions
into the operations registry if desired.
- **OQ-03**: Should actors be a node type or a standalone table? (open, deferred to ACL design)
- **OQ-04**: Should the repository layer be host-specific or host-agnostic? (open, start host-specific)
- **OQ-14**: Should encryption be per-attribute, per-node, or per-graph? (resolved: per-attribute)
- **OQ-15**: Should key management be in this package? (resolved: no, application provides key ring)
- **OQ-16**: Should the repository layer live in storage or a consumer package? (resolved: CRUD in storage, operations bridging in consumer)
## References