mikser-io 8.3.7 → 9.0.0

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 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 CHANGED
@@ -174,59 +174,6 @@ The 9.0 plugin set turns this from architectural potential into a shipped produc
174
174
 
175
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
176
 
177
- ## Streaming render output
178
-
179
- Row-as-entity (`mikser-io-csv`, 1M-row catalogs) breaks an assumption the current render path quietly relies on: that the output of `render(entity)` is a string or `Buffer` you can hand to `writeFile`. A render that serializes 1M rows to a single 500MB CSV string is the worst kind of failure mode — it works in dev with 100 rows, OOMs in production.
180
-
181
- The substrate already streams **input** (`iterateEntities` is a seek-paginated async generator, ADR-0009 phase 5). Output is the other half.
182
-
183
- ### What changes in the render dispatch
184
-
185
- `render.js` currently expects each renderer to return `string | Buffer`. Under 9.0 the contract widens:
186
-
187
- ```js
188
- return string // current path — buffer to disk
189
- return Buffer // current path — buffer to disk
190
- return Readable // new — pipe to a write stream
191
- return AsyncIterable<string | Buffer> // new — drained into a write stream
192
- ```
193
-
194
- The dispatcher inspects the return value and routes:
195
-
196
- - string/Buffer → `writeFile(destination, result)` (unchanged hot path; everything that exists today keeps working)
197
- - Readable or async iterable → `await pipeline(result, createWriteStream(destination))`
198
-
199
- Postprocess plugins (`render-file` for inlining, `mjml`, `pdf`, etc.) opt out of the streaming path automatically when they need a buffered input — they declare `bufferedInput: true` and the dispatcher collects the stream first. Most postprocess plugins already need the full document (HTML rewrite, MJML compile), so this is the common case; streaming is the optimization for renderers whose output is naturally append-only.
200
-
201
- ### What this enables
202
-
203
- | renderer | streaming wins because |
204
- |---|---|
205
- | `render-csv` | row count is corpus-scale; output is line-by-line append; no postprocess for CSV |
206
- | `render-rss` / `render-atom` | item list is corpus-scale; XML is append-only per item |
207
- | `render-ical` | event list is corpus-scale |
208
- | `data` (JSON catalog export) | the corpus IS the output; current code buffers the whole catalog |
209
- | future `render-ndjson` | naturally one entity per line |
210
- | `render-hbs` / `render-eta` / `render-markdown` | NOT a win — template engines produce one document, postprocess (front-matter inlining, link rewriting) needs the full string anyway |
211
-
212
- ### Memory budget the streaming path holds
213
-
214
- At 1M rows, `render-csv` running in streaming mode should never hold more than one row's worth of formatted output in memory. The peak isn't the row count, it's the slowest downstream consumer (disk write speed). On a SATA SSD, a 500MB CSV writes at ~400MB/sec — under 2 seconds wall-clock for the full file, with the JS heap never crossing 50MB for the render itself.
215
-
216
- The buffered path on the same workload: 500MB CSV string in V8 = ~1GB after the UTF-16 doubling, then a second 500MB allocation for the `Buffer` conversion, then GC pressure. Streaming isn't a 10% optimization here; it's the difference between working and OOM.
217
-
218
- ### What this doesn't change
219
-
220
- - HTML rendering paths (the dominant workload) keep returning strings. No measurable cost.
221
- - The journal's auto-persist diff still works — it diffs the entity object, not the output.
222
- - Manifest checksumming streams over the output too (`createHash` accepts piped data) — no extra read.
223
-
224
- ### Scope for 9.0
225
-
226
- Land the dispatcher change in the engine alongside `mikser-io-csv` + `mikser-io-render-csv`. The CSV plugins are the first consumers; they validate the contract. `data` plugin migrates to streaming as a second step (separate release). All existing renderers stay on the string path; the widening is additive.
227
-
228
- Add to the work list: dispatcher widening + `render-csv` shipping in streaming mode as the proof.
229
-
230
177
  ## Schemas plugin pulls real weight here
231
178
 
232
179
  `mikser-io-schemas` has been sitting idle. Under the 9.0 shape it becomes load-bearing:
@@ -451,6 +398,7 @@ The trade isn't worth it. `--install` as the explicit, opt-in command is the rig
451
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.
452
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.
453
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.
454
402
 
455
403
  ## Open questions to settle before shipping
456
404
 
@@ -469,19 +417,12 @@ The trade isn't worth it. `--install` as the explicit, opt-in command is the rig
469
417
 
470
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.
471
419
 
472
- 7. **Postprocess opt-in vs opt-out for buffered input.** The streaming render dispatcher needs to know whether a postprocess plugin can consume a stream. Two shapes:
473
- - (a) postprocess plugins declare `bufferedInput: true` on registration; default is "can stream"
474
- - (b) postprocess plugins declare `streamInput: true`; default is "needs buffer" (current behavior)
475
- Working proposal: **(b)** — default to buffered so existing plugins (mjml, pdf, front-matter-rewrite) keep working without code changes. Plugins opt into streaming only when the format genuinely supports it (CSV row-rewrite, NDJSON line-transform). One-line migration cost beats silently breaking the plugin set.
476
-
477
- 8. **Worker path for streaming renders.** TASKS.WORKER serializes return values through Piscina's structured clone — a Readable stream doesn't survive that boundary. Working proposal: **streaming renders are INLINE/SERIAL only for 9.0**. A renderer that declares streaming output is force-dispatched on the main thread regardless of its `task:` frontmatter. Documented as a limitation, not a bug. Revisit when Piscina ships transferable-stream support or we move to MessagePort-based piping (post-9.0).
478
-
479
420
 
480
421
  ## Concretely, what work this is
481
422
 
482
423
  Roughly in dependency order:
483
424
 
484
- 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. **Streaming render output**: widen the render dispatcher in `src/render.js` to accept `Readable` and `AsyncIterable` returns; pipe to `createWriteStream` instead of `writeFile`. Postprocess plugins declare `bufferedInput: true` to opt back into the buffered path. String/Buffer returns unchanged.
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.
485
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.
486
427
  3. **mikser-io-vector 2.0** — drop OpenAI HTTP client. Depend on substrate. Validate dimension against the registered embed pipeline.
487
428
  4. **mikser-io-layouts** — new repo. Extract `src/plugins/layouts.js` verbatim. CI + tests.
@@ -489,7 +430,7 @@ Roughly in dependency order:
489
430
  6. **mikser-io-ocr** — new repo. Consumer plugin for the ocr pipeline.
490
431
  7. **mikser-io-extract** — new repo. Consumer plugin for the extract pipeline. Wire into mikser-io-schemas's `lookup()` surface.
491
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.
492
- 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. **Ships in streaming mode** — returns an async iterable from `render()`, validating the widened dispatcher contract end-to-end. The "1M-row CSV builds in <2s without OOM" smoke test lives in this repo.
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.
493
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.
494
435
  11. **README rewrite** — recipe-driven Getting Started. Multiple compositions shown.
495
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.
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
@@ -71,7 +81,10 @@ brevity.
71
81
  (`minThreads: 0` + `idleTimeout: 30_000`) so INLINE-only workloads
72
82
  pay no worker overhead. `workerSafeOptions(runtime.options)`
73
83
  strips plugin-surface functions before TASKS.WORKER dispatch so
74
- Piscina's structured clone doesn't choke.
84
+ Piscina's structured clone doesn't choke. Renderer / postprocessor
85
+ descriptors in `runtime.options.plugins` are projected to their
86
+ `render-${name}` / `post-${name}` identifier so workers can
87
+ resolve them via dynamic import.
75
88
  - `database/` — `createSqliteDatabase()`, `registerSchema()`,
76
89
  `useDatabase()` (the `mikser_meta` table stamps schema_version).
77
90
  `sift-to-sql.js` translates sift filters to SQL WHERE clauses
@@ -101,10 +114,27 @@ brevity.
101
114
  the engine's logger. Each worker opens its own read-only sqlite
102
115
  handle on first task (`ensureWorkerDb` in render.js) so template
103
116
  helpers like `runtime.lookupHref` stay sync. Never touch the journal
104
- directly.
105
- - `config.js` loads `mikser.config.js` at `onLoad`.
106
- - `plugins.js` loads user plugins at `onLoad`. Plugin factories
107
- receive the full `core` exports as their first argument.
117
+ directly. Plugin loading: main-thread INLINE dispatch reads
118
+ `runtime.renderers` / `runtime.postprocessors` first (populated by
119
+ plugins.js from descriptor returns); workers see empty registries
120
+ in their separate-process runtime and fall through to dynamic-import
121
+ by `mikser-io-${name}` package name. Per-plugin options flow through
122
+ `descriptor.options` and arrive as the `config` arg to
123
+ `load`/`render`/`setup`/`postprocess`/`teardown`.
124
+ - `config.js` — loads `mikser.config.js` at `onLoad` into
125
+ `runtime.config`. v9 holds only engine-level keys (`server`,
126
+ `logging`, `catalog` if tuned) plus the `plugins` array — all
127
+ plugin options moved to the factory call site (ADR-0010).
128
+ - `plugins.js` — dispatches v9 plugin entries at `onLoad`. Each
129
+ entry in `plugins: []` is a factory call return; the dispatcher
130
+ duck-types on shape:
131
+ - function → lifecycle plugin; called with `core` so it can
132
+ register hooks.
133
+ - `{ name, options, load?, render? }` → renderer descriptor;
134
+ stored in `runtime.renderers`.
135
+ - `{ name, options, postprocess, output?, setup?, teardown? }` →
136
+ postprocessor descriptor; stored in `runtime.postprocessors`.
137
+ Strings produce a v9 migration error pointing at the new shape.
108
138
  - `manager.js` — file watching (chokidar) and cron scheduling.
109
139
  - `source.js` — `useSource` codifies the folder-of-files pattern.
110
140
  - `constants.js` — `OPERATION` (CREATE/UPDATE/DELETE/RENDER/
@@ -139,7 +169,10 @@ brevity.
139
169
  `import { queryEntities, subscribe, useRenderer, useCollection,
140
170
  readEntityContent, isTextEntity } from 'mikser-io'`.
141
171
  - **Plugin packages**: `mikser-io-<name>` (mikser-io-mcp, mikser-io-vector,
142
- mikser-io-schemas, etc.).
172
+ mikser-io-schemas, etc.). Each exports a v9 named factory in
173
+ camelCase: `import { vector } from 'mikser-io-vector'`,
174
+ `import { renderHbs } from 'mikser-io'`. Consumer uses
175
+ `plugins: [vector({...})]` — never the bare string.
143
176
  - **MCP tools**: `mikser_<verb>` or `mikser_<subsystem>_<verb>`:
144
177
  `mikser_query_entities`, `mikser_read_entity`, `mikser_update_entity`,
145
178
  `mikser_delete_entity`, `mikser_render`, `mikser_refs_inbound`,
@@ -196,25 +229,39 @@ brevity.
196
229
  persistence pattern. Journal-on-sqlite (Phase 7) enables `--resume`;
197
230
  auto-persist (Phase 9) means plugins mutate the yielded entity and
198
231
  the journal writes back without an explicit `updateEntry` call.
232
+ - **0010** — Plugin bundles + factory-call form + inline options.
233
+ Plugins are imported by name and called as factories;
234
+ `plugins: []` carries factory returns, never strings.
235
+ Lifecycle plugins are `(options) => (core) => void`; renderers
236
+ return `{name, options, load?, render?}`; postprocessors return
237
+ `{name, options, output?, setup?, postprocess, teardown?}`. Per-
238
+ plugin config moved entirely off `runtime.config.<plugin>`; it
239
+ arrives as factory args, gets stashed on the descriptor, and is
240
+ passed as `config` to `load`/`render`/`setup`/`postprocess`.
199
241
 
200
242
  ## MCP
201
243
 
202
- Lives in `mikser-io-mcp` plugin (separate repo). Activate by listing
203
- `'mcp'` **first** in your `mikser.config.js` plugins array:
244
+ Lives in `mikser-io-mcp` plugin (separate repo). Activate by calling
245
+ `mcp()` **first** in your `mikser.config.js` plugins array:
204
246
 
205
247
  ```js
248
+ import { mcp } from 'mikser-io-mcp'
249
+
206
250
  export default {
207
- plugins: ['mcp', /* ...other plugins */],
208
- mcp: {
209
- path: '/mcp', // optional; default '/mcp'
210
- // endpoints: { ... } // optional; per-endpoint token + scope
211
- },
251
+ plugins: [
252
+ mcp({
253
+ // path: '/mcp', // default '/mcp'
254
+ // endpoints: { ... }, // per-endpoint token + scope
255
+ }),
256
+ /* ...other plugins */
257
+ ],
212
258
  }
213
259
  ```
214
260
 
215
261
  Must be first because its factory creates `runtime.options.mcp`
216
- synchronously, and other plugins gate their MCP tool registration on
217
- `if (runtime.options.mcp)` in their own `onLoaded`.
262
+ synchronously when its closure runs, and other plugins gate their
263
+ MCP tool registration on `if (runtime.options.mcp)` in their own
264
+ `onLoaded`.
218
265
 
219
266
  There is **no `--mcp` CLI flag**. Activation is plugin-presence only.
220
267
 
@@ -266,9 +313,13 @@ There is **no `--mcp` CLI flag**. Activation is plugin-presence only.
266
313
 
267
314
  - **New engine capability?** Run through ADR-0006's five tests. Bar
268
315
  is high. Express is the only earned addition.
269
- - **New plugin?** Own repo, named `mikser-io-<name>`. Composes
270
- against `runtime.options.app` / `runtime.options.mcp` / lifecycle
271
- hooks. Never imports another plugin's source.
316
+ - **New plugin?** Own repo, named `mikser-io-<name>`. Exports a
317
+ named v9 factory (e.g. `export function vector(options = {}) {
318
+ return (core) => { ... } }`). Composes against
319
+ `runtime.options.app` / `runtime.options.mcp` / lifecycle hooks.
320
+ Never imports another plugin's source — and the engine never
321
+ reads `runtime.config.<plugin>` for plugin options; everything
322
+ flows through the factory arg (ADR-0010).
272
323
  - **New MCP tool?** Add to `mikser-io-mcp/index.js` via
273
324
  `mcp.simpleTool(name, description, zodSchema, handler)`. Tool name
274
325
  follows `mikser_*` convention.
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/README.md CHANGED
@@ -92,9 +92,11 @@ Plugins extend the tool surface the same way they mount HTTP routes; install the
92
92
 
93
93
  ```js
94
94
  // mikser.config.js
95
+ import { mcp } from 'mikser-io-mcp'
96
+
95
97
  export default {
96
- plugins: ['mcp', /* … */],
97
- // optional: mcp: { path: '/mcp', endpoints: { … } }
98
+ plugins: [mcp(), /* … */],
99
+ // mcp({ path: '/mcp', endpoints: { … } }) when options are needed
98
100
  }
99
101
  ```
100
102
 
package/index.js CHANGED
@@ -15,4 +15,35 @@ export * from './src/manager.js'
15
15
  export * from './src/logger.js'
16
16
  export * from './src/engine.js'
17
17
  export * from './src/render.js'
18
- export * from './src/source.js'
18
+ export * from './src/source.js'
19
+
20
+ // Built-in plugin factories. Each takes options and returns the
21
+ // (core) => void closure the engine calls at onLoad time. See ADR-0010
22
+ // for the v9 plugin shape.
23
+ export { api } from './src/plugins/api.js'
24
+ export { assets } from './src/plugins/assets.js'
25
+ export { commands } from './src/plugins/commands.js'
26
+ export { data } from './src/plugins/data.js'
27
+ export { documents } from './src/plugins/documents.js'
28
+ export { files } from './src/plugins/files.js'
29
+ export { frontMatter } from './src/plugins/front-matter.js'
30
+ export { json } from './src/plugins/json.js'
31
+ export { layouts } from './src/plugins/layouts.js'
32
+ export { mapper } from './src/plugins/mapper.js'
33
+ export { observer } from './src/plugins/observer.js'
34
+ export { preview } from './src/plugins/preview.js'
35
+ export { resources } from './src/plugins/resources.js'
36
+ export { shares } from './src/plugins/shares.js'
37
+ export { validator } from './src/plugins/validator.js'
38
+ export { yaml } from './src/plugins/yaml.js'
39
+
40
+ // Built-in renderers. v9 factory shape returns the descriptor that the
41
+ // loader stores in `runtime.renderers`; the same module also still
42
+ // exports `load`/`render` at the top level so Piscina worker dispatch
43
+ // can resolve via dynamic import. ADR-0010.
44
+ export { renderAsset } from './src/plugins/render/asset.js'
45
+ export { renderFile } from './src/plugins/render/file.js'
46
+ export { renderHbs } from './src/plugins/render/hbs.js'
47
+ export { renderHref } from './src/plugins/render/href.js'
48
+ export { renderPreset } from './src/plugins/render/preset.js'
49
+ export { renderResource } from './src/plugins/render/resource.js'