mikser-io 8.0.1 → 8.3.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/CLAUDE.md ADDED
@@ -0,0 +1,303 @@
1
+ # CLAUDE.md — mikser-io
2
+
3
+ Read this before proposing changes. Posture, architectural conventions,
4
+ and the landmarks that survive across sessions.
5
+
6
+ ## Posture
7
+
8
+ **Until v10, mikser has no users.** No back-compat. No deprecation
9
+ paths. No migration markdown. No `task: pool` legacy aliasing for
10
+ `task: inline`. Update READMEs and ADRs in place as source-of-truth
11
+ changes; rewrite, don't supersede. The freedom is the point.
12
+
13
+ **Position mikser by what it is, not by speed.** Hugo wins the speed
14
+ race; mikser doesn't compete there. Position: AI-native, lifecycle-
15
+ observable, files-as-source-of-truth, full-cycle introspection. Speed
16
+ claim cap: "fast enough that watch-mode rebuilds feel instant."
17
+ Honest range: 200–800 docs/sec depending on corpus size
18
+ (see `test/perf/`).
19
+
20
+ **Direct critique preferred.** Skip "great question," skip "solid but,"
21
+ skip softening. When something didn't pay off, say so. Match user
22
+ brevity.
23
+
24
+ ## Module map (`src/`)
25
+
26
+ - `runtime.js` — singleton, holds engine state (`runtime.catalog`,
27
+ `runtime.refs`) and `runtime.options`. Lifecycle hook arrays live
28
+ here.
29
+ - `lifecycle.js` — hook registration: `onLoad`/`onLoaded`/`onProcess`/
30
+ `onProcessed`/`onPersist`/`onBeforeRender`/`onRender`/`onAfterRender`/
31
+ `onBeforePostprocess`/`onPostprocess`/`onComplete`/`onFinalize`/
32
+ `onFinalized`/`onCancel`/`onCancelled`. Plus `runtime.create`/
33
+ `.update`/`.delete` (journal helpers).
34
+ - `journal.js` — per-cycle queue, persisted to `mikser_journal` rows in
35
+ `runtime/mikser.sqlite`. Drained at onFinalized; survives crashes so
36
+ `--resume` (`-R`) can pick up unfinalized entries on the next start.
37
+ Same public surface: `addEntry`/`addEntries`/`updateEntry`/
38
+ `useJournal`/`clearJournal`. Inserts `JSON.stringify` the row body for
39
+ snapshot isolation (replaces the prior `structuredClone`). Walks are
40
+ chunked (`CHUNK_SIZE=500`) so peak journal memory stays bounded
41
+ regardless of corpus.
42
+ **Auto-persist:** `useJournal` diffs the yielded entity after each
43
+ iteration and UPDATEs the row if it changed. Plugin authors mutate
44
+ the yielded entity and move on — no explicit `updateEntry({id,entity})`
45
+ required. `updateEntry` is still exported for engine-internal writes
46
+ (output, deps) and as a no-op safety valve for plugins that prefer to
47
+ call it; if the entity hasn't drifted, the auto-persist skips.
48
+ - `catalog.js` — entity persistence in the `mikser_entities` table of
49
+ `runtime/mikser.sqlite`. Indexed columns: id (PK), collection,
50
+ type, format, name, meta_href, meta_layout, meta_lang, meta_cache,
51
+ time, uri. Full entity body in `data` (JSON TEXT). 10k-entry LRU
52
+ cache in front of `findById`. Public ops: `findEntity`,
53
+ `findEntities`, `iterateEntities` (streaming async generator over the
54
+ same query shape, seek-paginated — use it when results may be
55
+ corpus-scale and the caller doesn't need an array), `queryEntities`,
56
+ `readEntity`, `subscribe`, `assertExpand`. Expand internals
57
+ (`expandLimits`, `expandAndProject`, `findRef`) are PRIVATE.
58
+ - `subscriptions.js` — `subscribe()` primitive. Two modes: journal-
59
+ walk dispatch (default) and graph dispatch via
60
+ `runtime.refs.subscribeGraph` (when `expand` is set).
61
+ - `refs.js` — inverse-reference graph (`$`-keyed refs per ADR-0007).
62
+ Persisted as `mikser_refs` rows with FK to `mikser_entities`
63
+ (`ON DELETE CASCADE`). Indexed on `target` and `source`. Exposed
64
+ at `runtime.refs.*`: `inboundFor`, `outboundFor`, `allRefs`,
65
+ `size`, `rename`, `subscribeGraph`, `inverseClosureOf`. Plus
66
+ `refExists` module-level. Prepared statements through the shared
67
+ sqlite handle.
68
+ - `engine.js` — `setup()`, lifecycle wiring, render + postprocess
69
+ dispatchers, manifest tracking. Owns the Piscina worker pools
70
+ (`renderWorkers`, `postprocessWorkers`); both are lazy
71
+ (`minThreads: 0` + `idleTimeout: 30_000`) so INLINE-only workloads
72
+ pay no worker overhead. `workerSafeOptions(runtime.options)`
73
+ strips plugin-surface functions before TASKS.WORKER dispatch so
74
+ Piscina's structured clone doesn't choke.
75
+ - `database/` — `createSqliteDatabase()`, `registerSchema()`,
76
+ `useDatabase()` (the `mikser_meta` table stamps schema_version).
77
+ `sift-to-sql.js` translates sift filters to SQL WHERE clauses
78
+ against `INDEXED_COLUMNS`; un-pushed clauses fall through to
79
+ JS-side sift. `query-context.js` is the AsyncLocalStorage that
80
+ lets catalog queries auto-report into the render-time `track`.
81
+ - `manifest.js` — render snapshots in `mikser_snapshots` table
82
+ (PK `(id, destination)`, `refClosure` as JSON, partial index on
83
+ `parent`). `recordedHashes` aggregates dep hashes via
84
+ `json_each` in C rather than parsing every row in JS.
85
+ - `server.js` — Express bring-up: CLI flags (`--server`, `--cors`,
86
+ `--no-cors`), trust-proxy, CORS (with extensible header arrays for
87
+ plugins to push onto), late-binding static mount + listen.
88
+ - `logger.js` — pino + pino-pretty (inline) + gauge progress + custom
89
+ Writable for progress coordination + `pino.multistream` for
90
+ third-party shipping (`runtime.config.logging.transports`).
91
+ - `utils.js` — shared pure helpers: `mimeForEntity`, `isLoopback`,
92
+ `expandEntity`, `projectMeta`, `useCollection`, `useRenderer` (via
93
+ render.js), `isTextEntity`, `readEntityContent`, `extractRefs`,
94
+ `isRefKey`, `writeEntity`, `matchEntity`, `getFormatInfo`,
95
+ `changeExtension`, `checksum`, `normalize`, `formatErrorContext`,
96
+ `formatLogArgs`, `ExpandError`, `AbortError`.
97
+ - `render.js` / `postprocess.js` — Piscina worker entry points AND the
98
+ default-export functions the INLINE/SERIAL dispatcher calls directly.
99
+ Each receives entity + options + config + state; the WORKER path also
100
+ receives a MessageChannel `port` that forwards pino records back to
101
+ the engine's logger. Each worker opens its own read-only sqlite
102
+ handle on first task (`ensureWorkerDb` in render.js) so template
103
+ 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.
108
+ - `manager.js` — file watching (chokidar) and cron scheduling.
109
+ - `source.js` — `useSource` codifies the folder-of-files pattern.
110
+ - `constants.js` — `OPERATION` (CREATE/UPDATE/DELETE/RENDER/
111
+ POSTPROCESS), `ACTION` (sync action types), `TASKS` (`INLINE`/
112
+ `SERIAL`/`WORKER` — dispatch modes).
113
+
114
+ ## Plugin map (`src/plugins/`)
115
+
116
+ - `documents` — file→entity sync for the documents collection
117
+ - `layouts` — layout matching + sitemap + `inspect()` primitive
118
+ (exposed at `runtime.options.layouts.inspect`)
119
+ - `files` — file→entity sync for the files collection
120
+ - `assets` — asset references and copy
121
+ - `resources` — resource references
122
+ - `front-matter` — YAML frontmatter extraction (HTML/MD files)
123
+ - `yaml` — YAML format support (.yml/.yaml entities)
124
+ - `json` — JSON format support
125
+ - `api` — HTTP catalog access. Pure transport — exposes nothing on
126
+ `runtime.options.*`. Per-query disk cache.
127
+ - `preview` — in-memory render cache + GET /preview/:filename route.
128
+ Exposed at `runtime.options.preview = { store, get, stats, config }`.
129
+ - `data` — JSON snapshots of entities/context/catalog to disk
130
+ - `observer` / `mapper` / `validator` / `commands` / `shares` —
131
+ utility plugins
132
+
133
+ ## Naming conventions
134
+
135
+ - **Engine state** at `runtime.<name>` — `runtime.refs`, `runtime.catalog`.
136
+ - **Plugin surfaces** at `runtime.options.<plugin>` —
137
+ `runtime.options.preview`, `runtime.options.layouts.inspect`.
138
+ - **Engine functions** as module-level exports from `mikser-io`:
139
+ `import { queryEntities, subscribe, useRenderer, useCollection,
140
+ readEntityContent, isTextEntity } from 'mikser-io'`.
141
+ - **Plugin packages**: `mikser-io-<name>` (mikser-io-mcp, mikser-io-vector,
142
+ mikser-io-schemas, etc.).
143
+ - **MCP tools**: `mikser_<verb>` or `mikser_<subsystem>_<verb>`:
144
+ `mikser_query_entities`, `mikser_read_entity`, `mikser_update_entity`,
145
+ `mikser_delete_entity`, `mikser_render`, `mikser_refs_inbound`,
146
+ `mikser_refs_outbound`, `mikser_refs_broken`, `mikser_refs_rename`,
147
+ `mikser_layouts_inspect`, `mikser_preview_render`,
148
+ `mikser_preview_ui`, `mikser_ui_action`, `mikser_ping`.
149
+ **Never `mikser_api_*`** — that prefix is dead.
150
+ - **TASKS constants**: `INLINE` (main-thread async), `SERIAL`
151
+ (p-queue concurrency 1), `WORKER` (Piscina pool). **Not** the old
152
+ `POOL`/`QUEUE`/`WORKER` (which misled readers — `POOL` was actually
153
+ main-thread, not Piscina).
154
+
155
+ ## Code style
156
+
157
+ - **No historical-narrative comments.** Describe what's there now.
158
+ "Used to live in X, moved in 8.2.0" → git log territory.
159
+ - **No separator-line comments** (`// ---- Section -----`).
160
+ - **No `_` prefix on engine-managed paths** (`runtimeFolder/foo`,
161
+ not `runtimeFolder/_foo`).
162
+ - **Engine-set entity fields under `entity.options`**, not
163
+ `_`-prefixed top-level.
164
+ - **No cross-plugin imports.** Plugins compose through lifecycle +
165
+ `runtime.options.*`. Shared pure helpers go in `src/utils.js`.
166
+ Audit: `grep -rEn "from '\./|await import\('\./" src/plugins/*.js`
167
+ should return nothing.
168
+ - **Single source of truth fixes.** For cross-plugin bugs, ask
169
+ "what's the canonical copy?" before symptom-site patching.
170
+ - **README claims are promises.** Every line in mikser-io's README
171
+ is a commitment — true today or actively being made true. Stale
172
+ claims are bugs.
173
+
174
+ ## ADRs (canonical decisions)
175
+
176
+ - **0001-0005** — foundational: content-layer-not-the-app,
177
+ files-as-source-of-truth, plugins-independent-engine-stable,
178
+ compose-via-protocols, engine-infrastructure-runs-before-plugin-hooks
179
+ - **0006** — five-test framework for adding to core: (1) substrate,
180
+ (2) strengthens strategy, (3) god-plugin check, (4) composability,
181
+ (5) release cadence. Conjunctive. Express passes; MCP failed test
182
+ #5 and ships as mikser-io-mcp.
183
+ - **0007** — `$`-prefixed reference declaration + `expand`
184
+ resolution. Implemented in `catalog.js` + `refs.js`. Caps
185
+ configurable under `catalog.expand.{maxDepth,maxPaths,maxResolved}`
186
+ (defaults 5/20/100).
187
+ - **0008** — MCP-UI rendering + action delivery. Lives in
188
+ `mikser-io-mcp/documentation/decisions/` (not core — moved with
189
+ MCP).
190
+ - **0009** — Sqlite is the engine's persistence substrate. Single
191
+ `runtime/mikser.sqlite` holds `mikser_entities` / `mikser_refs` /
192
+ `mikser_snapshots` / `mikser_journal` (+ `mikser_meta` for the
193
+ schema-version stamp). Sift→SQL pushdown + LRU for findById;
194
+ worker-side read-only sqlite for sync template helpers.
195
+ `registerSchema(name, sql)` + `useDatabase()` is the plugin-side
196
+ persistence pattern. Journal-on-sqlite (Phase 7) enables `--resume`;
197
+ auto-persist (Phase 9) means plugins mutate the yielded entity and
198
+ the journal writes back without an explicit `updateEntry` call.
199
+
200
+ ## MCP
201
+
202
+ Lives in `mikser-io-mcp` plugin (separate repo). Activate by listing
203
+ `'mcp'` **first** in your `mikser.config.js` plugins array:
204
+
205
+ ```js
206
+ export default {
207
+ plugins: ['mcp', /* ...other plugins */],
208
+ mcp: {
209
+ path: '/mcp', // optional; default '/mcp'
210
+ // endpoints: { ... } // optional; per-endpoint token + scope
211
+ },
212
+ }
213
+ ```
214
+
215
+ 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`.
218
+
219
+ There is **no `--mcp` CLI flag**. Activation is plugin-presence only.
220
+
221
+ ## Perf
222
+
223
+ - Rig: `npm run test:perf` (generates 10k corpus, runs render-only
224
+ pipeline). Configurable: `node test/perf/generate.js 50000`.
225
+ `SIZE=realistic node test/perf/generate.js 10000` switches to fat
226
+ entities (full SEO meta, hero/gallery image objects, $-refs to
227
+ author/category/related, longer body — ~7KB per catalog entry
228
+ instead of ~3KB). Add `task: worker` to a layout's frontmatter to
229
+ dispatch its renders + postprocess through Piscina.
230
+ - Current honest numbers (Apple Silicon, 4-thread default, INLINE
231
+ dispatch; see ADR-0009 for the substrate the numbers below run on):
232
+ - 14k realistic cold (--clear): 33s, RSS 1.4GB peak
233
+ - 14k realistic warm clean: 2.6s, RSS 156MB peak
234
+ - 14k realistic warm + 1 change: 3.0s, RSS 156MB peak
235
+ - 110k realistic cold (--clear): 5.5 min
236
+ - 110k realistic warm clean: 25s, RSS 3.2GB
237
+ - vs the Map+NDJSON baseline (origin/main 6922b33) at 14k realistic:
238
+ cold ~4× faster, warm ~2× faster, warm RSS ~9× smaller. 110k is a
239
+ workload Map+NDJSON couldn't reach — process OOMed before ADR-0009.
240
+ - Catalog scan cost was the 2024-era objection to sqlite. The
241
+ resolution lives in `src/database/sift-to-sql.js`: indexed sift
242
+ clauses ($eq/$in/$lt/$exists/etc. on collection / type / format /
243
+ name / meta_href / meta_layout / meta_lang / meta_cache / time /
244
+ uri) push down to SQL, so layouts.onLoaded and source.sweep don't
245
+ materialize the table per cycle. `findById` is a 10k-entry LRU in
246
+ front of PK lookup. Without those two, sqlite is strictly slower
247
+ than the old Map (we measured it — see ADR-0009).
248
+ - Piscina is lazy (`minThreads: 0` + `idleTimeout: 30_000`). INLINE
249
+ dispatch is the default for both render and postprocess; layouts
250
+ that opt into TASKS.WORKER (`task: worker` in frontmatter) get a
251
+ thread per first task. At 14k the lazy init dropped peak RSS
252
+ ~130MB on workloads that never use WORKER (which is most).
253
+ - **Profile before optimizing.** `node --cpu-prof app.js
254
+ --working-folder test/perf --clear` produces `.cpuprofile` for
255
+ Chrome DevTools. Intuition has a real miss rate (multiple perf
256
+ hypotheses across this rewrite turned out wrong; the profile
257
+ caught them every time).
258
+ - What actually helps when RSS is too high: trim entity weight
259
+ (don't store source `content` in the catalog if the renderer
260
+ re-reads it; don't keep computed fields you can recompute),
261
+ filter your `data.entities` exports so the catalog isn't
262
+ carrying the rendered shape, or drop `--threads` to 1-2 if the
263
+ build is memory-bound and cold time is acceptable.
264
+
265
+ ## When extending
266
+
267
+ - **New engine capability?** Run through ADR-0006's five tests. Bar
268
+ 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.
272
+ - **New MCP tool?** Add to `mikser-io-mcp/index.js` via
273
+ `mcp.simpleTool(name, description, zodSchema, handler)`. Tool name
274
+ follows `mikser_*` convention.
275
+ - **New lifecycle hook?** Almost certainly no. Existing hooks cover
276
+ all known patterns. If you think you need one, post the use case
277
+ to ADR-0006 review.
278
+
279
+ ## Test suites
280
+
281
+ - `npm run test:unit` — 363 unit tests across plugins + utilities
282
+ - `npm run test:scenarios` — 18 subprocess-spawned end-to-end runs
283
+ (manifest skip, refs replay, watch-mode change/delete). Spawns
284
+ mikser fresh per scenario so module-level catalog/refs/manifest
285
+ state can't leak between tests.
286
+ - `npm run test:smoke` — full lifecycle build of `test/fixture/`
287
+ (with vector + decap + post-mjml + post-pdf if env supports).
288
+ Exercises both INLINE postprocess (PDF) and WORKER postprocess
289
+ (MJML via `task: worker` on `welcome.yml`).
290
+ - `npm run test:perf` — render-pipeline perf rig (corpus generation
291
+ + clean-build timing). `SIZE=realistic` + entity count knobs
292
+ documented in **Perf**.
293
+
294
+ ## Reference
295
+
296
+ - `documentation/architecture.md` — module map (audit before relying
297
+ on specifics; may have drift)
298
+ - `documentation/decisions/` — ADRs
299
+ - `documentation/configuration.md` — config reference
300
+ - `documentation/api-reference.md` — public API
301
+ - `test/perf/` — render-pipeline perf rig
302
+ - Sibling repos: `mikser-io-mcp`, `mikser-io-vector`, `mikser-io-schemas`,
303
+ `mikser-io-sdk-{api,react,svelte,vue,vector}`, `mikser-io-example-blog`
package/README.md CHANGED
@@ -36,7 +36,7 @@ Build mikser into the parts of your application that are content-shaped. Keep th
36
36
 
37
37
  **Incremental builds that scale.** Mikser tracks every entity in a journal. When a file changes, only the affected entities re-process — not the whole site graph. On 10k+ documents this dramatically outpaces tools that rebuild more on every change.
38
38
 
39
- **Concurrent rendering.** Renders fan out across a worker pool that keeps every CPU core hot. Multi-format outputs (HTML, PDF, MJML email, etc.) generate in parallel from the same source.
39
+ **Concurrent rendering.** Renders run async by default and CPU-heavy layouts (MJML compile, image processing, custom transforms) opt into a Piscina worker pool per layout via `task: worker` in frontmatter. Multi-format outputs (HTML, PDF, MJML email, etc.) generate from the same source; the pool is lazy — no workers spawn until they're asked for.
40
40
 
41
41
  **Asset pipelines are whatever Node can do.** Most static frameworks (Astro, Next.js, Hugo) ship image optimization and stop there — video transcoding, AI upscaling, watermarking all need a separate service. Mikser runs user-written modules over binary inputs: ~10 lines around `sharp` resize an image, ~10 around `fluent-ffmpeg` transcode a video, ~30 around the Replicate API upscale with AI. Anything an npm package can do, your pipeline can do — including pulling uploads from a DAM or CDN through the same flow.
42
42
 
@@ -79,7 +79,7 @@ The honest caveat: this advantage is real on **content-shaped work** — adding
79
79
 
80
80
  ## Control mikser from your AI agent
81
81
 
82
- Add `--mcp` to your mikser command and any MCP-speaking client — Claude Desktop, Claude Code, ChatGPT, custom agents — connects to the running engine. From inside a chat, your AI can:
82
+ Install the [`mikser-io-mcp`](https://github.com/almero-digital-marketing/mikser-io-mcp) plugin and any MCP-speaking client — Claude Desktop, Claude Code, ChatGPT, custom agents — connects to the running engine. From inside a chat, your AI can:
83
83
 
84
84
  - read every entity in the catalog
85
85
  - write new content files (markdown, layouts, configuration) — writes land on disk and the next cycle picks them up
@@ -90,8 +90,16 @@ Add `--mcp` to your mikser command and any MCP-speaking client — Claude Deskto
90
90
 
91
91
  Plugins extend the tool surface the same way they mount HTTP routes; install the plugin, the agent gets new verbs. No glue code, no per-project agent wiring.
92
92
 
93
+ ```js
94
+ // mikser.config.js
95
+ export default {
96
+ plugins: ['mcp', /* … */],
97
+ // optional: mcp: { path: '/mcp', endpoints: { … } }
98
+ }
99
+ ```
100
+
93
101
  ```bash
94
- mikser --server --mcp # mounts MCP at /mcp on the same port as --server
102
+ mikser --server # MCP mounts at /mcp on the same port
95
103
  ```
96
104
 
97
105
  What that feels like in practice: *"draft three hero-section variants and show me previews"* — three layouts written, three previews returned inline, one chat turn. *"Why did the build break?"* — the agent reads the rolling log buffer and answers from the same view your terminal sees. *"Update this article's tone and show me the preview"* — the agent edits the file and surfaces the rendered article inline; you click Approve or Reject, the agent acts on your choice. Operator, AI, and any observer dashboard share the same engine because mikser is single-tenant by design.
@@ -109,7 +117,7 @@ When an AI agent edits ten files, the next question is: *did it do what I asked?
109
117
 
110
118
  The shift this enables: AI review stops being *"read every change"* and becomes *"spot-check the agent's confidence."* The agent verifies its own work; the human samples and approves. That's the workflow that lets a content team actually use AI at scale — change the tone across the entire site in a morning, ship it after a coffee.
111
119
 
112
- Full tool reference and twelve worked scenarios in [MCP — talking to mikser from AI](./documentation/mcp.md).
120
+ Full tool reference and twelve worked scenarios in the [`mikser-io-mcp` plugin docs](https://github.com/almero-digital-marketing/mikser-io-mcp#readme).
113
121
 
114
122
  ## Plugins on top of the engine
115
123
 
@@ -132,14 +140,14 @@ The engine is what stays stable — the lifecycle, the catalog, the file-based c
132
140
  |---|---|
133
141
  | `data` | JSON snapshots of entities / context / catalog, written to disk for static serving |
134
142
  | `api` | REST endpoints with sift-backed queries, per-endpoint tokens, optional render, opt-in [per-query disk cache](./documentation/caching.md) for reverse-proxy failover |
135
- | `preview` | In-memory render cache + `GET /preview/:filename` route. Companion to the [`mikser_preview`](./documentation/mcp.md) MCP tool — transient render bytes served at a clickable URL, no filesystem footprint |
143
+ | `preview` | In-memory render cache + `GET /preview/:filename` route. Companion to the `mikser_preview_render` MCP tool (in [`mikser-io-mcp`](https://github.com/almero-digital-marketing/mikser-io-mcp)) — transient render bytes served at a clickable URL, no filesystem footprint |
136
144
 
137
145
  **Integrations:**
138
146
 
139
147
  | Plugin | What it does |
140
148
  |---|---|
141
149
  | [`mikser-io-vector`](https://github.com/almero-digital-marketing/mikser-io-vector) | OpenAI embeddings + semantic search (sqlite-vec or pgvector) |
142
- | [`mikser-io-plugin-schemas`](https://github.com/almero-digital-marketing/mikser-io-plugin-schemas) | Zod-backed entity validation + auto-generated TypeScript declarations for the SDK. Auto-detects `$`-keyed references and warns on broken ones — see [ADR-0007](./documentation/decisions/0007-references-declaration-and-expansion.md) |
150
+ | [`mikser-io-schemas`](https://github.com/almero-digital-marketing/mikser-io-schemas) | Zod-backed entity validation + auto-generated TypeScript declarations for the SDK. Auto-detects `$`-keyed references and warns on broken ones — see [ADR-0007](./documentation/decisions/0007-references-declaration-and-expansion.md) |
143
151
  | [`mikser-io-archive`](https://github.com/almero-digital-marketing/mikser-io-archive) | Persist matching entities to YAML — audit trail, versioned content history, downstream export |
144
152
  | `mapper` | Run config-supplied transforms over matched entities each cycle (in-core, generic transformation layer) |
145
153
  | [`mikser-io-live`](https://github.com/almero-digital-marketing/mikser-io-live) | Lightweight dev server with browser auto-refresh — pair with `--watch` for the classic save→reload loop |
@@ -162,7 +170,7 @@ The `api`, `vector`, and `schemas` plugins are paired with client-side SDKs so a
162
170
  | [`mikser-io-sdk-api`](https://github.com/almero-digital-marketing/mikser-io-sdk-api) | `api` | `entities(name).list / query / urlFor / pages / update / delete / render / live` — Mongo-style filter operators backed by sift, sort, projection, pagination, SSE-driven live subscriptions, and `expand: [...]` to inline-resolve `$`-keyed references in one round-trip (multi-hop chains, `*` array iteration) |
163
171
  | [`mikser-io-sdk-vector`](https://github.com/almero-digital-marketing/mikser-io-sdk-vector) | `vector` | `vector(storeName).findSimilar(text, { limit })` — semantic search hits with the original mapped object attached |
164
172
 
165
- **Framework integrations** — all three wrap `mikser-io-sdk-api` in framework-idiomatic shapes. Same surface: `useDocument` / `useDocuments` live data, multilingual `useHref` / `useAlternates`, asset resolution via `useAsset`, generic on entity type so `mikser-io-plugin-schemas`-emitted types compose:
173
+ **Framework integrations** — all three wrap `mikser-io-sdk-api` in framework-idiomatic shapes. Same surface: `useDocument` / `useDocuments` live data, multilingual `useHref` / `useAlternates`, asset resolution via `useAsset`, generic on entity type so `mikser-io-schemas`-emitted types compose:
166
174
 
167
175
  | Package | Framework | Notes |
168
176
  |---|---|---|
@@ -170,7 +178,7 @@ The `api`, `vector`, and `schemas` plugins are paired with client-side SDKs so a
170
178
  | [`mikser-io-sdk-react`](https://github.com/almero-digital-marketing/mikser-io-sdk-react) | React 18+ / 19+ | Hooks, `<MikserProvider>` Context, React Router v6+ integration via `useMikserRoutes` → `useRoutes()`. |
171
179
  | [`mikser-io-sdk-svelte`](https://github.com/almero-digital-marketing/mikser-io-sdk-svelte) | Svelte 5 (runes) | `$state` / `$effect` reactives, SvelteKit-friendly `generateMikserRoutes` for `entries()` prerender, `useMikserPages` for live nav. |
172
180
 
173
- Each SDK ships TypeScript declarations so client projects get autocomplete on filters, envelopes, and the `MikserError` thrown on non-2xx responses. Pair any of the framework SDKs with the `entities.d.ts` emitted by `mikser-io-plugin-schemas` for typed entity meta per layout. Install only the one(s) a project needs.
181
+ Each SDK ships TypeScript declarations so client projects get autocomplete on filters, envelopes, and the `MikserError` thrown on non-2xx responses. Pair any of the framework SDKs with the `entities.d.ts` emitted by `mikser-io-schemas` for typed entity meta per layout. Install only the one(s) a project needs.
174
182
 
175
183
  ## Quick Start
176
184
 
@@ -201,7 +209,7 @@ The shape mikser fits cleanly:
201
209
 
202
210
  - **Marketing sites with editorial teams** — content authors work in files (via their editor, a Git client, or `mikser-io-decap`), engineers ship features without negotiating with a CMS schema, the site stays portable.
203
211
  - **Multilingual publishing platforms** — the `useHref()` / `useAlternates()` pattern in `sdk-vue` decouples logical references from per-locale URLs. One source tree, many language deployments.
204
- - **Content-heavy product catalogues** — `documents` + `mikser-io-plugin-schemas` + `data` plugin + a Vue frontend = typed product listings with live updates, semantic search via `vector`, and static-CDN-friendly JSON snapshots all at once.
212
+ - **Content-heavy product catalogues** — `documents` + `mikser-io-schemas` + `data` plugin + a Vue frontend = typed product listings with live updates, semantic search via `vector`, and static-CDN-friendly JSON snapshots all at once.
205
213
  - **AI-augmented media pipelines** — `assets` plugin presets call out to Replicate / OpenAI / local models to upscale images, transcribe audio, transcode video. The pipeline is JS code, so anything Node can do is in scope.
206
214
  - **Mixed-output publishing** — the same source document renders to HTML, PDF (via `post-pdf`), MJML email (via `post-mjml`), and JSON snapshots. One catalog, many output formats, all concurrent.
207
215
  - **Headless backends for static frontends** — pair the `api` plugin with `sdk-api` for SSE-driven live frontends; pair the `data` plugin output with any static host for pre-rendered consumption.
@@ -217,11 +225,25 @@ What you get from how this project is built:
217
225
  - **Builds are deterministic; no async middleware layer.** The journal is the only synchronization primitive — no event bus, no IoC container, no orchestrator running plugins in surprising order. The lifecycle is a list of named phases; "what ran when?" has an answer you can read off the source.
218
226
  - **The whole engine is one read.** The [Architecture Overview](./documentation/overview.md) walks the full pipeline top to bottom. Onboarding a new engineer is an afternoon, not a tour through fifteen reference docs.
219
227
 
228
+ ## Mikser among static site generators
229
+
230
+ **The only SSG that's both fast enough for daily use and deep enough for AI agents to drive.**
231
+
232
+ | SSG | Speed | Feature surface | The tradeoff |
233
+ |---|---|---|---|
234
+ | Hugo | Fastest full rebuild | Minimal | Fast but limited; rebuilds everything every run |
235
+ | Eleventy | OK | Broad, no introspection | Flexible but slow at corpus scale |
236
+ | Astro | OK | Modern, framework-coupled | Tied to a frontend framework |
237
+ | Next.js SSG | meh | Full framework | Framework first, content second |
238
+ | **Mikser** | Incremental beats Hugo's full rebuild | Broad, deeply observable, AI-native | None of the speed-vs-features kind |
239
+
240
+ Every other SSG asks you to trade something — raw speed for features (Hugo), features for framework lock-in (Astro, Next), introspection for any of the above (Eleventy). Mikser doesn't make that trade. Hugo still wins a full cold rebuild — and that's worth knowing — but most cycles aren't full cold rebuilds. CI deploys, watch-mode edits, "ran mikser, nothing changed" — these are the daily case, where Mikser's persistent manifest skips what's still current while Hugo rebuilds everything from scratch. The rest of the feature surface (MCP introspection on every lifecycle phase, files-as-source-of-truth, 20-phase composability, multi-format outputs from one corpus) is what no other "fast" SSG carries.
241
+
220
242
  ## Acknowledgments
221
243
 
222
244
  The earliest version of mikser was inspired by [DocPad](https://github.com/docpad/docpad) (Benjamin Lupton, with Michael Duane Mooring and Rob Loach). DocPad's "freeway, not a box" philosophy — files on disk, any pre-processor or template engine, plugin-by-convention extension — shaped how mikser started.
223
245
 
224
- Mikser itself has a previous chapter: the [legacy 7.x line](https://github.com/almero-digital-marketing/mikser) (last release 2022) introduced the real-time SSG model the current engine still carries forward. The redesign dropped MongoDB (the catalog lives in-process now, not in a database), modernized to Node ESM with a structured 20-phase lifecycle, added the live SSE channel that powers the framework SDKs, and replaced cluster-based rendering with an async worker pool. Same intent — content as files, real-time previews, multi-format output at scale — clearer foundations.
246
+ Mikser itself has a previous chapter: the [legacy 7.x line](https://github.com/almero-digital-marketing/mikser) (last release 2022) introduced the real-time SSG model the current engine still carries forward. The redesign dropped MongoDB for a single in-process sqlite database, modernized to Node ESM with a structured 20-phase lifecycle, added the live SSE channel that powers the framework SDKs, and replaced cluster-based rendering with a lazy worker pool. Same intent — content as files, real-time previews, multi-format output at scale — clearer foundations.
225
247
 
226
248
  ## Documentation Index
227
249
 
@@ -235,7 +257,7 @@ Mikser itself has a previous chapter: the [legacy 7.x line](https://github.com/a
235
257
  | [Entities](./documentation/entities.md) | Users & Developers | Entity model, operations, journal, catalog |
236
258
  | [Rendering](./documentation/rendering.md) | Users & Developers | Render pipeline, render plugins, render modes |
237
259
  | [Watch Mode](./documentation/watch-mode.md) | Users | File watching, scheduled tasks, incremental builds |
238
- | [MCP](./documentation/mcp.md) | Users | The `--mcp` server — tool surface, `mikser://` resources, twelve worked AI-driven scenarios |
260
+ | [MCP](https://github.com/almero-digital-marketing/mikser-io-mcp#readme) | Users | The `mikser-io-mcp` plugin — tool surface, `mikser://` resources, twelve worked AI-driven scenarios |
239
261
  | [Caching](./documentation/caching.md) | Users (production) | The `cache: true` disk cache + working nginx config for reverse-proxy failover |
240
262
  | [Architecture](./documentation/architecture.md) | Developers | Module-level reference — what's in each file |
241
263
  | [API Reference](./documentation/api-reference.md) | Developers | Complete public API reference |
package/index.js CHANGED
@@ -1,15 +1,18 @@
1
1
  export { default as runtime } from './src/runtime.js'
2
2
  export * as constants from './src/constants.js'
3
3
  export * from './src/utils.js'
4
- export * from './src/journal.js'
5
4
  export * from './src/lifecycle.js'
5
+ export * from './src/database/index.js'
6
+ export * from './src/journal.js'
6
7
  export * from './src/catalog.js'
7
8
  export * from './src/refs.js'
9
+ export * from './src/manifest.js'
10
+ export * from './src/track.js'
11
+ export * from './src/subscriptions.js'
8
12
  export * from './src/config.js'
9
13
  export * from './src/plugins.js'
10
14
  export * from './src/manager.js'
11
- export * from './src/tracking.js'
15
+ export * from './src/logger.js'
12
16
  export * from './src/engine.js'
13
- export * from './src/api.js'
14
- export * from './src/source.js'
15
- export * from './src/mcp.js'
17
+ export * from './src/render.js'
18
+ export * from './src/source.js'
package/package.json CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "mikser-io",
3
- "version": "8.0.1",
3
+ "version": "8.3.0",
4
4
  "description": "<p align=\"center\"> <img src=\"mikser-lockup-stacked.svg\" alt=\"mikser\" width=\"198\" /> </p>",
5
5
  "main": "index.js",
6
6
  "scripts": {
7
- "debug": "node --no-warnings app.js --debug --server --watch --working-folder test/fixture",
7
+ "debug": "node --no-warnings app.js --server --watch --working-folder test/fixture",
8
8
  "test:unit": "node --test --test-reporter=spec 'test/unit/**/*.test.js'",
9
9
  "test:smoke": "node --no-warnings app.js --working-folder test/fixture",
10
- "test": "npm run test:unit && npm run test:smoke"
10
+ "test:scenarios": "node --test --test-reporter=spec --test-timeout=60000 'test/scenarios/**/*.test.js'",
11
+ "test:perf": "node test/perf/generate.js && node --no-warnings app.js --working-folder test/perf",
12
+ "test": "npm run test:unit && npm run test:scenarios && npm run test:smoke"
11
13
  },
12
14
  "bin": {
13
15
  "mikser": "app.js"
@@ -21,25 +23,24 @@
21
23
  "license": "ISC",
22
24
  "dependencies": {
23
25
  "@budibase/handlebars-helpers": "^0.14.3",
24
- "@modelcontextprotocol/sdk": "^1.29.0",
25
26
  "await-semaphore": "^0.1.3",
26
27
  "axios": "^1.17.0",
28
+ "better-sqlite3": "^12.10.0",
27
29
  "chokidar": "^5.0.0",
28
- "cli-progress": "^3.12.0",
29
30
  "commander": "^15.0.0",
31
+ "cors": "^2.8.5",
30
32
  "dayjs": "^1.11.21",
31
33
  "deepdash": "^5.3.9",
32
34
  "escape-string-regexp": "^5.0.0",
33
35
  "execa": "^9.6.1",
34
36
  "front-matter": "^4.0.2",
37
+ "gauge": "^5.0.2",
35
38
  "globby": "^16.2.0",
36
39
  "handlebars": "^4.7.9",
37
40
  "hasha": "^7.0.0",
38
41
  "is-url": "^1.2.4",
39
- "knex": "^3.2.10",
40
42
  "line-reader": "^0.4.0",
41
43
  "lodash": "^4.18.1",
42
- "lowdb": "^7.0.1",
43
44
  "minimatch": "^10.2.5",
44
45
  "node-cron": "^4.2.1",
45
46
  "p-map": "^7.0.4",
@@ -48,7 +49,6 @@
48
49
  "pino-pretty": "^13.1.3",
49
50
  "piscina": "^5.1.4",
50
51
  "sift": "^17.1.3",
51
- "sqlite3": "^6.0.1",
52
52
  "truncate-stream": "^1.0.2",
53
53
  "yaml": "^2.9.0"
54
54
  },