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:
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user