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 +303 -0
- package/README.md +33 -11
- package/index.js +8 -5
- package/package.json +8 -8
- package/src/catalog.js +592 -55
- package/src/constants.js +24 -2
- package/src/database/index.js +251 -0
- package/src/database/query-context.js +18 -0
- package/src/database/sift-to-sql.js +201 -0
- package/src/engine.js +308 -280
- package/src/journal.js +293 -93
- package/src/logger.js +228 -0
- package/src/manifest.js +542 -0
- package/src/plugins/api.js +176 -480
- package/src/plugins/data.js +72 -27
- package/src/plugins/front-matter.js +1 -3
- package/src/plugins/json.js +6 -8
- package/src/plugins/layouts.js +324 -179
- package/src/plugins/mapper.js +1 -3
- package/src/plugins/observer.js +7 -7
- package/src/plugins/preview.js +76 -474
- package/src/plugins/render/hbs.js +64 -8
- package/src/plugins/render/href.js +3 -6
- package/src/plugins/resources.js +1 -3
- package/src/plugins/yaml.js +1 -3
- package/src/postprocess.js +21 -4
- package/src/refs.js +391 -375
- package/src/render.js +267 -2
- package/src/runtime.js +6 -5
- package/src/server.js +148 -0
- package/src/source.js +202 -13
- package/src/subscriptions.js +157 -0
- package/src/track.js +63 -0
- package/src/utils.js +194 -4
- package/src/api.js +0 -263
- package/src/mcp.js +0 -627
- package/src/tracking.js +0 -75
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
|
|
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
|
-
|
|
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
|
|
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 [
|
|
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 [`
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
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](
|
|
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/
|
|
15
|
+
export * from './src/logger.js'
|
|
12
16
|
export * from './src/engine.js'
|
|
13
|
-
export * from './src/
|
|
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
|
|
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 --
|
|
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": "
|
|
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
|
},
|