mikser-io 8.3.6 → 8.3.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/10.0-PLAN.md +182 -0
- package/9.0-PLAN.md +438 -0
- package/CLAUDE.md +10 -0
- package/CLOUD-PLAN.md +147 -0
- package/package.json +1 -1
- package/src/database/index.js +170 -18
package/10.0-PLAN.md
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# Mikser 10.0 — Planning (stub)
|
|
2
|
+
|
|
3
|
+
**The shift in one line:** scale moves from "make one mikser hold more" to "compose more miksers." A new coordination layer ships as `party-mikser-io` — separate package, separate ADR series, separate release cadence. `mikser-io` itself stays single-instance-pure.
|
|
4
|
+
|
|
5
|
+
This is a v10 placeholder — direction-setting, not a finished design. The hard pieces (cross-mikser refs, discovery, partition behavior) get worked out as `party-mikser-io` takes shape. This file gets dropped at release; the surviving decisions become ADRs.
|
|
6
|
+
|
|
7
|
+
## The gate: v9 has to pay out first
|
|
8
|
+
|
|
9
|
+
**No v10 work begins until v9 is incorporated into our own daily work and the workflow pays out.** Not "v9 has shipped." Not "v9 has been tested." **Used, daily, for our own real work, until the format-liberation pitch is something we'd defend with our own time.**
|
|
10
|
+
|
|
11
|
+
This is the strongest gate in the roadmap. It exists because the v10 architecture is more interesting to build than the v9 documentation is to write, and that asymmetry kills projects with strong substrates. The discipline is to refuse v10 design work — even thinking about it as an escape — until v9 has earned the next layer through real use.
|
|
12
|
+
|
|
13
|
+
What "pays out" means operationally — concrete signals to watch:
|
|
14
|
+
|
|
15
|
+
| signal | reading |
|
|
16
|
+
|---|---|
|
|
17
|
+
| New work naturally flows into mikser, not into "I should mikser-ize this later" | ✅ working |
|
|
18
|
+
| You stop wanting to switch back to your previous setup | ✅ working |
|
|
19
|
+
| The format-liberation flows (PDF in → CSV out, CSV in → HTML out) are things you actually run, not just demos | ✅ working |
|
|
20
|
+
| The MCP agent integration makes daily work faster, not just possible | ✅ working |
|
|
21
|
+
| Watch-mode + incremental rebuild holds up under your real editing patterns | ✅ working |
|
|
22
|
+
| You stop writing helper scripts that work around mikser instead of through it | ✅ working |
|
|
23
|
+
| Friends/colleagues see what you have running and ask how to use it themselves | ✅ working — the strongest signal |
|
|
24
|
+
|
|
25
|
+
Failure signals — any of these means v9 hasn't paid out yet, fix before v10:
|
|
26
|
+
|
|
27
|
+
| signal | reading |
|
|
28
|
+
|---|---|
|
|
29
|
+
| You manually run `mikser --clear` more than weekly | ❌ incremental rebuild has rough edges |
|
|
30
|
+
| You keep editing files outside the mikser folder because watch-mode is slow / breaks | ❌ watch-mode regression somewhere |
|
|
31
|
+
| Specific plugins break in ways that make you avoid them | ❌ plugin needs work, not v10 work |
|
|
32
|
+
| You stop using MCP integration because grep is faster | ❌ MCP surface isn't carrying its weight |
|
|
33
|
+
| You compose pipelines outside mikser (one-off scripts) because composing in mikser is awkward | ❌ recipe documentation or substrate ergonomics gap |
|
|
34
|
+
| You don't find yourself reaching for it for new tasks | ❌ the substrate isn't actually changing how you work |
|
|
35
|
+
|
|
36
|
+
When the working signals dominate over a sustained period of real use, v10 design starts. Not before.
|
|
37
|
+
|
|
38
|
+
## Why v10 (not 9.x)
|
|
39
|
+
|
|
40
|
+
The 9.0 plan deliberately deferred mikser-to-mikser composition to v10. The reasoning is in `9.0-PLAN.md` under "Out of scope":
|
|
41
|
+
|
|
42
|
+
> Cross-instance concerns (discovery, peer identity, routing, cross-mikser refs, watch propagation, liveness, partition behavior, peer auth) belong to a distributed-systems substrate that iterates at "we hit a production partition bug" speed, not at engine-substrate speed. Folding any of it into `mikser-io` would re-fail ADR-0006 test #5 (release cadence) the moment it ships.
|
|
43
|
+
|
|
44
|
+
v10 is the version where that substrate ships, alongside the small `mikser-io` additions that make it possible.
|
|
45
|
+
|
|
46
|
+
Until then: one folder, one mikser, domain-sized catalog. That's the v9 design center, and it earns the v10 work by staying small.
|
|
47
|
+
|
|
48
|
+
## The split
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
mikser-io → single-instance substrate (unchanged from 9.0)
|
|
52
|
+
party-mikser-io → coordination layer between mikser instances
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The repo-split check is one line: **does this code reference anything outside this mikser's `runtime`? If yes → wrong repo.** That makes ADR-0006 test #1 (substrate?) concrete for distributed code, which is otherwise ambiguous.
|
|
56
|
+
|
|
57
|
+
| concern | lives in | why |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| Catalog / refs / manifest / journal | `mikser-io` | every mikser needs this |
|
|
60
|
+
| Source plugins, render dispatch, lifecycle | `mikser-io` | same |
|
|
61
|
+
| Discovery (how A finds B) | `party-mikser-io` | mikser-io never needs to know peers exist |
|
|
62
|
+
| Identity (`mikser.name` as a stable handle) | `party-mikser-io` | mikser-io has no notion of "self" beyond `runtime` |
|
|
63
|
+
| Routing (which mikser owns entity X) | `party-mikser-io` | local catalog queries don't ask this |
|
|
64
|
+
| Cross-mikser refs (`$author` in A → entity in B) | `party-mikser-io` | refs.js stays local-graph-pure |
|
|
65
|
+
| Watch propagation across instances | `party-mikser-io` | chokidar covers one folder; cross-mikser is a different transport |
|
|
66
|
+
| Liveness / health / partition behavior | `party-mikser-io` | distributed-systems posture, not file-engine posture |
|
|
67
|
+
| Auth / trust between peers | `party-mikser-io` | engine auth is per-endpoint; peer auth is its own model |
|
|
68
|
+
|
|
69
|
+
## The metaphor
|
|
70
|
+
|
|
71
|
+
A "party" — voluntary attendance, each guest brings what they own, the host knows who's there but nobody's centrally in charge.
|
|
72
|
+
|
|
73
|
+
More accurate than:
|
|
74
|
+
|
|
75
|
+
- **Cluster** — implies homogeneity (every node same code, same role). A photos mikser and an invoices mikser are different roles.
|
|
76
|
+
- **Mesh** — implies n-to-n by default. Real parties talk to small explicit peer sets.
|
|
77
|
+
- **Federation** — implies central authority or shared schema. Each mikser owns its catalog and exposes whatever shape it wants.
|
|
78
|
+
|
|
79
|
+
Reads correctly at the call site:
|
|
80
|
+
|
|
81
|
+
```js
|
|
82
|
+
import { join } from 'party-mikser-io'
|
|
83
|
+
|
|
84
|
+
await join({
|
|
85
|
+
as: 'invoices',
|
|
86
|
+
peers: [
|
|
87
|
+
{ name: 'documents', url: 'http://localhost:8081' },
|
|
88
|
+
{ name: 'photos', url: 'http://localhost:8082' },
|
|
89
|
+
],
|
|
90
|
+
})
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## What lands in `mikser-io` to support this
|
|
94
|
+
|
|
95
|
+
These are the only `mikser-io` 10.0 changes — small, surgical, and they don't introduce peer-awareness into the engine. They expose information party-mikser-io needs in order to coordinate.
|
|
96
|
+
|
|
97
|
+
### `mikser-io-api` surface additions
|
|
98
|
+
|
|
99
|
+
Three new endpoints. None of them know about peers; they just expose what a peer would need to read.
|
|
100
|
+
|
|
101
|
+
| endpoint | purpose | rough LOC |
|
|
102
|
+
|---|---|---|
|
|
103
|
+
| `GET /api/identity` | mikser name (from config) + version + capabilities (plugin list + their versions) | ~20 |
|
|
104
|
+
| `GET /api/checkpoint` | current catalog snapshot id (for partition-recovery sync) | ~30 |
|
|
105
|
+
| `GET /api/refs/:id?direction=in|out` | inbound or outbound refs for an entity | ~40 |
|
|
106
|
+
|
|
107
|
+
The `subscribe` endpoint also gains a `?since=<checkpoint>` query param for replay-on-reconnect. ~20 LOC.
|
|
108
|
+
|
|
109
|
+
Total: ~110 LOC of additive endpoints. Nothing existing changes. Each one is independently testable.
|
|
110
|
+
|
|
111
|
+
### `mikser.config.js` gets a `name` field
|
|
112
|
+
|
|
113
|
+
```js
|
|
114
|
+
export default {
|
|
115
|
+
name: 'invoices', // stable identifier; used by /api/identity and party.join({as})
|
|
116
|
+
plugins: [...],
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Engine reads it, exposes via `runtime.name`. Default falls back to the working folder's basename. That's it — `mikser-io` doesn't act on it; party-mikser-io does.
|
|
121
|
+
|
|
122
|
+
## What `party-mikser-io` ships in 10.0
|
|
123
|
+
|
|
124
|
+
Not designing this in detail yet — the design happens as the package gets built. The shape at this stage is:
|
|
125
|
+
|
|
126
|
+
1. **`join(config)`** — handshake with each declared peer via `/api/identity`. Verifies peer is reachable and reports name/capabilities.
|
|
127
|
+
2. **`runtime.options.party.peers`** — map of peer name → metadata. Available to plugins after join completes.
|
|
128
|
+
3. **`mikser-io-source-party`** — source plugin: mounts a peer's `/api` SSE feed as a local collection. Live-reload via the existing subscribe semantics.
|
|
129
|
+
4. **Cross-mikser ref resolution** — `$author@documents` syntax (entity id + peer name). Resolution proxies through the source-party plugin's mirrored cache. **Design call deferred** to first implementation pass.
|
|
130
|
+
5. **Liveness checks** — periodic `/api/checkpoint` poll per peer. Stale peers logged but don't break the build (their entities go cold; resync on reconnect).
|
|
131
|
+
|
|
132
|
+
## Open questions (working proposals at v10 design time)
|
|
133
|
+
|
|
134
|
+
1. **Cross-mikser ref shape — mirror or proxy?**
|
|
135
|
+
- (a) Mirror: source-party plugin pulls all peer entities into local catalog. Refs resolve locally. Cost: memory grows with peer corpus size.
|
|
136
|
+
- (b) Proxy: refs resolve via HTTP at lookup time. Cost: latency on every render that touches a cross-mikser ref.
|
|
137
|
+
- Working proposal: **mirror with TTL** — pull peer entities lazily on first ref, cache with checkpoint-based invalidation. Worst-case memory is bounded by what's actually referenced.
|
|
138
|
+
|
|
139
|
+
2. **Discovery mechanism.**
|
|
140
|
+
- Config-pinned peer URLs (the snippet above) for v10. Anything more (mDNS, registry, DNS-SD) lives in 10.x or 11.0 after we see what real deployments need. Working proposal: **explicit URLs only for v10.**
|
|
141
|
+
|
|
142
|
+
3. **Authentication between peers.**
|
|
143
|
+
- The api plugin already supports per-endpoint tokens. Reuse that: each peer in `join()` config carries a token. Working proposal: **bearer token per peer, configured in `join()`.**
|
|
144
|
+
|
|
145
|
+
4. **What if two peers claim entities with the same id?**
|
|
146
|
+
- The fully-qualified form is `id@peer-name`. Local entities have no `@`. Working proposal: **namespacing by peer name avoids collision entirely** — cross-mikser refs always carry the peer name.
|
|
147
|
+
|
|
148
|
+
5. **What about cycles?**
|
|
149
|
+
- A subscribes to B, B subscribes to A. Working proposal: **explicitly allowed**. Each side mirrors the other; the checkpoint mechanism prevents replay loops.
|
|
150
|
+
|
|
151
|
+
## Out of scope for 10.0
|
|
152
|
+
|
|
153
|
+
- **Write-across-peers** — `party.create(entity, { in: 'invoices' })` from peer A creating an entity in peer B's catalog. Probably 10.x or 11.0. Single-direction read is enough to validate the architecture.
|
|
154
|
+
- **Distributed consensus / conflict resolution** — each peer owns its catalog, period. Nobody writes to anyone else's.
|
|
155
|
+
- **Replication / HA** — that's a different problem. A party isn't a cluster; if a peer goes down, its entities go cold, not get failed-over.
|
|
156
|
+
- **Plugin marketplace / discovery service** — same answer as 9.0. Not building this.
|
|
157
|
+
|
|
158
|
+
## ADR series
|
|
159
|
+
|
|
160
|
+
`party-mikser-io` gets its own decision log under `party-mikser-io/documentation/decisions/`, starting from 0001. Not extending the `mikser-io` ADR series — keeps the substrate test (#1) clean. `mikser-io` ADRs cover the single-instance engine; `party-mikser-io` ADRs cover the coordination plane.
|
|
161
|
+
|
|
162
|
+
Cross-references between the two ADR series are fine; the boundary stays at "does this code reference anything outside this mikser's `runtime`?"
|
|
163
|
+
|
|
164
|
+
## Concretely, what work this is (rough order)
|
|
165
|
+
|
|
166
|
+
1. **`mikser-io` engine** — add `runtime.name` (read from `mikser.config.js`). One file change.
|
|
167
|
+
2. **`mikser-io-api`** — add `/api/identity`, `/api/checkpoint`, `/api/refs/:id`, and `?since=` on subscribe. ~110 LOC.
|
|
168
|
+
3. **`party-mikser-io`** — new repo. `join()`, peer registry, liveness loop. The boring substrate of the coordination layer. ~400 LOC.
|
|
169
|
+
4. **`mikser-io-source-party`** — new repo (or sub-package). Source plugin that mounts a peer's catalog. The mirror cache + checkpoint invalidation logic. ~300 LOC.
|
|
170
|
+
5. **Cross-mikser ref resolution** — extends `mikser-io-source-party`. Parses `id@peer` syntax in `$`-refs and resolves through the mirror. ~150 LOC.
|
|
171
|
+
6. **Recipe documentation** — README composition examples: PDF mikser + CSV mikser + HTML mikser, piped together. The example that proves the v10 pitch works end-to-end.
|
|
172
|
+
7. **`mikser-io-example-party`** — new example repo. Three miksers in one repo (subfolders), each with its own config, demonstrating the full composition.
|
|
173
|
+
|
|
174
|
+
Items 1-2 ship in `mikser-io` 10.0 itself. Items 3-7 are the v10 ecosystem release.
|
|
175
|
+
|
|
176
|
+
## What this earns us, when it lands
|
|
177
|
+
|
|
178
|
+
The 9.0 pitch was "file-based knowledge substrate with AI superpowers." v10 extends that with one word: **composable**.
|
|
179
|
+
|
|
180
|
+
> A file-based knowledge substrate with AI superpowers, composable across instances.
|
|
181
|
+
|
|
182
|
+
The PDF-in-CSV-out-HTML-elsewhere story works as separate miksers connected through a party — not as one process trying to be everything. That's the architecture the substrate was designed for; v10 just makes it real.
|
package/9.0-PLAN.md
ADDED
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
# Mikser 9.0 — Planning
|
|
2
|
+
|
|
3
|
+
**The shift in one line:** `mikser-io` moves from "AI-native SSG" to "file-based knowledge substrate with AI superpowers." Layouts (and the other rendering plugins currently bundled in `src/plugins/`) move out to their own packages. A new AI substrate plugin family (`mikser-io-transformers` and friends) registers inference pipelines that consumer plugins compose against.
|
|
4
|
+
|
|
5
|
+
This is a planning document, not an ADR. The ADR(s) land after the move so the wording reflects what actually shipped. This file gets dropped at release.
|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
The OCR + structured-extraction conversation forced the question: when "drop a folder of PDFs and ask an AI agent about them" works as a primary use case, is layouts still in the substrate's critical path? No. SSG is one composition of the substrate, not the substrate itself.
|
|
10
|
+
|
|
11
|
+
Re-running ADR-0006's five-test against `layouts`:
|
|
12
|
+
|
|
13
|
+
| test | result |
|
|
14
|
+
|---|---|
|
|
15
|
+
| 1. Substrate? Engine genuinely needs this | **No** — document/knowledge consumers don't render layouts |
|
|
16
|
+
| 2. Strengthens strategy? Load-bearing for positioning | **Was. Isn't post-shift.** |
|
|
17
|
+
| 3. God-plugin check? Engine becoming kitchen sink | **Yes** — ~900 LOC of pagination + sidecar + sitemap on top of dispatch |
|
|
18
|
+
| 4. Composability? Could ship external | **Yes** — only uses public surfaces |
|
|
19
|
+
| 5. Release cadence? Same as engine | **No** — iterates at SSG-feature cadence |
|
|
20
|
+
|
|
21
|
+
Three fails. Test 2 became a fail when positioning shifted. The architecture doesn't believe in layouts-in-core anymore; only inertia does.
|
|
22
|
+
|
|
23
|
+
## What stays in the engine
|
|
24
|
+
|
|
25
|
+
`src/plugins/` slims from 14 directories to 5:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
documents · files · front-matter · yaml · json
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
These are entity-sourcing primitives — any consumer reading from a working folder needs them. Plus the substrate code (`runtime.js`, `engine.js`, `lifecycle.js`, `catalog.js`, `refs.js`, `manifest.js`, `journal.js`, `database/`, `source.js`, `subscriptions.js`, `track.js`, `render.js`/`postprocess.js` as dispatch shims — the actual rendering lives in plugins).
|
|
32
|
+
|
|
33
|
+
## What moves out
|
|
34
|
+
|
|
35
|
+
Each becomes its own package with its own version cadence:
|
|
36
|
+
|
|
37
|
+
| package | what moves | approx. LOC |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| `mikser-io-layouts` | layout matching + pagination + sidecar protocol | ~900 |
|
|
40
|
+
| `mikser-io-assets` | preset processing + binary pipelines | ~600 |
|
|
41
|
+
| `mikser-io-resources` | external resource fetching | ~400 |
|
|
42
|
+
| `mikser-io-preview` | in-memory render cache + `GET /preview/` | ~250 |
|
|
43
|
+
| `mikser-io-data` | JSON catalog exports | ~150 |
|
|
44
|
+
| `mikser-io-observer` | HTTP source polling | ~200 |
|
|
45
|
+
| `mikser-io-mapper` | config-driven field rewrites | ~100 |
|
|
46
|
+
| `mikser-io-validator` | custom validation hooks | ~100 |
|
|
47
|
+
| `mikser-io-commands` | cron jobs | ~150 |
|
|
48
|
+
| `mikser-io-shares` | multi-output replication | ~200 |
|
|
49
|
+
|
|
50
|
+
Engine LOC drops ~40%. None of these plugins is wrong to ship — they're just not substrate.
|
|
51
|
+
|
|
52
|
+
## New: AI substrate
|
|
53
|
+
|
|
54
|
+
The substrate exposes a verb-level pipelines surface:
|
|
55
|
+
|
|
56
|
+
```js
|
|
57
|
+
runtime.options.pipelines = {
|
|
58
|
+
embed: (text, opts?) => Promise<number[]>,
|
|
59
|
+
imageEmbed: (imageBuffer, opts?) => Promise<number[]>,
|
|
60
|
+
ocr: (input, opts?) => Promise<string>,
|
|
61
|
+
extract: (input, { schema, prompt }, opts?) => Promise<unknown>,
|
|
62
|
+
classify: (text, labels, opts?) => Promise<{ label, score }[]>,
|
|
63
|
+
rerank: (query, docs, opts?) => Promise<{ idx, score }[]>,
|
|
64
|
+
summarize: (text, opts?) => Promise<string>,
|
|
65
|
+
transcribe: (audioBuffer, opts?) => Promise<string>,
|
|
66
|
+
translate: (text, { from, to }, opts?) => Promise<string>,
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
All pipeline functions accept `{ signal }` so consumer plugins can pass through the cycle's abort signal. Watch-mode cancellations short-circuit pending inference cleanly.
|
|
71
|
+
|
|
72
|
+
### Provider plugins
|
|
73
|
+
|
|
74
|
+
Each provider plugin registers only the pipelines it can support:
|
|
75
|
+
|
|
76
|
+
| plugin | typical pipelines | runtime requirement |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| `mikser-io-transformers` | embed, imageEmbed, ocr, extract, classify, rerank, summarize, transcribe | `@xenova/transformers`; first-run model download |
|
|
79
|
+
| `mikser-io-openai` | embed, extract (via JSON mode), classify, summarize, transcribe | `OPENAI_API_KEY` |
|
|
80
|
+
| `mikser-io-ollama` | embed, summarize (depends on pulled models) | ollama daemon |
|
|
81
|
+
| `mikser-io-anthropic` | extract, classify, summarize | `ANTHROPIC_API_KEY` |
|
|
82
|
+
|
|
83
|
+
Consumer plugins gate on what's registered:
|
|
84
|
+
|
|
85
|
+
```js
|
|
86
|
+
onLoaded(() => {
|
|
87
|
+
if (!runtime.options.pipelines?.ocr) {
|
|
88
|
+
logger.warn(
|
|
89
|
+
'mikser-io-ocr needs an ocr pipeline — install mikser-io-transformers ' +
|
|
90
|
+
'(or another provider that registers ocr).'
|
|
91
|
+
)
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
// wire up
|
|
95
|
+
})
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Same gate pattern the api / preview / mcp plugins already use against `runtime.options.app` / `runtime.options.mcp`.
|
|
99
|
+
|
|
100
|
+
## New consumer plugins
|
|
101
|
+
|
|
102
|
+
| package | needs pipeline | what it does |
|
|
103
|
+
|---|---|---|
|
|
104
|
+
| `mikser-io-vector` | embed | semantic search over text (refactored to depend on substrate) |
|
|
105
|
+
| `mikser-io-ocr` | ocr | populate `entity.content` from PDF/image source files |
|
|
106
|
+
| `mikser-io-extract` | extract | schema-driven structured extraction into `entity.meta` |
|
|
107
|
+
| `mikser-io-image-search` | imageEmbed | CLIP-based image search; text query → matching images |
|
|
108
|
+
| `mikser-io-rerank` | rerank | post-process vector search results with cross-encoder |
|
|
109
|
+
| `mikser-io-summarize` | summarize | auto-populate `meta.summary` when authors don't |
|
|
110
|
+
| `mikser-io-classify` | classify | zero-shot tag suggestions per entity create/update |
|
|
111
|
+
| `mikser-io-transcribe` | transcribe | podcasts/videos → searchable transcripts |
|
|
112
|
+
| `mikser-io-translate` | translate | multilingual content |
|
|
113
|
+
|
|
114
|
+
Each is small — 100-400 LOC. The substrate handles model lifecycle (download, cache, progress, dimension introspection); the consumer handles the entity-lifecycle integration.
|
|
115
|
+
|
|
116
|
+
## Format liberation: source ↔ render symmetry
|
|
117
|
+
|
|
118
|
+
Mikser already has rendering plugins that emit entities AS specific formats (`render-hbs`, `render-eta`, `render-liquid`, `render-markdown`, `render-file`). The 9.0 lens reveals the missing other half: matching source plugins that parse FROM specific formats INTO entities. The substrate becomes a **format translation layer** — anything in, anything out, all composing on the same catalog.
|
|
119
|
+
|
|
120
|
+
### The symmetry
|
|
121
|
+
|
|
122
|
+
| source plugin (format → entities) | render plugin (entities → format) | format |
|
|
123
|
+
|---|---|---|
|
|
124
|
+
| `documents` (md/html/yml/json) | `render-hbs`, `render-eta`, `render-liquid`, `render-markdown` | text documents |
|
|
125
|
+
| `files` (any binary) | `post-pdf`, `post-mjml` | binary outputs |
|
|
126
|
+
| `mikser-io-csv` *(new)* | `mikser-io-render-csv` *(new)* | CSV |
|
|
127
|
+
| existing (api plugin's data exports) | `mikser-io-data` *(extracted in 9.0)* | JSON snapshots |
|
|
128
|
+
| `mikser-io-ocr` + `mikser-io-extract` (PDF/image → entities) | `mikser-io-render-pdf` *(future)* | PDF documents |
|
|
129
|
+
| future: `mikser-io-ical`, `mikser-io-vcard`, `mikser-io-rss` | future: matching renderers | calendars, contacts, feeds |
|
|
130
|
+
|
|
131
|
+
The substrate doesn't care which side a format lives on. The catalog's job is to hold queryable entities; source plugins fill it from whatever the world produces; render plugins emit whatever the world consumes.
|
|
132
|
+
|
|
133
|
+
### The round-trip that demonstrates it
|
|
134
|
+
|
|
135
|
+
A folder of scanned PDF invoices walks through the entire stack and lands as a CSV for the accountant — without anyone writing pipeline code:
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
PDFs in documents/invoices/
|
|
139
|
+
↓ documents plugin (file → entity, raw)
|
|
140
|
+
↓ mikser-io-ocr (OCR pipeline populates entity.content)
|
|
141
|
+
↓ mikser-io-extract (schema-driven, populates entity.meta)
|
|
142
|
+
→ catalog now holds typed invoice entities
|
|
143
|
+
↓ mikser-io-render-csv (entities → rows in invoices.csv)
|
|
144
|
+
+ mikser-io-vector (semantic search across invoices)
|
|
145
|
+
+ mikser-io-mcp (agent answers "which Q1 vendors charged us most")
|
|
146
|
+
+ mikser-io-api (HTTP /api/public/entities/invoices)
|
|
147
|
+
|
|
148
|
+
Each individual file → live-reload on watch-mode
|
|
149
|
+
A schema change → re-extraction on next cycle
|
|
150
|
+
A new PDF dropped in → automatic ingest + index + export
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Edit a PDF, save it, the CSV regenerates with the corrected row a couple seconds later. The accountant pulls `invoices.csv` from a synced folder. The AI agent answers questions. The API exposes the data. **All from the same canonical source — the PDFs on disk.**
|
|
154
|
+
|
|
155
|
+
### Why this is a positioning shift, not just a feature list
|
|
156
|
+
|
|
157
|
+
Other tools trap your data in their source format:
|
|
158
|
+
|
|
159
|
+
- **Notion** — your data lives in their proprietary database tables; export is a deliberate, lossy operation
|
|
160
|
+
- **Airtable** — same shape with a different paint job
|
|
161
|
+
- **Glean** — indexes your data but you can't re-emit it in another format cleanly
|
|
162
|
+
- **OneDrive / Dropbox / Box** — store files but don't make their content queryable
|
|
163
|
+
|
|
164
|
+
Mikser flips this. Source format is where data enters; **what shape it takes after that is the user's call.** PDF in → CSV out. CSV in → HTML out. Markdown in → semantic embeddings out. Anything in → anything out, with watch-mode keeping every output current as the inputs change.
|
|
165
|
+
|
|
166
|
+
The 9.0 plugin set turns this from architectural potential into a shipped product. With `csv`, `render-csv`, `ocr`, `extract`, `transformers`, `vector`, and the existing render-* family, mikser is a real "knowledge format translation substrate" rather than "an SSG that has some AI hooks."
|
|
167
|
+
|
|
168
|
+
### What lands in 9.0 specifically
|
|
169
|
+
|
|
170
|
+
| package | role | LOC est. |
|
|
171
|
+
|---|---|---|
|
|
172
|
+
| `mikser-io-csv` | source: CSV file → row entities (one entity per row; `idColumn` config for stable ids) | ~250 |
|
|
173
|
+
| `mikser-io-render-csv` | render: entities → CSV file (RFC 4180 escaping; column ordering from sidecar) | ~150 |
|
|
174
|
+
|
|
175
|
+
Future render/source pairs (post-9.0, demand-driven): `render-pdf`, `render-rss`, `render-ical`, `ical` source, `vcard` source. Each ~150-300 LOC. The substrate carries them.
|
|
176
|
+
|
|
177
|
+
## Schemas plugin pulls real weight here
|
|
178
|
+
|
|
179
|
+
`mikser-io-schemas` has been sitting idle. Under the 9.0 shape it becomes load-bearing:
|
|
180
|
+
|
|
181
|
+
- Declares entity shapes via zod
|
|
182
|
+
- `extract: true` flag per schema turns the declaration into an auto-extraction target
|
|
183
|
+
- `mikser-io-extract` walks new entities, looks up the schema for the entity's `meta.layout`, calls `runtime.options.pipelines.extract(input, { schema })`, validates the result, populates `entity.meta`
|
|
184
|
+
- Validated structured data becomes queryable through standard indexed columns
|
|
185
|
+
|
|
186
|
+
Drop a folder of PDF invoices → schema declares `{vendor, date, items, total}` → catalog has fully-typed invoice rows → `findEntities({'meta.layout': 'invoice', 'meta.total': {$gt: 5000}})` returns them.
|
|
187
|
+
|
|
188
|
+
The schemas plugin also needs the lifecycle-ordering bug fixed before this lands (currently `onValidate` fires before `front-matter` populates `meta`). Separate but related work.
|
|
189
|
+
|
|
190
|
+
## Recipe compositions
|
|
191
|
+
|
|
192
|
+
**SSG** (today's flagship — same shape, different distribution):
|
|
193
|
+
|
|
194
|
+
```js
|
|
195
|
+
plugins: [
|
|
196
|
+
'documents', 'files', 'front-matter', 'yaml',
|
|
197
|
+
'layouts',
|
|
198
|
+
'render-hbs', 'render-markdown', 'render-href',
|
|
199
|
+
'assets', 'resources', 'preview',
|
|
200
|
+
'data',
|
|
201
|
+
]
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
**Document management system**:
|
|
205
|
+
|
|
206
|
+
```js
|
|
207
|
+
plugins: [
|
|
208
|
+
'documents', 'files', 'front-matter', 'yaml',
|
|
209
|
+
'transformers', 'ocr', 'extract',
|
|
210
|
+
'schemas',
|
|
211
|
+
'vector',
|
|
212
|
+
'api', 'mcp',
|
|
213
|
+
]
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
**Personal knowledge management**:
|
|
217
|
+
|
|
218
|
+
```js
|
|
219
|
+
plugins: [
|
|
220
|
+
'documents', 'files', 'front-matter', 'yaml',
|
|
221
|
+
'transformers', 'ocr', 'extract', 'schemas',
|
|
222
|
+
'vector',
|
|
223
|
+
'mcp',
|
|
224
|
+
]
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
No HTTP — Claude Desktop talks MCP directly. An Obsidian competitor in ~8 plugins.
|
|
228
|
+
|
|
229
|
+
**Photo library with semantic search**:
|
|
230
|
+
|
|
231
|
+
```js
|
|
232
|
+
plugins: [
|
|
233
|
+
'files',
|
|
234
|
+
'transformers', 'image-search',
|
|
235
|
+
'api', 'mcp',
|
|
236
|
+
]
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
No `documents` even. Just files + image embeddings + access.
|
|
240
|
+
|
|
241
|
+
**Tabular data workflow** (the round-trip from the format-liberation section):
|
|
242
|
+
|
|
243
|
+
```js
|
|
244
|
+
plugins: [
|
|
245
|
+
'documents', 'files', 'front-matter', 'yaml',
|
|
246
|
+
'csv', // CSV files → row entities
|
|
247
|
+
'transformers', 'ocr', 'extract', // PDFs → row entities via schema
|
|
248
|
+
'schemas',
|
|
249
|
+
'render-csv', 'render-hbs', // entities → CSV + HTML
|
|
250
|
+
'data',
|
|
251
|
+
'api', 'mcp',
|
|
252
|
+
]
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
PDF invoices and CSV exports land as the same entity shape. Editor renders a dashboard HTML page; accountant downloads regenerated CSV; agent answers semantic questions. One source of truth, three output formats.
|
|
256
|
+
|
|
257
|
+
**Newsletter platform**:
|
|
258
|
+
|
|
259
|
+
```js
|
|
260
|
+
plugins: [
|
|
261
|
+
'documents', 'files', 'front-matter', 'yaml',
|
|
262
|
+
'layouts', 'render-hbs',
|
|
263
|
+
'post-mjml', 'post-mailchimp',
|
|
264
|
+
'data',
|
|
265
|
+
]
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Each composition is a coherent product. None of them carry weight they don't need.
|
|
269
|
+
|
|
270
|
+
## Migration shape
|
|
271
|
+
|
|
272
|
+
ADR-0002 (files-as-truth) plus the schema-version recovery pattern shipped in 8.3.7 carry the migration. The engine knows the world has changed and tells the user how to fix it.
|
|
273
|
+
|
|
274
|
+
1. **`mikser-io@9.0.0`** — `src/plugins/` slimmed to the five engine plugins. Legacy stub modules at the old paths throw a clear error:
|
|
275
|
+
|
|
276
|
+
```
|
|
277
|
+
Plugin "layouts" not found in src/plugins/. It has moved to its own
|
|
278
|
+
package — run: npm install mikser-io-layouts
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
`schema_version` stamp bumps; auto-recover wipes the cache on first run (no manual `--clear` needed, per 8.3.7).
|
|
282
|
+
|
|
283
|
+
2. **New plugin repos shipped at the same time**:
|
|
284
|
+
- `mikser-io-layouts` (the big one)
|
|
285
|
+
- `mikser-io-assets`, `mikser-io-resources`, `mikser-io-preview`, `mikser-io-data`
|
|
286
|
+
- `mikser-io-observer`, `mikser-io-mapper`, `mikser-io-validator`, `mikser-io-commands`, `mikser-io-shares`
|
|
287
|
+
|
|
288
|
+
3. **New AI plugin repos**:
|
|
289
|
+
- `mikser-io-transformers` (substrate)
|
|
290
|
+
- `mikser-io-ocr`, `mikser-io-extract` (consumer plugins for the document workflow)
|
|
291
|
+
- Others land on rolling cadence as demand justifies
|
|
292
|
+
|
|
293
|
+
4. **`mikser-io-vector` bumps to 2.0.0** — drops its OpenAI HTTP client, depends on the pipelines substrate. Old config keys (`vector.openai.apiKey`) keep working through a one-time deprecation cycle if `mikser-io-openai` is loaded; recommended path is `mikser-io-transformers` (no API key, fully offline).
|
|
294
|
+
|
|
295
|
+
5. **README rewrite** — "Getting Started" splits into recipes. The current single-default-everyone-gets-SSG path becomes one of several. Each recipe shows its plugin list, the workflow it enables, and links to an example repo.
|
|
296
|
+
|
|
297
|
+
6. **Example repos**:
|
|
298
|
+
- `mikser-io-example-blog` (existing — the SSG recipe)
|
|
299
|
+
- `mikser-io-example-dms` (new — document management)
|
|
300
|
+
- `mikser-io-example-pkm` (new — personal knowledge management)
|
|
301
|
+
- `mikser-io-example-photos` (new — image library)
|
|
302
|
+
|
|
303
|
+
## `mikser --install` — explicit plugin installation
|
|
304
|
+
|
|
305
|
+
The plugin count balloons from "everyone uses the default" to "everyone composes a recipe." A document management recipe is 10 packages; an SSG recipe is 12. Without a clean install story, every recipe shift is "edit `package.json`, `npm install`, hit a missing plugin, repeat."
|
|
306
|
+
|
|
307
|
+
But the engine shouldn't shell out to a package manager during normal runs. No magic, no surprise installs as a side effect of `mikser`. Package management is its own step, run explicitly.
|
|
308
|
+
|
|
309
|
+
### Shape
|
|
310
|
+
|
|
311
|
+
`mikser --install` reads `mikser.config.js`, identifies which plugins aren't resolvable via the existing four-step lookup chain, batches them into one install call, and exits. No `onInitialize`, no `onLoaded`, no render, no watch — the substrate doesn't even open `mikser.sqlite`. Just dependency resolution and the package install.
|
|
312
|
+
|
|
313
|
+
```
|
|
314
|
+
$ mikser --install
|
|
315
|
+
|
|
316
|
+
🟡 Plugins not installed locally: layouts, vector, transformers
|
|
317
|
+
Resolving to: mikser-io-layouts, mikser-io-vector, mikser-io-transformers
|
|
318
|
+
Running: npm install --save mikser-io-layouts mikser-io-vector mikser-io-transformers
|
|
319
|
+
[npm output...]
|
|
320
|
+
✓ Installed 3 plugins (4.2s)
|
|
321
|
+
|
|
322
|
+
$ mikser
|
|
323
|
+
🟢 Continuing normally...
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
Plain `mikser` without `--install` does **not** auto-install. If a plugin is missing, the engine fails with a clear error pointing at the install command:
|
|
327
|
+
|
|
328
|
+
```
|
|
329
|
+
$ mikser
|
|
330
|
+
|
|
331
|
+
🔴 Plugin "layouts" not found. Install with:
|
|
332
|
+
mikser --install
|
|
333
|
+
|
|
334
|
+
(or directly: npm install mikser-io-layouts)
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
The fallback path is the same as the schema-version recovery shipped in 8.3.7 — engine explains exactly what's wrong and exactly how to fix it. The fix is one command.
|
|
338
|
+
|
|
339
|
+
### Implementation notes
|
|
340
|
+
|
|
341
|
+
- **Package name resolution.** Plugin name `layouts` → package `mikser-io-layouts`. Same convention as today's loader. User can override per-plugin in config when they need a non-standard package name:
|
|
342
|
+
|
|
343
|
+
```js
|
|
344
|
+
plugins: [
|
|
345
|
+
'documents',
|
|
346
|
+
{ name: 'custom', package: 'my-org-mikser-custom' },
|
|
347
|
+
]
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
- **Package manager detection.** Detect from lock file in the working folder:
|
|
351
|
+
- `package-lock.json` → npm
|
|
352
|
+
- `yarn.lock` → yarn
|
|
353
|
+
- `pnpm-lock.yaml` → pnpm
|
|
354
|
+
- `bun.lockb` → bun
|
|
355
|
+
- Default to npm if no lock file detected. CLI override via `--package-manager` for edge cases.
|
|
356
|
+
|
|
357
|
+
- **Batched install.** Scan all missing plugins, install in one call. Ten missing plugins → one network round-trip, not ten.
|
|
358
|
+
|
|
359
|
+
- **Persistence via `--save`.** Updates `package.json` so the deps are recorded. Future `mikser` invocations find them via normal node resolution. No state hidden in `runtime/` or anywhere else.
|
|
360
|
+
|
|
361
|
+
- **Version pinning.** Pin to the engine's matching major: `mikser-io@9.x` installs `mikser-io-layouts@^9`. Prevents accidental cross-major drift. Users can override per-plugin with explicit version in the config object form (`{name, package, version}`).
|
|
362
|
+
|
|
363
|
+
- **Safe-by-name.** Only resolve plugin names matching the `mikser-io-*` convention (or the explicit `package:` override). Catches typos before installing random packages — `plugins: ['layoutz']` errors at resolution time instead of silently installing `mikser-io-layoutz`.
|
|
364
|
+
|
|
365
|
+
- **Network failure → clear error.** No network, no install. Standard npm error with the engine's note: "Run when you have network connectivity, or install the listed packages manually."
|
|
366
|
+
|
|
367
|
+
- **`--install` is loud.** The install step logs every resolved package name, the exact command run, the duration, and the result. The user explicitly asked for this; show them what happened.
|
|
368
|
+
|
|
369
|
+
### CLI
|
|
370
|
+
|
|
371
|
+
```
|
|
372
|
+
mikser # normal run; fails clearly on missing plugins
|
|
373
|
+
mikser --install # resolve + install missing plugins; exit
|
|
374
|
+
mikser --install --package-manager=pnpm # override lock-file detection
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
That's the whole surface. No `--no-auto-install` (nothing to disable). No `NODE_ENV` branch (no behavior to gate on environment).
|
|
378
|
+
|
|
379
|
+
### Why no auto-install
|
|
380
|
+
|
|
381
|
+
Two compounding reasons:
|
|
382
|
+
|
|
383
|
+
1. **No magic during normal runs.** `mikser` is for building. `mikser --install` is for installing. One command does one thing. The engine doesn't shell out to npm as a side effect of starting the lifecycle — that would surprise CI, surprise production deploys, surprise air-gapped environments, and surprise anyone debugging "why did `mikser` take 30 seconds to start?"
|
|
384
|
+
|
|
385
|
+
2. **It matches the rest of the npm ecosystem.** `node app.js` doesn't auto-install missing packages. `next build` doesn't. `vite` doesn't. Adding auto-install to mikser would be the odd one out for no real win — the install command is one line, the error message tells you exactly what to run, the workflow is "edit config → `mikser --install` → `mikser`" which is the same shape every JS project already follows.
|
|
386
|
+
|
|
387
|
+
The friction the auto-install would have saved is one explicit command after editing the plugin list. The cost would have been: engine that may install packages during `mikser`, harder-to-reason-about CI, NODE_ENV branches, watch-mode interaction edge cases, and a "what just got installed?" question on every cold cycle.
|
|
388
|
+
|
|
389
|
+
The trade isn't worth it. `--install` as the explicit, opt-in command is the right shape.
|
|
390
|
+
|
|
391
|
+
### What this isn't
|
|
392
|
+
|
|
393
|
+
- **Not a package manager.** Mikser shells out to npm/yarn/pnpm/bun for the install. No proprietary install logic, no opinion about lockfiles, no version solver. The engine just identifies which packages need to be installed and asks the user's existing tool to do it.
|
|
394
|
+
- **Not a cross-major migration tool.** Pins to the matching major. Pre-9 plugins don't get upgraded through this path; users explicitly `npm install mikser-io-X@9` if they need to.
|
|
395
|
+
|
|
396
|
+
## Out of scope / deferred
|
|
397
|
+
|
|
398
|
+
- **ADR-0010** ("the engine is the substrate; rendering is a plugin"). Written when the move is done so the wording reflects what shipped, not a forward-projection. CLAUDE.md note added at that point too.
|
|
399
|
+
- **Plugin marketplace / discovery**. Not building this. README recipes + ecosystem docs handle discovery. Anyone shipping a third-party plugin uses the existing `mikser-io-<name>` convention.
|
|
400
|
+
- **Automated migration tool**. The legacy stub errors give the `npm install` command. That's the migration tool. Users running `mikser` after upgrade get told what to install per missing plugin; one `npm install` per package gets them running again.
|
|
401
|
+
- **Mikser-to-mikser composition** (v10 territory, separate package). The scaling answer for big corpora isn't "make one mikser hold N million entities"; it's "compose N miksers, each holding a domain-sized corpus, talking through a coordination layer." That coordination layer ships as **`party-mikser-io`** — its own package, its own ADR series, its own release cadence. Naming it separately is deliberate: cross-instance concerns (discovery, peer identity, routing, cross-mikser refs, watch propagation, liveness, partition behavior, peer auth) belong to a distributed-systems substrate that iterates at "we hit a production partition bug" speed, not at engine-substrate speed. Folding any of it into `mikser-io` would re-fail ADR-0006 test #5 (release cadence) the moment it ships. The metaphor: each mikser is a guest in a party — voluntary attendance, each brings what it owns, the host knows who's there but nobody's centrally in charge. Until `party-mikser-io` ships, `mikser-io`'s design center stays "one folder, one mikser, domain-sized catalog." This deferral also kills any 9.0-era pressure to add output streaming, iterator-first API redesigns, or journal chunked commits — those exist to make a single process scale, which isn't where v10 is heading.
|
|
402
|
+
|
|
403
|
+
## Open questions to settle before shipping
|
|
404
|
+
|
|
405
|
+
1. **Naming.** "Engine plugins" vs "substrate" vs "core" — what's the term we use in the README to distinguish what ships in `mikser-io` from what doesn't? Working proposal: **"substrate"** for the engine-only pieces (catalog, refs, manifest, journal, the 5 entity-sourcing plugins), **"plugins"** for everything else (internal or external — no asymmetry in language because there's no asymmetry in mechanism).
|
|
406
|
+
|
|
407
|
+
2. **Pipeline signal contract.** All pipeline functions accept `{ signal }` for abort. Required by the substrate's TypeScript types? Or convention with the substrate enforcing at registration time? Working proposal: **required** — the substrate validates registered functions have the signature.
|
|
408
|
+
|
|
409
|
+
3. **Multiple provider plugins loaded at once.** Can `mikser-io-transformers` AND `mikser-io-openai` both register `embed`? If yes, which one wins for the shared verb name, and can consumers explicitly request one vendor? Working proposal: **registration order wins for the shared name**. Consumer can reach a specific provider via `runtime.options.pipelines.embed.openai(text)` for vendor-specific cases. The substrate exposes both.
|
|
410
|
+
|
|
411
|
+
4. **Schemas plugin integration with extract.** `mikser-io-extract` needs to read schema declarations from `mikser-io-schemas` to know what shape to coerce into. Two options:
|
|
412
|
+
- (a) schemas plugin exposes `runtime.options.schemas.lookup(entity)` → returns the matching zod schema. Extract consumes that surface.
|
|
413
|
+
- (b) schemas plugin enriches entities with the schema reference attached; extract reads it off the entity.
|
|
414
|
+
Working proposal: **(a)** — lookup surface. Keeps the schema declaration in one place; extract reads on demand.
|
|
415
|
+
|
|
416
|
+
5. **Version bump cascade.** `mikser-io` 9.0, `mikser-io-vector` 2.0, layouts/assets/etc. 1.0 (initial extraction releases). Coordinated or independent? Working proposal: **coordinated for the initial 9.0 set** (so users can `npm install` a consistent slate); independent cadence after.
|
|
417
|
+
|
|
418
|
+
6. **What about `mikser-io-render-*`?** Those already live external. The question: do they stay shipped from their own repos as before, or get consolidated under `mikser-io-layouts` as siblings? Working proposal: **stay external as today** — they're rendering-engine adapters (hbs / eta / liquid / markdown / file), one per template language, and consolidating them serves nobody.
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
## Concretely, what work this is
|
|
422
|
+
|
|
423
|
+
Roughly in dependency order:
|
|
424
|
+
|
|
425
|
+
1. **mikser-io engine** — slim `src/plugins/`, add legacy stub error files at the old paths, document the `runtime.options.pipelines` surface in `documentation/api-reference.md`. Pipeline functions are no-op until a provider plugin registers them. **`mikser --install` command** (see section above) lands here too — same release, so users have a one-command way to populate a recipe's plugin set after editing config.
|
|
426
|
+
2. **mikser-io-transformers** — new repo. Wire @xenova/transformers; register embed, ocr, extract, classify, rerank, summarize, transcribe. Handle model download, cache directory, progress bar, dimension introspection.
|
|
427
|
+
3. **mikser-io-vector 2.0** — drop OpenAI HTTP client. Depend on substrate. Validate dimension against the registered embed pipeline.
|
|
428
|
+
4. **mikser-io-layouts** — new repo. Extract `src/plugins/layouts.js` verbatim. CI + tests.
|
|
429
|
+
5. **mikser-io-assets, mikser-io-resources, mikser-io-preview, mikser-io-data** — new repos. Verbatim extraction with their own version cadence.
|
|
430
|
+
6. **mikser-io-ocr** — new repo. Consumer plugin for the ocr pipeline.
|
|
431
|
+
7. **mikser-io-extract** — new repo. Consumer plugin for the extract pipeline. Wire into mikser-io-schemas's `lookup()` surface.
|
|
432
|
+
8. **mikser-io-csv** — new repo. Source plugin: each CSV row becomes a queryable entity. `idColumn` config for stable per-row ids; checksum-gated re-emit on file change.
|
|
433
|
+
9. **mikser-io-render-csv** — new repo. Render plugin. RFC 4180 escaping; aggregations from sidecar `findEntities()` calls; first non-text-document render plugin in the family.
|
|
434
|
+
10. **mikser-io-schemas** — fix lifecycle ordering (front-matter populates meta before validation fires). Add `extract: true` flag handling. Add `lookup(entity)` surface for the extract plugin.
|
|
435
|
+
11. **README rewrite** — recipe-driven Getting Started. Multiple compositions shown.
|
|
436
|
+
12. **mikser-io-example-dms, -pkm, -photos, -invoices** — new example repos. The invoices example shows the PDF → entity → CSV round-trip end-to-end.
|
|
437
|
+
|
|
438
|
+
Items 1-3 are blocking for the 9.0 release. Items 4-8 ship at 9.0 too (so users have something to install when the engine tells them a plugin moved). Items 9-10 follow but should aim for the same release window — without recipe docs, users don't know which composition to install.
|
package/CLAUDE.md
CHANGED
|
@@ -10,6 +10,16 @@ paths. No migration markdown. No `task: pool` legacy aliasing for
|
|
|
10
10
|
`task: inline`. Update READMEs and ADRs in place as source-of-truth
|
|
11
11
|
changes; rewrite, don't supersede. The freedom is the point.
|
|
12
12
|
|
|
13
|
+
**v9 has to pay out before v10 starts.** Not "ship v9 and start v10."
|
|
14
|
+
**Use v9 in real daily work until the workflow pays out**, then
|
|
15
|
+
v10. The v10 architecture (party-mikser-io) is more fun to design
|
|
16
|
+
than v9 documentation is to write — the gate exists because that
|
|
17
|
+
asymmetry kills projects. Concrete pass/fail signals in
|
|
18
|
+
`10.0-PLAN.md` under "The gate." If a session brings up v10 design
|
|
19
|
+
work, check the gate first; if v9 isn't paying out yet, the
|
|
20
|
+
question to answer is "what about v9 isn't working in daily use?"
|
|
21
|
+
— not "what should v10 look like?"
|
|
22
|
+
|
|
13
23
|
**Position mikser by what it is, not by speed.** Hugo wins the speed
|
|
14
24
|
race; mikser doesn't compete there. Position: AI-native, lifecycle-
|
|
15
25
|
observable, files-as-source-of-truth, full-cycle introspection. Speed
|
package/CLOUD-PLAN.md
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# Mikser Cloud — Planning (stub)
|
|
2
|
+
|
|
3
|
+
**The shift in one line:** mikser stops being only a CLI/library you install and becomes a hosted product. Pre-configured appliances (invoices, blog, photos, pkm, ...) run as managed containers; a hosted `party-mikser-io` instance orchestrates them per tenant. The OSS substrate doesn't change — cloud is a product built on it, not an extension of it.
|
|
4
|
+
|
|
5
|
+
This is a strategic stub, not a technical plan. Cloud isn't versioned alongside `mikser-io` (engine versions and product versions iterate independently). The document exists to stake the architectural boundary and name the hard problems before any cloud code gets written.
|
|
6
|
+
|
|
7
|
+
## The discipline this stub enforces
|
|
8
|
+
|
|
9
|
+
**OSS earns cloud, not the other way around.** Cloud doesn't get built until `mikser-io` 10.0 has been used in real workloads by users who didn't pay for it. Building cloud on an untested substrate compounds risk — every cloud assumption built on an unvalidated substrate assumption is a refactor waiting to happen.
|
|
10
|
+
|
|
11
|
+
The boundary check at any cloud-era PR: **does this code reference cloud concepts? If yes → wrong repo unless that repo is `mikser-cloud` or an appliance repo.** No multi-tenancy, no billing hooks, no platform observability in `mikser-io` or `party-mikser-io`.
|
|
12
|
+
|
|
13
|
+
## What cloud cleanly is
|
|
14
|
+
|
|
15
|
+
| layer | what ships | where it lives |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| **Hosted runtime** | Containers running mikser appliances. Persistent storage for files + `runtime/`. Health probes. Restart semantics. | `mikser-cloud` |
|
|
18
|
+
| **Appliances** | Pre-configured mikser images, one per problem domain. Each is `mikser-io` + opinionated plugin set + sample config + starter folder structure. | `mikser-appliance-<name>` per repo |
|
|
19
|
+
| **Party as a service** | Per-tenant hosted `party-mikser-io` instance. Routes discovery / refs / subscribe between the tenant's appliances. | `mikser-cloud` (uses party-mikser-io as a library) |
|
|
20
|
+
| **Control plane** | Web UI: spin up an appliance, connect it to others, expose to an MCP agent, billing, auth, tenant isolation. | `mikser-cloud` |
|
|
21
|
+
|
|
22
|
+
## Initial appliance set
|
|
23
|
+
|
|
24
|
+
Each is its own product (own repo, own README, own sample data, own support burden). Underestimating this is the easy mistake — five appliances = five products to maintain.
|
|
25
|
+
|
|
26
|
+
| appliance | plugin set | the problem it solves |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| `mikser-appliance-blog` | documents, files, layouts, front-matter, yaml, render-hbs, render-markdown, vector, api, mcp | personal/team blog with semantic search and AI agent integration |
|
|
29
|
+
| `mikser-appliance-invoices` | files, csv, ocr, extract, transformers, schemas, render-csv, api, mcp | drop PDF invoices → typed catalog → exportable CSV → agent answers questions |
|
|
30
|
+
| `mikser-appliance-photos` | files, assets, transformers (image embeddings), vector, api, mcp | drop photo folder → semantic search by description |
|
|
31
|
+
| `mikser-appliance-pkm` | documents, front-matter, yaml, json, layouts, render-markdown, render-hbs, vector, mcp | obsidian-shaped knowledge base with AI agent |
|
|
32
|
+
| `mikser-appliance-dms` | files, ocr, extract, transformers, schemas, vector, api, mcp | document management: drop arbitrary docs → typed extraction → searchable archive |
|
|
33
|
+
|
|
34
|
+
Each appliance is opinionated. The point of an appliance is "you don't configure plugins, you don't pick a layout engine, you just use it." Power users still get `mikser-io` standalone with full control; appliances are the easy path.
|
|
35
|
+
|
|
36
|
+
## Hard problems cloud forces us to confront
|
|
37
|
+
|
|
38
|
+
Not dealbreakers, but real. Each one needs a designed answer before cloud launches.
|
|
39
|
+
|
|
40
|
+
### Files-as-source-of-truth in a container
|
|
41
|
+
|
|
42
|
+
The whole `mikser-io` pitch is "drop files in a folder." In a container, where IS that folder?
|
|
43
|
+
|
|
44
|
+
Options (each with cost):
|
|
45
|
+
|
|
46
|
+
- **Mounted persistent volume (EBS / pd / etc.)** — simplest mental model. Files live on a real disk per appliance. User edits via web upload or sync agent. Cost: scales per-appliance, snapshot/backup is the platform's problem.
|
|
47
|
+
- **S3-backed FS (s3fs / goofys / mountpoint-s3)** — files live in object storage. Cheap, infinite, but file-system semantics are partial. Chokidar might not work cleanly; rename + metadata operations can be slow or non-atomic.
|
|
48
|
+
- **Git-synced** — appliance pulls from a user-provided git repo. File source-of-truth lives in git, not the container. Best for power users; weird for non-technical ones.
|
|
49
|
+
- **Web upload only** — appliance has a control-plane UI for managing files. No local "folder" per se. Cost: builds a CMS-shaped UI, which is a product surface unto itself.
|
|
50
|
+
|
|
51
|
+
Working assumption: **persistent volume + web upload UI for non-power-users, optional git sync for power users.** S3-backed FS isn't worth the file-system-semantics tax. **Decision deferred** until first appliance is built.
|
|
52
|
+
|
|
53
|
+
### Multi-tenancy in the party host
|
|
54
|
+
|
|
55
|
+
A central party host serving N tenants must route discovery + refs + subscribe per tenant. One user's appliances can't see another's, ever.
|
|
56
|
+
|
|
57
|
+
`party-mikser-io` itself doesn't need to know about tenants — it's per-process. The cloud's deployment is **one party-mikser-io process per tenant**, sitting in front of that tenant's appliance set. Cost: N processes for N tenants. At scale, that's a real ops bill.
|
|
58
|
+
|
|
59
|
+
Alternative: a multi-tenant party host with tenant-aware routing. Cost: complicates `party-mikser-io` with tenant-awareness it shouldn't have. Working bias: **per-tenant process** — keeps party-mikser-io clean. Revisit if economics force consolidation.
|
|
60
|
+
|
|
61
|
+
### Persistent state lifecycle
|
|
62
|
+
|
|
63
|
+
`runtime/mikser.sqlite` is the catalog cache. ADR-0002: files are truth, the cache is derived. But re-deriving 50k entities + refs + manifest on container restart takes minutes — that's a cold-start UX problem.
|
|
64
|
+
|
|
65
|
+
Cloud needs the cache to survive restarts. The container's persistent volume holds `runtime/` alongside the file folder. On restart, `mikser` reuses the cache and rebuilds incrementally. This already works in `mikser-io`; cloud just needs the volume mount.
|
|
66
|
+
|
|
67
|
+
The non-obvious bit: **`runtime/` is per-appliance, not per-tenant**. If a tenant has 3 appliances, that's 3 sqlite files in 3 volumes. Don't try to consolidate.
|
|
68
|
+
|
|
69
|
+
### Watch-mode vs push
|
|
70
|
+
|
|
71
|
+
Chokidar is right for a laptop. In cloud, file changes come from API uploads / git pulls / webhook syncs. The trigger model is different.
|
|
72
|
+
|
|
73
|
+
Easiest path: the sync layer drops files into the volume; chokidar sees them and triggers rebuild. Works out of the box. The sync layer is the cloud platform's problem, not the engine's.
|
|
74
|
+
|
|
75
|
+
Worth verifying once cloud starts: chokidar on a mounted volume behaves predictably across pod restarts.
|
|
76
|
+
|
|
77
|
+
### Billing surface
|
|
78
|
+
|
|
79
|
+
Per-appliance-hour? Per-entity? Per-render? Per-MCP-call?
|
|
80
|
+
|
|
81
|
+
Each metric is a different observability investment. Per-render and per-MCP-call require engine-side counters that don't exist today. Per-entity is weird (what's an entity, a row of a CSV?). Per-appliance-hour is the simplest honest unit and matches how the user thinks ("I have 3 appliances running").
|
|
82
|
+
|
|
83
|
+
Working assumption: **per-appliance-hour, with a generous free tier on the smallest plan size.** Per-call billing for the API tier comes later if usage patterns justify it.
|
|
84
|
+
|
|
85
|
+
### Open-core boundary
|
|
86
|
+
|
|
87
|
+
The standard hard question. Two cleanly defensible answers:
|
|
88
|
+
|
|
89
|
+
- **Convenience-only cloud (working bias).** Everything in OSS works standalone. Cloud sells hosting + the appliance catalog + the web UI + the hosted party-mikser-io. No engine feature is paid-only. The OSS substrate stays the same software cloud runs on; contributors stay aligned.
|
|
90
|
+
- **Open-core with paid features.** Some engine features (advanced auth, audit logging, SLA-level support) become cloud/enterprise-only. Cost: contributor alienation, harder to make external use feel first-class.
|
|
91
|
+
|
|
92
|
+
The convenience-only model has worked for many shops (Cloudron, PostHog, Plausible). The open-core model has worked for others (GitLab, Sentry). For mikser specifically — file-based, agent-native, small surface area — **convenience-only is probably right**, but the decision deserves a real conversation when cloud actually starts.
|
|
93
|
+
|
|
94
|
+
### Appliance lifecycle as a product surface
|
|
95
|
+
|
|
96
|
+
Each appliance is its own product. That means:
|
|
97
|
+
|
|
98
|
+
- Its own README + docs
|
|
99
|
+
- Sample folder structure shipped with the image
|
|
100
|
+
- Sample data for the demo experience
|
|
101
|
+
- Schema definitions (for OCR + extract appliances)
|
|
102
|
+
- Plugin pinning + version compatibility
|
|
103
|
+
- Security review per release
|
|
104
|
+
- Support tickets specific to that appliance's domain
|
|
105
|
+
|
|
106
|
+
Five appliances at launch = five product tracks. Realistically, **start with one or two** (probably `blog` and `invoices`), validate the appliance-shape pattern, then expand.
|
|
107
|
+
|
|
108
|
+
## What cloud needs from the substrate (forward-look only — no commit)
|
|
109
|
+
|
|
110
|
+
Things that *might* land in `mikser-io` or `party-mikser-io` to enable cloud. Each one passes the substrate test independently — they're not cloud-specific:
|
|
111
|
+
|
|
112
|
+
| candidate | substrate justification |
|
|
113
|
+
|---|---|
|
|
114
|
+
| `GET /api/health` endpoint in mikser-io-api | Operationally useful for any deployment, not just cloud. Probably trivial. |
|
|
115
|
+
| Env-var-based config overrides in mikser-io | Already useful for OSS users running in CI. Cloud just consumes the same surface. |
|
|
116
|
+
| `--rebuild` mode (one-shot rebuild on signal, no chokidar) | Useful for any CI / scheduled-build scenario. |
|
|
117
|
+
| Structured operation metrics (entities created/updated/rendered per cycle) — already partially via pino logs | Useful for any operator wanting observability. |
|
|
118
|
+
| `mikser.config.js` deriving from env vars cleanly | Already standard JS — no engine change needed. |
|
|
119
|
+
| `party-mikser-io` as a standalone server binary, not just a library | Useful for any self-hosted multi-mikser deployment, not just cloud. |
|
|
120
|
+
|
|
121
|
+
None of these get added pre-emptively for cloud. Each goes through the five-test framework on its own merits, and lands when an OSS user benefits.
|
|
122
|
+
|
|
123
|
+
## What cloud explicitly is NOT
|
|
124
|
+
|
|
125
|
+
- **Not an Anthropic / Notion / Glean clone.** Mikser cloud sells hosted mikser, with mikser's specific positioning (file-based, agent-native, OSS substrate). It's not a general note-taking SaaS.
|
|
126
|
+
- **Not a Backend-as-a-Service.** No exposed-to-end-users database API. Each tenant gets appliances, not raw catalog access (except via the appliance's own MCP + api surfaces).
|
|
127
|
+
- **Not a multi-tenant single mikser.** No shared substrate between tenants. Each tenant gets their own container(s); ADR-0002 (files-as-source-of-truth) means a shared catalog across tenants would conflate user data, which is operationally and legally fraught.
|
|
128
|
+
- **Not a competitor to plugin authors.** Anyone shipping a `mikser-io-<name>` plugin can also be an appliance author. The appliance catalog is open; we're not gatekeeping the ecosystem.
|
|
129
|
+
|
|
130
|
+
## Out of scope (defer until cloud actually starts)
|
|
131
|
+
|
|
132
|
+
- Pricing — every dimension of it. Tiers, free-tier limits, overages, enterprise contracts. Real numbers need real cost data, which we won't have until we run actual appliances.
|
|
133
|
+
- Compliance posture (SOC 2, GDPR, HIPAA) — depends on which markets cloud targets. Decided when go-to-market is real.
|
|
134
|
+
- Geographic regions — same.
|
|
135
|
+
- White-label / self-hosted cloud for enterprises — possible product variant, but premature.
|
|
136
|
+
- Marketplace for third-party appliances — possible, but starting with first-party only is the sane shape.
|
|
137
|
+
|
|
138
|
+
## When this stub becomes a real plan
|
|
139
|
+
|
|
140
|
+
When two things are true:
|
|
141
|
+
|
|
142
|
+
1. `mikser-io` 10.0 has shipped and has at least one real-world deployment that isn't a toy example.
|
|
143
|
+
2. `party-mikser-io` has shipped and someone outside the immediate dev circle has composed two miksers without help.
|
|
144
|
+
|
|
145
|
+
If both of those are true, the substrate is validated enough that cloud can build on it confidently. This stub becomes `CLOUD-PLAN.md` proper, with timelines, appliance prioritization, and pricing dimensions.
|
|
146
|
+
|
|
147
|
+
Until both are true, this document exists to keep cloud as a coherent direction without distorting current engine work. That's the whole purpose of the stub.
|
package/package.json
CHANGED
package/src/database/index.js
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
// version, open() throws with a clear "run mikser --clear" message.
|
|
26
26
|
|
|
27
27
|
import path from 'node:path'
|
|
28
|
-
import { mkdirSync } from 'node:fs'
|
|
28
|
+
import { mkdirSync, unlinkSync, existsSync } from 'node:fs'
|
|
29
29
|
import Database from 'better-sqlite3'
|
|
30
30
|
import runtime from '../runtime.js'
|
|
31
31
|
import { onLoaded } from '../lifecycle.js'
|
|
@@ -49,6 +49,38 @@ const DEFAULT_FILENAME = 'mikser.sqlite'
|
|
|
49
49
|
// vector_documents in mikser-io-vector).
|
|
50
50
|
const schemas = new Map()
|
|
51
51
|
|
|
52
|
+
// Provisioning callbacks registered by plugins. Run on every open(),
|
|
53
|
+
// between the raw `new Database(dbPath)` (with PRAGMAs + meta bootstrap)
|
|
54
|
+
// and the schema apply pass. Each callback receives a context object:
|
|
55
|
+
//
|
|
56
|
+
// {
|
|
57
|
+
// firstRun: boolean — db file didn't exist before this open
|
|
58
|
+
// (or :memory:, or was wiped by --clear /
|
|
59
|
+
// schema mismatch)
|
|
60
|
+
// upgraded: boolean — stored schema_version differed from
|
|
61
|
+
// current (db was just wiped + recreated)
|
|
62
|
+
// previousVersion: string | null — the version stamp we found before
|
|
63
|
+
// this open, null if firstRun
|
|
64
|
+
// currentVersion: string — what this mikser binary expects
|
|
65
|
+
// handle: Database — the raw better-sqlite3 handle, for
|
|
66
|
+
// loadExtension / collations / custom
|
|
67
|
+
// functions / one-time data seeding
|
|
68
|
+
// logger: pino|null — for surfacing user-visible setup events
|
|
69
|
+
// }
|
|
70
|
+
//
|
|
71
|
+
// Use cases:
|
|
72
|
+
// - Load runtime extensions before they're referenced by schemas
|
|
73
|
+
// (sqlite-vec's vec0 module is the canonical example)
|
|
74
|
+
// - Register custom collations, application_id, etc.
|
|
75
|
+
// - First-run data seeding (use `if (ctx.firstRun)` to gate)
|
|
76
|
+
// - Upgrade-time data migrations (gate on `ctx.upgraded` and check
|
|
77
|
+
// `ctx.previousVersion`)
|
|
78
|
+
//
|
|
79
|
+
// Plugins register at module-eval time, same shape as registerSchema.
|
|
80
|
+
// Order doesn't matter unless callbacks depend on each other; if you
|
|
81
|
+
// have such a dep, both should be in the same plugin.
|
|
82
|
+
const provisioners = []
|
|
83
|
+
|
|
52
84
|
// Active database handle. Set during the first onLoaded; persists across
|
|
53
85
|
// cycles (sqlite stays open for the lifetime of the process). Public API
|
|
54
86
|
// readers see null only before the first onLoaded fires.
|
|
@@ -102,6 +134,53 @@ export function registerSchema(name, sqlScript) {
|
|
|
102
134
|
}
|
|
103
135
|
}
|
|
104
136
|
|
|
137
|
+
// Register a database-provisioning callback. Fires once per open(),
|
|
138
|
+
// AFTER PRAGMAs and the mikser_meta bootstrap (so version detection
|
|
139
|
+
// and the schema-mismatch wipe have already happened) and BEFORE
|
|
140
|
+
// schema apply (so callbacks can install virtual-table modules,
|
|
141
|
+
// register collations, etc. that the schemas about to apply may
|
|
142
|
+
// reference).
|
|
143
|
+
//
|
|
144
|
+
// Callback receives a context object with everything needed to act
|
|
145
|
+
// situationally:
|
|
146
|
+
//
|
|
147
|
+
// onProvision(({ firstRun, upgraded, previousVersion, currentVersion, handle, logger }) => {
|
|
148
|
+
// // Install runtime extensions on every open:
|
|
149
|
+
// sqliteVec.load(handle)
|
|
150
|
+
//
|
|
151
|
+
// // Seed default data on first run only:
|
|
152
|
+
// if (firstRun) {
|
|
153
|
+
// handle.exec("INSERT INTO mikser_meta (key, value) VALUES ('site_id', '...')")
|
|
154
|
+
// }
|
|
155
|
+
//
|
|
156
|
+
// // Migrate after a specific upgrade:
|
|
157
|
+
// if (upgraded && previousVersion?.startsWith('8.2')) {
|
|
158
|
+
// handle.exec("UPDATE my_plugin_table SET ...")
|
|
159
|
+
// }
|
|
160
|
+
// })
|
|
161
|
+
//
|
|
162
|
+
// Plugins register at module-eval time, same shape as registerSchema.
|
|
163
|
+
// Lazy-applies on the live handle if the database is already open
|
|
164
|
+
// (matches registerSchema's late-registration semantics) — the
|
|
165
|
+
// callback fires with firstRun=false, upgraded=false (since the open
|
|
166
|
+
// already happened cleanly), and the live handle.
|
|
167
|
+
export function onProvision(callback) {
|
|
168
|
+
if (typeof callback !== 'function') {
|
|
169
|
+
throw new Error('onProvision: callback must be a function (ctx) => void')
|
|
170
|
+
}
|
|
171
|
+
provisioners.push(callback)
|
|
172
|
+
if (db?.isOpen) {
|
|
173
|
+
callback({
|
|
174
|
+
firstRun: false,
|
|
175
|
+
upgraded: false,
|
|
176
|
+
previousVersion: db.provisioning?.currentVersion ?? null,
|
|
177
|
+
currentVersion: db.provisioning?.currentVersion ?? null,
|
|
178
|
+
handle: db.handle,
|
|
179
|
+
logger: runtime.engine?.logger ?? null,
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
105
184
|
// Return the active database handle. Hot-path callers (catalog, refs,
|
|
106
185
|
// manifest) should cache the reference once at onLoaded rather than
|
|
107
186
|
// calling per-operation.
|
|
@@ -118,8 +197,12 @@ export function useDatabase() {
|
|
|
118
197
|
// the full onLoaded chain). The runtime path uses this internally from
|
|
119
198
|
// onLoaded below.
|
|
120
199
|
export function createSqliteDatabase({
|
|
121
|
-
runtimeFolder, version, logger, config = {}, schemas,
|
|
200
|
+
runtimeFolder, version, logger, config = {}, schemas, provisioners: provisionersArg,
|
|
122
201
|
}) {
|
|
202
|
+
// Tests inject their own provisioners; the runtime path falls
|
|
203
|
+
// through to the module-level `provisioners` array that plugins
|
|
204
|
+
// populate via onProvision() at module-eval.
|
|
205
|
+
const provisionersToRun = provisionersArg ?? provisioners
|
|
123
206
|
// Resolve the on-disk path. Honor an absolute `config.filename`,
|
|
124
207
|
// otherwise resolve relative to the runtime folder. `:memory:`
|
|
125
208
|
// works for tests — better-sqlite3 treats it as ephemeral.
|
|
@@ -132,6 +215,11 @@ export function createSqliteDatabase({
|
|
|
132
215
|
: path.join(runtimeFolder, DEFAULT_FILENAME)
|
|
133
216
|
|
|
134
217
|
let handle = null
|
|
218
|
+
// Provisioning context exposed on the returned wrapper so any code
|
|
219
|
+
// with a handle (catalog onLoaded, plugin onLoaded, etc.) can ask
|
|
220
|
+
// "was this open a fresh db / a version upgrade / a normal cycle?"
|
|
221
|
+
// without subscribing to onProvision.
|
|
222
|
+
let provisioningCtx = null
|
|
135
223
|
|
|
136
224
|
function open() {
|
|
137
225
|
if (handle) return // idempotent — caller may invoke twice across cycles
|
|
@@ -140,34 +228,91 @@ export function createSqliteDatabase({
|
|
|
140
228
|
mkdirSync(path.dirname(dbPath), { recursive: true })
|
|
141
229
|
}
|
|
142
230
|
|
|
143
|
-
|
|
231
|
+
// Detect firstRun BEFORE we touch the filesystem. :memory:
|
|
232
|
+
// always counts as firstRun (no persistence across opens).
|
|
233
|
+
const fileExistedBeforeOpen = dbPath !== ':memory:' && existsSync(dbPath)
|
|
144
234
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
235
|
+
const setupConnection = () => {
|
|
236
|
+
handle.exec('PRAGMA journal_mode = WAL')
|
|
237
|
+
handle.exec('PRAGMA synchronous = NORMAL')
|
|
238
|
+
handle.exec('PRAGMA foreign_keys = ON')
|
|
239
|
+
handle.exec(`
|
|
240
|
+
CREATE TABLE IF NOT EXISTS mikser_meta (
|
|
241
|
+
key TEXT PRIMARY KEY,
|
|
242
|
+
value TEXT NOT NULL
|
|
243
|
+
)
|
|
244
|
+
`)
|
|
245
|
+
}
|
|
148
246
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
handle.exec(`
|
|
152
|
-
CREATE TABLE IF NOT EXISTS mikser_meta (
|
|
153
|
-
key TEXT PRIMARY KEY,
|
|
154
|
-
value TEXT NOT NULL
|
|
155
|
-
)
|
|
156
|
-
`)
|
|
247
|
+
handle = new Database(dbPath)
|
|
248
|
+
setupConnection()
|
|
157
249
|
|
|
158
250
|
const recorded = handle.prepare('SELECT value FROM mikser_meta WHERE key = ?')
|
|
159
251
|
.get('schema_version')?.value
|
|
252
|
+
let upgradedFromVersion = null
|
|
160
253
|
if (recorded && recorded !== version) {
|
|
254
|
+
// Schema mismatch on upgrade or downgrade. Per ADR-0002 the
|
|
255
|
+
// files on disk are the source of truth and this database
|
|
256
|
+
// is a derived cache, so the right behavior is to wipe the
|
|
257
|
+
// cache and let the next cycle rebuild it from source —
|
|
258
|
+
// not to halt the build with an error.
|
|
259
|
+
//
|
|
260
|
+
// Loud warning so operators can see it happened and know to
|
|
261
|
+
// expect a cold-start rebuild on this run. No data loss
|
|
262
|
+
// beyond the cache itself; everything in mikser.sqlite is
|
|
263
|
+
// recoverable from the working folder.
|
|
264
|
+
logger?.warn(
|
|
265
|
+
'Database schema mismatch: stored=%s, current=%s. Wiping the cache and rebuilding from sources (files are the source of truth — no source data is affected).',
|
|
266
|
+
recorded, version,
|
|
267
|
+
)
|
|
161
268
|
handle.close()
|
|
162
269
|
handle = null
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
270
|
+
|
|
271
|
+
if (dbPath !== ':memory:') {
|
|
272
|
+
// sqlite WAL leaves -wal and -shm sidecar files. Remove
|
|
273
|
+
// them along with the main file so the next open starts
|
|
274
|
+
// from a guaranteed-clean slate.
|
|
275
|
+
for (const suffix of ['', '-wal', '-shm']) {
|
|
276
|
+
try { unlinkSync(dbPath + suffix) } catch { /* file may not exist */ }
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
upgradedFromVersion = recorded
|
|
281
|
+
handle = new Database(dbPath)
|
|
282
|
+
setupConnection()
|
|
167
283
|
}
|
|
168
284
|
handle.prepare('INSERT OR REPLACE INTO mikser_meta (key, value) VALUES (?, ?)')
|
|
169
285
|
.run('schema_version', version)
|
|
170
286
|
|
|
287
|
+
// Build provisioning context. firstRun is true when the file
|
|
288
|
+
// didn't exist before this open OR when the schema mismatch
|
|
289
|
+
// wiped it (and the previous open's stamp is gone) — both shapes
|
|
290
|
+
// present an empty-state database to provisioners.
|
|
291
|
+
provisioningCtx = {
|
|
292
|
+
firstRun: !fileExistedBeforeOpen || upgradedFromVersion !== null,
|
|
293
|
+
upgraded: upgradedFromVersion !== null,
|
|
294
|
+
previousVersion: upgradedFromVersion,
|
|
295
|
+
currentVersion: version,
|
|
296
|
+
handle,
|
|
297
|
+
logger: logger ?? null,
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Fire provisioning callbacks before schema apply, so they can
|
|
301
|
+
// load runtime extensions (sqlite-vec's vec0, etc.) that the
|
|
302
|
+
// schemas about to apply might reference, register custom
|
|
303
|
+
// collations, or do first-run / upgrade work that needs to
|
|
304
|
+
// happen before the engine tables exist.
|
|
305
|
+
for (const provision of provisionersToRun) {
|
|
306
|
+
try {
|
|
307
|
+
provision(provisioningCtx)
|
|
308
|
+
} catch (err) {
|
|
309
|
+
handle.close()
|
|
310
|
+
handle = null
|
|
311
|
+
provisioningCtx = null
|
|
312
|
+
throw new Error(`onProvision callback failed: ${err.message}`)
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
171
316
|
// Apply each subsystem's registered schema script. Idempotent
|
|
172
317
|
// CREATE statements mean replay-safe across opens.
|
|
173
318
|
for (const [name, sqlScript] of schemas) {
|
|
@@ -177,6 +322,7 @@ export function createSqliteDatabase({
|
|
|
177
322
|
} catch (err) {
|
|
178
323
|
handle.close()
|
|
179
324
|
handle = null
|
|
325
|
+
provisioningCtx = null
|
|
180
326
|
throw new Error(`Schema "${name}" failed to apply: ${err.message}`)
|
|
181
327
|
}
|
|
182
328
|
}
|
|
@@ -204,6 +350,12 @@ export function createSqliteDatabase({
|
|
|
204
350
|
open,
|
|
205
351
|
close,
|
|
206
352
|
get isOpen() { return handle !== null },
|
|
353
|
+
// Provisioning context from the most recent open. Plugins'
|
|
354
|
+
// onLoaded handlers can read this without subscribing to
|
|
355
|
+
// onProvision — useful when the work needs the catalog or
|
|
356
|
+
// refs schemas to exist first (which onProvision callbacks
|
|
357
|
+
// can't assume because they fire before schema apply).
|
|
358
|
+
get provisioning() { return provisioningCtx },
|
|
207
359
|
// Pass-through primitives for subsystems. They prepare their
|
|
208
360
|
// own statements at the underlying better-sqlite3 handle.
|
|
209
361
|
prepare(sql) {
|