mikser-io 9.50.2 → 9.57.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 +32 -11
- package/docs/api-reference.md +80 -1
- package/docs/decisions/0009-database-engine-substrate.md +49 -3
- package/docs/diagnostics.md +114 -6
- package/index.js +6 -0
- package/package.json +3 -2
- package/src/changeset.js +229 -158
- package/src/database/durable.js +405 -0
- package/src/database/index.js +58 -256
- package/src/inventory.js +93 -0
- package/src/logger.js +22 -10
- package/src/plugins/api.js +29 -0
- package/src/plugins/render/hbs.js +30 -11
- package/src/report.js +76 -1
- package/src/roles.js +94 -42
- package/src/runtime.js +7 -0
package/CLAUDE.md
CHANGED
|
@@ -137,8 +137,15 @@ brevity.
|
|
|
137
137
|
descriptors in `runtime.options.plugins` are projected to their
|
|
138
138
|
`render-${name}` / `post-${name}` identifier so workers can
|
|
139
139
|
resolve them via dynamic import.
|
|
140
|
-
- `database
|
|
141
|
-
`useDatabase()` (the `mikser_meta` table stamps
|
|
140
|
+
- `database/index.js` — the CACHE only. `createSqliteDatabase()`,
|
|
141
|
+
`registerSchema()`, `useDatabase()` (the `mikser_meta` table stamps
|
|
142
|
+
schema_version). Nothing durable lives here any more.
|
|
143
|
+
- `database/durable.js` — the durable store. `registerMigrations()`,
|
|
144
|
+
`useDurableDatabase()` (a knex instance), `runMigrations()`,
|
|
145
|
+
`closeDurableDatabase()`. `adoptFromCache` carries tables out of a
|
|
146
|
+
pre-9.56 database, driven by the `durable_tables` record the old design
|
|
147
|
+
kept in `mikser_meta` — names alone would move any cache table that
|
|
148
|
+
happened to collide. `ensureIgnored` adds the file to `.gitignore`.
|
|
142
149
|
`sift-to-sql.js` translates sift filters to SQL WHERE clauses
|
|
143
150
|
against `INDEXED_COLUMNS`; un-pushed clauses fall through to
|
|
144
151
|
JS-side sift. `query-context.js` is the AsyncLocalStorage that
|
|
@@ -150,6 +157,12 @@ brevity.
|
|
|
150
157
|
- `server.js` — Express bring-up: CLI flags (`--server`, `--cors`,
|
|
151
158
|
`--no-cors`), trust-proxy, CORS (with extensible header arrays for
|
|
152
159
|
plugins to push onto), late-binding static mount + listen.
|
|
160
|
+
- `report.js` — the `--json` build report. `warnings` is a VIEW of
|
|
161
|
+
`logger.warn`; `faults` is a view of `logger.error` **carrying a
|
|
162
|
+
`code`** — a subsystem declaring it cannot work, deduped by that code,
|
|
163
|
+
never cleared per cycle, and surfaced in `mikser_ping`. The log call is
|
|
164
|
+
the only registration for either; there is no `reportWarning()` or
|
|
165
|
+
`registerFault()` and there must not be.
|
|
153
166
|
- `logger.js` — pino + pino-pretty (inline) + gauge progress + custom
|
|
154
167
|
Writable for progress coordination + `pino.multistream` for
|
|
155
168
|
third-party shipping. Two registration surfaces for transports:
|
|
@@ -434,15 +447,23 @@ Test coverage: `test/unit/source-sweep.test.js`.
|
|
|
434
447
|
- **0008** — MCP-UI rendering + action delivery. Lives in
|
|
435
448
|
`mikser-io-mcp/documentation/decisions/` (not core — moved with
|
|
436
449
|
MCP).
|
|
437
|
-
- **0009** —
|
|
438
|
-
`runtime/mikser.sqlite`
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
`
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
450
|
+
- **0009** — TWO databases, split by whether the data is DERIVED.
|
|
451
|
+
`runtime/mikser.sqlite` is the CACHE: sqlite + better-sqlite3, synchronous
|
|
452
|
+
(render workers need a sync handle for template helpers), holding
|
|
453
|
+
`mikser_entities` / `mikser_refs` / `mikser_snapshots` / `mikser_journal` /
|
|
454
|
+
`mikser_meta`. Deletable at any moment; a wipe is `unlink`.
|
|
455
|
+
`mikser.data.sqlite` at the WORKING-FOLDER ROOT is the DURABLE store: auth
|
|
456
|
+
grants, the change-set log — behind **knex**, async, main-thread only, so it
|
|
457
|
+
can point at Postgres via `config.database.durable`. Built by
|
|
458
|
+
`registerMigrations(owner, [{ name, up }])`, per owner, recorded in
|
|
459
|
+
`mikser_migrations`; `registerSchema(..., { durable: true })` now THROWS.
|
|
460
|
+
Outside `runtime/` because that folder exists to be deleted, and gitignored
|
|
461
|
+
on creation because it holds credentials. `runtime.start()` closes the knex
|
|
462
|
+
pool for a one-shot run or the process never exits.
|
|
463
|
+
Sift→SQL pushdown + LRU for findById on the cache side.
|
|
464
|
+
Journal-on-sqlite (Phase 7) enables `--resume`; auto-persist (Phase 9) means
|
|
465
|
+
plugins mutate the yielded entity and the journal writes back without an
|
|
466
|
+
explicit `updateEntry` call.
|
|
446
467
|
- **0010** — Plugin bundles + factory-call form + inline options.
|
|
447
468
|
Plugins are imported by name and called as factories;
|
|
448
469
|
`plugins: []` carries factory returns, never strings.
|
package/docs/api-reference.md
CHANGED
|
@@ -62,9 +62,17 @@ logger.info('Hello %s', 'world')
|
|
|
62
62
|
logger.debug({ data }, 'Debug message')
|
|
63
63
|
logger.warn('Something might be wrong')
|
|
64
64
|
logger.error('Something failed: %s', err.message)
|
|
65
|
+
logger.error({ code: 'vector-index' }, 'The index is unreadable: %s', err.message) // a fault
|
|
65
66
|
logger.notice('Completion message') // styled green in info mode
|
|
66
67
|
```
|
|
67
68
|
|
|
69
|
+
Two levels are also read by the build report, and the log call is the only way
|
|
70
|
+
to raise either. `logger.warn({ code, ...fields }, msg)` becomes a `warnings`
|
|
71
|
+
entry for that cycle. `logger.error` **with a `code`** becomes a *fault* — a
|
|
72
|
+
subsystem declaring it cannot work — which persists across cycles and shows up
|
|
73
|
+
in `mikser_ping`. Uncoded error lines are ordinary errors and are not captured.
|
|
74
|
+
See [Diagnostics → Faults](./diagnostics.md#faults).
|
|
75
|
+
|
|
68
76
|
---
|
|
69
77
|
|
|
70
78
|
## Runtime Singleton
|
|
@@ -844,6 +852,36 @@ AsyncLocalStorage than the engine's, queries record no edges, and index pages,
|
|
|
844
852
|
sitemaps and feeds silently stop rebuilding. Production consumers resolve both
|
|
845
853
|
from their own tree, so the problem is local to the dev workspace.
|
|
846
854
|
|
|
855
|
+
## Inventory
|
|
856
|
+
|
|
857
|
+
What this mikser is made of.
|
|
858
|
+
|
|
859
|
+
### `inventory({ workingFolder } = {})`
|
|
860
|
+
|
|
861
|
+
Every installed mikser package with its version, purpose and links, and
|
|
862
|
+
`active: true` on the ones actually running.
|
|
863
|
+
|
|
864
|
+
```js
|
|
865
|
+
[
|
|
866
|
+
{ name: 'mikser-io-drive', version: '0.14.0', active: true,
|
|
867
|
+
summary: 'WebDAV for mikser-io. Exposes working-folder directories as …',
|
|
868
|
+
homepage: '…', repository: 'https://github.com/…', npm: 'https://www.npmjs.com/package/…' },
|
|
869
|
+
]
|
|
870
|
+
```
|
|
871
|
+
|
|
872
|
+
**Derived, never declared.** Every plugin already carries a description, a
|
|
873
|
+
homepage and a repository in its own `package.json`, kept current because npm
|
|
874
|
+
publishes from it. A second summary held elsewhere would be one more thing to
|
|
875
|
+
drift — and the first time it drifted it would be describing a plugin that had
|
|
876
|
+
changed underneath it.
|
|
877
|
+
|
|
878
|
+
**Installed and active are different facts.** A package in `node_modules` that
|
|
879
|
+
no config loads explains nothing about a site's behaviour, and an agent told
|
|
880
|
+
otherwise looks for a feature that is not switched on. `active` is reported
|
|
881
|
+
only where it can be established — a plugin that mounts a route names itself
|
|
882
|
+
there, a renderer registers under a known name — and is absent rather than
|
|
883
|
+
`false` where it cannot, because `false` would be a claim.
|
|
884
|
+
|
|
847
885
|
## Roles
|
|
848
886
|
|
|
849
887
|
Enforcement needs only the flat capability list. Explaining a refusal needs the
|
|
@@ -855,7 +893,37 @@ indistinguishable from inside a session.
|
|
|
855
893
|
| `describeAuthority({ capabilities, roles, catalogue, summaries })` | everything a session can say about its own authority |
|
|
856
894
|
| `reachOf(capabilities)` | `{ writable, readOnly }` as collection names |
|
|
857
895
|
| `actingRole(held, catalogue)` | which role is in force |
|
|
858
|
-
| `
|
|
896
|
+
| `rolesIn(catalogue, { acting, summaries })` | every role and its reach, the acting one marked |
|
|
897
|
+
| `registerCapability(capability, meaning)` | declare what a capability protects |
|
|
898
|
+
| `capabilityMeaning(capability)` | what is known about one |
|
|
899
|
+
|
|
900
|
+
### Capabilities describe themselves
|
|
901
|
+
|
|
902
|
+
Core does not know what `drive:layouts` protects, where that folder is, or what
|
|
903
|
+
it is for — the plugin enforcing it does. So the plugin declares it, the same
|
|
904
|
+
way it declares a route or a schema:
|
|
905
|
+
|
|
906
|
+
```js
|
|
907
|
+
registerCapability('drive:documents', {
|
|
908
|
+
plugin: 'drive',
|
|
909
|
+
grants: 'read', // 'read' | 'write' | 'operate'
|
|
910
|
+
resource: {
|
|
911
|
+
kind: 'collection',
|
|
912
|
+
name: 'documents',
|
|
913
|
+
folder: 'documents', // where it is on disk
|
|
914
|
+
summary: 'the words on the pages' // what it is FOR
|
|
915
|
+
},
|
|
916
|
+
})
|
|
917
|
+
```
|
|
918
|
+
|
|
919
|
+
`reachOf` then describes a credential in terms of the SITE rather than of
|
|
920
|
+
verbs — a collection, its folder and its purpose — which is what an agent needs
|
|
921
|
+
to reason about where it is working, not merely whether a call will be refused.
|
|
922
|
+
A capability nothing has declared lands in `also` rather than being dropped: a
|
|
923
|
+
role described only by the part of it that maps to folders is not described.
|
|
924
|
+
|
|
925
|
+
The `drive:<name>[:write]` convention is still recognised for deployments whose
|
|
926
|
+
plugins predate the registry, so they keep their answer.
|
|
859
927
|
| `explainRefusal({ capability, role, target, catalogue, summaries })` | the sentence an agent repeats |
|
|
860
928
|
|
|
861
929
|
`readOnly` is the field that makes a refusal explainable, and it is more useful
|
|
@@ -867,6 +935,11 @@ capabilities cover the others — roles are normally written as widening tiers
|
|
|
867
935
|
and `null` when none dominates, because the acting authority genuinely is the
|
|
868
936
|
union and naming half of it would be a lie.
|
|
869
937
|
|
|
938
|
+
`roles` lists every role, not only the ones the session lacks. One field has to
|
|
939
|
+
serve two readers: someone deciding who to ask, and someone holding the widest
|
|
940
|
+
role trying to see what exists at all — and a "roles you do not have" field
|
|
941
|
+
tells the second one nothing, since for an admin it is always empty.
|
|
942
|
+
|
|
870
943
|
> **Informational, permanently.** Naming the role that could do something is
|
|
871
944
|
> what makes a handoff possible. There is no way to request one and none should
|
|
872
945
|
> be added: a role is a decision about a person, taken by whoever configures the
|
|
@@ -1013,6 +1086,12 @@ Together they turn "write and guess" into one call that says what the edit
|
|
|
1013
1086
|
invalidated. `currentCycle()` and `buildReport()` read the cycle in progress and
|
|
1014
1087
|
the last completed report.
|
|
1015
1088
|
|
|
1089
|
+
`faults()` returns every fault raised since the process started — subsystems
|
|
1090
|
+
that have reported they cannot work — most recently seen first. Each entry
|
|
1091
|
+
carries its `code`, message, any fields from the log call, and `count` /
|
|
1092
|
+
`first` / `last`. Faults are raised by logging at error level with a `code`
|
|
1093
|
+
and nothing else; see [Diagnostics → Faults](./diagnostics.md#faults).
|
|
1094
|
+
|
|
1016
1095
|
## Logging
|
|
1017
1096
|
|
|
1018
1097
|
`addLogTransport({ target, options, level })` adds a pino transport from a
|
|
@@ -56,9 +56,55 @@ the synchronization point.
|
|
|
56
56
|
|
|
57
57
|
## Decision
|
|
58
58
|
|
|
59
|
-
###
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
### Two database files, schemas registered per subsystem
|
|
60
|
+
|
|
61
|
+
Two files, split by whether the data is DERIVED.
|
|
62
|
+
|
|
63
|
+
`runtime/mikser.sqlite` is the cache — catalog, refs, snapshots, journal.
|
|
64
|
+
Per ADR-0002 the files on disk are the source of truth, so this can be
|
|
65
|
+
deleted at any moment and costs a rebuild.
|
|
66
|
+
|
|
67
|
+
`mikser.data.sqlite`, at the working-folder ROOT, is everything that is not
|
|
68
|
+
derived: OAuth clients and refresh tokens, the change-set log. No file can
|
|
69
|
+
reproduce it, so deleting it is data loss.
|
|
70
|
+
|
|
71
|
+
They were one file, with a `durable: true` flag exempting tables from the
|
|
72
|
+
wipe. That required a LIST of which tables were which, and the list was where
|
|
73
|
+
the bugs lived — a config that did not load the owning plugin saw no durable
|
|
74
|
+
tables and unlinked them; `--clear` had to be routed through the wipe rather
|
|
75
|
+
than removing the file; the table names had to be parsed back out of DDL.
|
|
76
|
+
Two files delete the list: a wipe is `unlink` again, and what must survive it
|
|
77
|
+
is not in the file being unlinked. The durable file lives outside `runtime/`
|
|
78
|
+
because that folder exists to be thrown away.
|
|
79
|
+
|
|
80
|
+
The two are DIFFERENT ENGINES, not two files behind one API.
|
|
81
|
+
|
|
82
|
+
The cache is locked to sqlite and a SYNCHRONOUS driver: render workers open
|
|
83
|
+
their own read-only handle so template helpers like `lookupHref` can resolve a
|
|
84
|
+
reference inline, and a promise cannot be awaited inside a Handlebars helper.
|
|
85
|
+
That constraint is what killed the earlier pluggable-driver attempt.
|
|
86
|
+
|
|
87
|
+
None of it reaches the durable store, which is read and written on the main
|
|
88
|
+
thread only, by HTTP handlers and lifecycle hooks that are already async. So it
|
|
89
|
+
sits behind **knex** and can point at Postgres instead of a local file — which
|
|
90
|
+
is what several mikser processes sharing one sign-in and one change-set log
|
|
91
|
+
would need. Splitting the files is what split that constraint apart.
|
|
92
|
+
|
|
93
|
+
Durable tables are built by MIGRATIONS, not by an idempotent CREATE:
|
|
94
|
+
`registerMigrations(owner, [{ name, up }])`, per owner, recorded in
|
|
95
|
+
`mikser_migrations (owner, name, applied_at)`. Per owner because the tables
|
|
96
|
+
belong to independently versioned npm packages, and installing a plugin next
|
|
97
|
+
month must not reorder migrations that ran last year — which every
|
|
98
|
+
directory-based migration runner assumes it can do. `CREATE TABLE IF NOT
|
|
99
|
+
EXISTS` does nothing to a table that already exists, so a schema replayed every
|
|
100
|
+
boot can never add a column, rename one, or backfill a value; that is invisible
|
|
101
|
+
for a cache table and the permanent condition of a durable one.
|
|
102
|
+
|
|
103
|
+
`registerSchema(name, sql, { durable: true })` now THROWS, pointing at
|
|
104
|
+
`registerMigrations`. Accepting it would put the table in the file that gets
|
|
105
|
+
deleted, and the first sign would be an operator asked to sign in again.
|
|
106
|
+
|
|
107
|
+
For the cache: WAL mode + `synchronous=NORMAL` +
|
|
62
108
|
`foreign_keys=ON`. Every persistent subsystem registers its schema via
|
|
63
109
|
`registerSchema(name, sql)` at module-eval time; the database is
|
|
64
110
|
opened once in `onInitialize` and applies all registered schemas
|
package/docs/diagnostics.md
CHANGED
|
@@ -25,6 +25,7 @@ engine source, the entry point is missing and belongs on this page.
|
|
|
25
25
|
| What would break if I changed this file? | [`runtime.manifest`](#runtimemanifest) `affectedBy` |
|
|
26
26
|
| I am an agent reading CLI output, not speaking MCP | [`--tools` / `--tool`](#the-two-agent-workflows) |
|
|
27
27
|
| Did my schema validate anything at all? | [`schemas.names()`](#schemasnames--schemaslookup) |
|
|
28
|
+
| A tool answered emptily — is it broken, or is there nothing to find? | [`faults`](#faults) |
|
|
28
29
|
|
|
29
30
|
## Command line
|
|
30
31
|
|
|
@@ -137,7 +138,7 @@ under this flag, so stdout parses whole.
|
|
|
137
138
|
npx mikser --json | jq '.summary'
|
|
138
139
|
```
|
|
139
140
|
|
|
140
|
-
|
|
141
|
+
The buckets, and the distinction between them is the point:
|
|
141
142
|
|
|
142
143
|
| Bucket | Meaning |
|
|
143
144
|
| --- | --- |
|
|
@@ -146,6 +147,8 @@ Five buckets, and the distinction between them is the point:
|
|
|
146
147
|
| `unchanged` | the render ran and produced bytes identical to what was already on disk |
|
|
147
148
|
| `errors` | the render ran and **threw** — with `id`, `destination`, `error`, `layout` |
|
|
148
149
|
| `gated` | a count — the source was unchanged, so no render was ever scheduled |
|
|
150
|
+
| `warnings` | everything that went through `logger.warn` this cycle, with its `code` |
|
|
151
|
+
| `faults` | subsystems that reported they **cannot work** — see [Faults](#faults) |
|
|
149
152
|
|
|
150
153
|
Each report also carries `cycleId`, `startedAt` and `finishedAt`. Under
|
|
151
154
|
`--watch` two consecutive reports are otherwise indistinguishable, so
|
|
@@ -441,16 +444,31 @@ denial-of-service knob rather than a diagnostic, and it stays on the CLI.
|
|
|
441
444
|
|
|
442
445
|
## The database
|
|
443
446
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
447
|
+
**Two files, and the difference between them is what is safe to delete.**
|
|
448
|
+
|
|
449
|
+
| File | Holds | If you delete it |
|
|
450
|
+
| --- | --- | --- |
|
|
451
|
+
| `runtime/mikser.sqlite` | the derived cache — catalog, refs, snapshots, journal | costs a rebuild, nothing else (ADR-0002: your files are the source of truth) |
|
|
452
|
+
| `mikser.data.sqlite` | everything **not** derived — sign-ins, the change-set log | data loss; no file can reproduce it |
|
|
453
|
+
|
|
454
|
+
The durable one sits at the working-folder root rather than in `runtime/`
|
|
455
|
+
on purpose. That folder exists to be thrown away — it is gitignored, and
|
|
456
|
+
`rm -rf runtime` is a line in real deploy scripts — so anything kept
|
|
457
|
+
inside it is one careless command from gone. `--clear` removes the cache
|
|
458
|
+
and does not touch the other file.
|
|
459
|
+
|
|
460
|
+
`mikser.data.sqlite` holds credentials, so mikser adds it to `.gitignore`
|
|
461
|
+
the first time it creates it. A working folder is usually a git repo, and
|
|
462
|
+
`mikser-io-git` runs `git add -A` over it.
|
|
463
|
+
|
|
464
|
+
Both are readable while a build runs (WAL mode), and it is often faster to
|
|
465
|
+
ask them directly than to add logging.
|
|
448
466
|
|
|
449
467
|
```bash
|
|
450
468
|
sqlite3 runtime/mikser.sqlite
|
|
451
469
|
```
|
|
452
470
|
|
|
453
|
-
|
|
471
|
+
The cache holds five tables:
|
|
454
472
|
|
|
455
473
|
| Table | Holds |
|
|
456
474
|
| --- | --- |
|
|
@@ -460,6 +478,34 @@ Five tables:
|
|
|
460
478
|
| `mikser_snapshots` | what was rendered, where, and from which inputs |
|
|
461
479
|
| `mikser_meta` | schema version and config checksum, for cache invalidation |
|
|
462
480
|
|
|
481
|
+
The durable store is behind **knex**, so it is the one part of mikser that can
|
|
482
|
+
point at another engine — `config.database.durable` takes a path or a
|
|
483
|
+
connection string. The cache cannot: render workers need a synchronous handle
|
|
484
|
+
for template helpers, and nothing about that applies to a store only the main
|
|
485
|
+
thread touches.
|
|
486
|
+
|
|
487
|
+
Durable tables are built by migrations, per owning package:
|
|
488
|
+
|
|
489
|
+
```js
|
|
490
|
+
registerMigrations('grants', [
|
|
491
|
+
{ name: '001-initial', up: (knex) => knex.schema.createTable('my_grants', (t) => {
|
|
492
|
+
t.string('id').primary()
|
|
493
|
+
}) },
|
|
494
|
+
{ name: '002-last-used', up: (knex) => knex.schema.alterTable('my_grants', (t) => {
|
|
495
|
+
t.bigInteger('last_used_at')
|
|
496
|
+
}) },
|
|
497
|
+
])
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
Applied once each and recorded in `mikser_migrations (owner, name, applied_at)`.
|
|
501
|
+
Per owner because the tables belong to independently versioned packages —
|
|
502
|
+
installing a plugin next month must not reorder migrations that ran last year.
|
|
503
|
+
Names are permanent: renaming one makes it run again against a database that
|
|
504
|
+
already has its effect. Append, never edit. There is no `down` — a durable
|
|
505
|
+
store's rollback is a restore from backup.
|
|
506
|
+
|
|
507
|
+
`registerSchema(..., { durable: true })` throws now and says so.
|
|
508
|
+
|
|
463
509
|
### `mikser_journal`
|
|
464
510
|
|
|
465
511
|
The ordered record of what the engine did this cycle — one row per
|
|
@@ -657,6 +703,64 @@ content is clean.
|
|
|
657
703
|
get, stats, config }` — the on-demand render cache, useful for asking
|
|
658
704
|
what has been rendered outside a build.
|
|
659
705
|
|
|
706
|
+
## Faults
|
|
707
|
+
|
|
708
|
+
A **fault** is a subsystem saying it cannot do its job — as opposed to an
|
|
709
|
+
error, which is one render that ran and threw.
|
|
710
|
+
|
|
711
|
+
The difference matters because of how a broken subsystem fails. It does not
|
|
712
|
+
raise; it answers. Search returns `[]`. The change-set log lists nothing.
|
|
713
|
+
`affectedBy` reports that nothing is affected. Every one of those is
|
|
714
|
+
byte-identical to the answer a *working* subsystem gives when there is
|
|
715
|
+
genuinely nothing to report, which is why this class of bug is normally found
|
|
716
|
+
by someone noticing the site is wrong days later.
|
|
717
|
+
|
|
718
|
+
A fault is raised by logging at error level with a `code`:
|
|
719
|
+
|
|
720
|
+
```js
|
|
721
|
+
useLogger().error({ code: 'change-set-log' },
|
|
722
|
+
'The change-set log could not be written (%s). Writes still land on disk, '
|
|
723
|
+
+ 'but they cannot be listed or undone until this is fixed.', err.message)
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
That log call is the *only* registration — the same contract as warnings,
|
|
727
|
+
where `report.warnings` is a view of what went through `logger.warn`. There is
|
|
728
|
+
no `registerFault()`, because a second way to raise one is a second thing to
|
|
729
|
+
forget.
|
|
730
|
+
|
|
731
|
+
The `code` is required, and is what separates a fault from an ordinary error
|
|
732
|
+
line. It is the condition's identity, so forty occurrences are one fault with
|
|
733
|
+
`count: 40` rather than forty faults. An uncoded `logger.error('Render error:
|
|
734
|
+
%s', id)` is an event about one entity and already travels in `errors` with the
|
|
735
|
+
entity attached. `logger.error(err, msg)` does not become a fault either — pino
|
|
736
|
+
puts an errno under `err`, not at the top level, so a raw throw logged that way
|
|
737
|
+
stays an event.
|
|
738
|
+
|
|
739
|
+
Faults are recorded whether or not anyone asked for a report, and are **not**
|
|
740
|
+
cleared between cycles. Both are deliberate: a subsystem that cannot work goes
|
|
741
|
+
on not working after the cycle that noticed it ends, and the reader who most
|
|
742
|
+
needs to know is usually not the operator watching the terminal — they saw the
|
|
743
|
+
log line — but an agent connecting an hour later, for which the log is a
|
|
744
|
+
channel it never reads.
|
|
745
|
+
|
|
746
|
+
Where they surface:
|
|
747
|
+
|
|
748
|
+
| Surface | Shape |
|
|
749
|
+
| --- | --- |
|
|
750
|
+
| `--json` | the `faults` array and `summary.faults` |
|
|
751
|
+
| `mikser_ping` | a `faults` key, **absent** when there are none |
|
|
752
|
+
| `faults()` | the same array, for a plugin or a REPL |
|
|
753
|
+
|
|
754
|
+
Each entry carries the `code`, the message, any fields from the log call, and
|
|
755
|
+
`count` / `first` / `last`. `last` is the field that says whether it is still
|
|
756
|
+
happening: a fault seen once at boot and one firing every cycle read the same
|
|
757
|
+
by presence alone.
|
|
758
|
+
|
|
759
|
+
Nothing clears a fault. The condition is fixed by an operator, and the restart
|
|
760
|
+
that follows is what clears it — an engine deciding on a subsystem's behalf
|
|
761
|
+
that it has recovered would be inventing the one fact this surface exists to
|
|
762
|
+
report honestly.
|
|
763
|
+
|
|
660
764
|
## When mikser is silent
|
|
661
765
|
|
|
662
766
|
Silence is this engine's characteristic failure mode: a declaration that
|
|
@@ -677,6 +781,10 @@ surfaces that turn silence into a statement:
|
|
|
677
781
|
part in cache invalidation, so a config change forces a rebuild; but a
|
|
678
782
|
module the config *imports* does not. If you changed a helper the config
|
|
679
783
|
pulls in, use `--force`.
|
|
784
|
+
- **A tool that answers emptily because it is broken** — check
|
|
785
|
+
[`faults`](#faults) before reading an empty result as a fact about the
|
|
786
|
+
site. This is the one case where the answer and the failure are the same
|
|
787
|
+
bytes.
|
|
680
788
|
- **A plugin that appears to do nothing** — `No plugins loaded` with a
|
|
681
789
|
config present is a warning naming the file. A config that fails to
|
|
682
790
|
load now exits non-zero rather than loading as empty.
|
package/index.js
CHANGED
|
@@ -3,6 +3,7 @@ export * as constants from './src/constants.js'
|
|
|
3
3
|
export * from './src/utils.js'
|
|
4
4
|
export * from './src/auth.js'
|
|
5
5
|
export * from './src/roles.js'
|
|
6
|
+
export * from './src/inventory.js'
|
|
6
7
|
export * from './src/report.js'
|
|
7
8
|
// The diagnostics behind --explain. Exported so a transport — the MCP tool
|
|
8
9
|
// surface, the api plugin's routes — can serve the same structured report the
|
|
@@ -10,6 +11,11 @@ export * from './src/report.js'
|
|
|
10
11
|
export * from './src/explain.js'
|
|
11
12
|
export * from './src/lifecycle.js'
|
|
12
13
|
export * from './src/database/index.js'
|
|
14
|
+
// The durable store — registerMigrations / useDurableDatabase. A separate
|
|
15
|
+
// module from the cache because it is a separate database with none of the
|
|
16
|
+
// cache's constraints: main-thread only, so async, so knex, so portable to
|
|
17
|
+
// another engine.
|
|
18
|
+
export * from './src/database/durable.js'
|
|
13
19
|
export * from './src/journal.js'
|
|
14
20
|
export * from './src/catalog.js'
|
|
15
21
|
export * from './src/search.js'
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mikser-io",
|
|
3
|
-
"version": "9.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "9.57.0",
|
|
4
|
+
"description": "A mixer for content: entities in, configurable render pipelines, outputs of any kind. Static sites are the canonical recipe, not the definition — the same engine renders PDFs, emails and whatever a renderer plugin produces. Files are the source of truth, every lifecycle phase is observable, and the build graph is queryable by an agent.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": "./index.js",
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"handlebars": "^4.7.9",
|
|
45
45
|
"hasha": "^7.0.0",
|
|
46
46
|
"is-url": "^1.2.4",
|
|
47
|
+
"knex": "^3.3.0",
|
|
47
48
|
"line-reader": "^0.4.0",
|
|
48
49
|
"lodash": "^4.18.1",
|
|
49
50
|
"mime-types": "^3.0.2",
|