mikser-io 9.76.0 → 9.78.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/README.md +1 -1
- package/app.js +2 -2
- package/docs/api-reference.md +1 -1
- package/docs/configuration.md +1 -1
- package/docs/diagnostics.md +38 -19
- package/docs/getting-started.md +1 -1
- package/package.json +1 -1
- package/src/builtin-tools.js +6 -6
- package/src/catalog.js +1 -1
- package/src/engine.js +37 -17
- package/src/explain.js +1 -1
- package/src/instance.js +2 -2
- package/src/manifest.js +61 -6
- package/src/plugins/api.js +5 -5
- package/src/references.js +1 -1
- package/src/report.js +1 -1
- package/src/tools.js +2 -2
package/README.md
CHANGED
|
@@ -315,7 +315,7 @@ Mikser itself has a previous chapter: the [legacy 7.x line](https://github.com/a
|
|
|
315
315
|
| [Entities](./docs/entities.md) | Users & Developers | Entity model, operations, journal, catalog |
|
|
316
316
|
| [Rendering](./docs/rendering.md) | Users & Developers | Render pipeline, render plugins, render modes |
|
|
317
317
|
| [Watch Mode](./docs/watch-mode.md) | Users | File watching, scheduled tasks, incremental builds |
|
|
318
|
-
| [Diagnostics](./docs/diagnostics.md) | Users & Developers | **"Why did it do that?"** — `--explain`, `--json`, `--
|
|
318
|
+
| [Diagnostics](./docs/diagnostics.md) | Users & Developers | **"Why did it do that?"** — `--explain`, `--json`, `--audit-output`, the sqlite tables, and every introspection surface, indexed by the question it answers |
|
|
319
319
|
| [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 |
|
|
320
320
|
| [Caching](./docs/caching.md) | Users (production) | The `cache: true` disk cache + working nginx config for reverse-proxy failover |
|
|
321
321
|
| [Architecture](./docs/architecture.md) | Developers | Module-level reference — what's in each file |
|
package/app.js
CHANGED
|
@@ -35,14 +35,14 @@ function locate(argv) {
|
|
|
35
35
|
// What to ask the instance for. Report-only commands go over the same
|
|
36
36
|
// socket as a build: they read, so running them locally never damaged
|
|
37
37
|
// anything, but a catalogue being written by another process is not a
|
|
38
|
-
// catalogue anyone can answer from — a --
|
|
38
|
+
// catalogue anyone can answer from — a --audit-output against a half-finished
|
|
39
39
|
// cycle reports drift that is not there.
|
|
40
40
|
const tool = value('--tool')
|
|
41
41
|
const explain = value('--explain')
|
|
42
42
|
const request = has('--tools') ? { type: 'report', tools: true, json: has('--json') }
|
|
43
43
|
: tool ? { type: 'report', tool, toolArgs: value('--tool-args'), json: has('--json') }
|
|
44
44
|
: explain ? { type: 'report', explain, json: has('--json') }
|
|
45
|
-
: has('--
|
|
45
|
+
: has('--audit-output') ? { type: 'report', auditOutput: true, json: has('--json') }
|
|
46
46
|
: { type: 'build', clear: has('--clear') }
|
|
47
47
|
|
|
48
48
|
return {
|
package/docs/api-reference.md
CHANGED
|
@@ -41,7 +41,7 @@ await runtime.start()
|
|
|
41
41
|
| `watch` | boolean | `false` | Watch mode |
|
|
42
42
|
| `force` | boolean | `false` | Rebuild everything; disable incremental dispatch |
|
|
43
43
|
| `resume` | boolean | `false` | Continue from journal left by a previous interrupted run; skip the initial filesystem scan |
|
|
44
|
-
| `
|
|
44
|
+
| `auditOutput` | boolean | `false` | Audit the output folder against the snapshots the last build recorded — detects files changed or removed outside mikser, not a render that changed |
|
|
45
45
|
| `debug` | boolean | `false` | Debug logging |
|
|
46
46
|
| `trace` | boolean | `false` | Trace logging |
|
|
47
47
|
| `threads` | number | `4` | Worker thread count |
|
package/docs/configuration.md
CHANGED
|
@@ -53,7 +53,7 @@ These options are part of `runtime.options` and apply to the engine itself.
|
|
|
53
53
|
| `watch` | `-w, --watch` | boolean | `false` | Watch source folders for changes and rebuild incrementally. |
|
|
54
54
|
| `force` | `-f, --force` | boolean | `false` | Rebuild everything; disable incremental dispatch. |
|
|
55
55
|
| `resume` | `-R, --resume` | boolean | `false` | Continue from journal entries left by a previous interrupted run; skip the initial filesystem scan. The journal table survives crashes, so an interrupted cycle can be picked up by re-running with `--resume`. |
|
|
56
|
-
| `verify` | `--
|
|
56
|
+
| `verify` | `--audit-output` | boolean | `false` | Verify the output folder against the manifest snapshot — report drift instead of building. |
|
|
57
57
|
| `debug` | `-d, --debug` | boolean | `false` | Enable debug-level logging. |
|
|
58
58
|
| `trace` | `-t, --trace` | boolean | `false` | Enable trace-level logging (very verbose). |
|
|
59
59
|
| `threads` | — | number | `4` | Worker thread count for the Piscina pools (`renderWorkers`, `postprocessWorkers`). Both pools are lazy (`minThreads: 0` + `idleTimeout: 30_000`) so INLINE-only workloads spin up zero workers. |
|
package/docs/diagnostics.md
CHANGED
|
@@ -14,12 +14,12 @@ engine source, the entry point is missing and belongs on this page.
|
|
|
14
14
|
| --- | --- |
|
|
15
15
|
| Why didn't this page rebuild? | [`--explain`](#--explain-entity) |
|
|
16
16
|
| What did this build actually change? | [`--json`](#--json) |
|
|
17
|
-
| Does the output folder match what mikser thinks it wrote? | [`--
|
|
17
|
+
| Does the output folder match what mikser thinks it wrote? | [`--audit-output`](#--audit-output) |
|
|
18
18
|
| What happened during the last cycle, in order? | [`mikser_journal`](#mikser_journal) |
|
|
19
19
|
| What depends on this entity? | [`runtime.refs`](#runtimerefs) |
|
|
20
20
|
| Which layout claimed this document, and why that one? | [`layouts.inspect()`](#layoutsinspect) |
|
|
21
21
|
| Why does this page's output look stale? | [`runtime.manifest`](#runtimemanifest) |
|
|
22
|
-
| Two files seem to fight over one output | [`--explain`](#--explain-entity), [`--
|
|
22
|
+
| Two files seem to fight over one output | [`--explain`](#--explain-entity), [`--audit-output`](#--audit-output) |
|
|
23
23
|
| Which source file produced this built output? | [`runtime.manifest`](#runtimemanifest), `mikser_which` |
|
|
24
24
|
| Where was this VALUE written — file, field, line? | [`runtime.provenance`](#runtimeprovenance) |
|
|
25
25
|
| What would break if I changed this file? | [`runtime.manifest`](#runtimemanifest) `affectedBy` |
|
|
@@ -203,8 +203,8 @@ make that readable — "broke just now" and "broken for an hour" are
|
|
|
203
203
|
different situations. The marker clears itself on the first success.
|
|
204
204
|
|
|
205
205
|
**A one-shot build with render errors exits `1`.** That is the signal a CI
|
|
206
|
-
gate needs, because `mikser && mikser --
|
|
207
|
-
build in which nothing rendered: `--
|
|
206
|
+
gate needs, because `mikser && mikser --audit-output` would otherwise pass a
|
|
207
|
+
build in which nothing rendered: `--audit-output` compares the output against the
|
|
208
208
|
manifest, both of which still describe the last good render. Watch mode
|
|
209
209
|
keeps running — a failed render there is a state to fix on the next cycle,
|
|
210
210
|
not a reason to tear down the watcher.
|
|
@@ -274,7 +274,7 @@ Warnings carry a stable `code` alongside their prose, so a test can
|
|
|
274
274
|
assert "this build produced no preset-no-match" without grepping a
|
|
275
275
|
sentence someone may later reword.
|
|
276
276
|
|
|
277
|
-
### `--
|
|
277
|
+
### `--audit-output`
|
|
278
278
|
|
|
279
279
|
Walks the output folder against the recorded snapshots and reports drift
|
|
280
280
|
instead of building. Four categories, and the split matters:
|
|
@@ -299,7 +299,7 @@ missing or mismatched, `1` if only warnings, `0` when clean, and `2` when
|
|
|
299
299
|
there is no manifest to check against.
|
|
300
300
|
|
|
301
301
|
```bash
|
|
302
|
-
npx mikser --
|
|
302
|
+
npx mikser --audit-output || echo "output folder has drifted"
|
|
303
303
|
```
|
|
304
304
|
|
|
305
305
|
A destination is resolved against the output folder first and, when that
|
|
@@ -313,6 +313,25 @@ it. That is normal for anything written without a render snapshot (the
|
|
|
313
313
|
`files` and `data` plugins), and a real signal for a page whose layout
|
|
314
314
|
stopped producing it.
|
|
315
315
|
|
|
316
|
+
**What a pass means, and what it does not.** It compares each output file
|
|
317
|
+
against the hash *its own render recorded*. Every render rewrites that
|
|
318
|
+
snapshot, so a render whose output changed records the new bytes and then
|
|
319
|
+
matches them. It is a tampering check — files edited, truncated or removed
|
|
320
|
+
outside mikser — and it cannot, by construction, tell you a render changed.
|
|
321
|
+
|
|
322
|
+
That question is answered by **`output-drift`** instead, reported by the build
|
|
323
|
+
itself rather than by a check afterwards: at the moment a snapshot is
|
|
324
|
+
replaced, the row being overwritten is still readable, so an output that moved
|
|
325
|
+
while its `inputHash` stood still is knowable exactly once — before the
|
|
326
|
+
evidence is gone. Unchanged inputs and changed bytes means the cause was not
|
|
327
|
+
the content: an upgraded renderer, a changed helper, a dependency that shifted
|
|
328
|
+
under the build.
|
|
329
|
+
|
|
330
|
+
Only entities that actually rendered can drift, so an ordinary build reports
|
|
331
|
+
on what moved. Under `--force` everything re-renders with unchanged inputs,
|
|
332
|
+
which makes it a full sweep — **`mikser --force` after a package upgrade is
|
|
333
|
+
the regression check**.
|
|
334
|
+
|
|
316
335
|
### The rest, briefly
|
|
317
336
|
|
|
318
337
|
| Flag | Use |
|
|
@@ -344,16 +363,16 @@ npx mikser --tools
|
|
|
344
363
|
npx mikser --tool which --tool-args '{"destination":"/bg/index.html","text":"Контакти"}'
|
|
345
364
|
```
|
|
346
365
|
|
|
347
|
-
Tool names are **bare** here — `explain`, `
|
|
366
|
+
Tool names are **bare** here — `explain`, `audit_output`, `sources`, `which`. The
|
|
348
367
|
`mikser_` prefix belongs to MCP, where tool names share one flat namespace
|
|
349
|
-
across every server a client has connected to and an unprefixed `
|
|
368
|
+
across every server a client has connected to and an unprefixed `audit_output`
|
|
350
369
|
would collide with anyone else's. The engine has no such problem, and
|
|
351
370
|
`mikser --tool mikser_explain` says mikser twice. The prefix is added at
|
|
352
371
|
the session boundary, so an MCP client sees exactly the names it always
|
|
353
372
|
did. Either form is accepted on the CLI, because an agent reading MCP
|
|
354
373
|
documentation should not have to know which surface stripped what.
|
|
355
374
|
|
|
356
|
-
A report-and-exit run — `--explain`, `--
|
|
375
|
+
A report-and-exit run — `--explain`, `--audit-output`, `--tools`, `--tool` —
|
|
357
376
|
never wipes the cache, even when the config or the schema version has
|
|
358
377
|
moved. Wiping for one destroys the state it was asked to describe and
|
|
359
378
|
then answers from the empty result as though that were the answer;
|
|
@@ -396,7 +415,7 @@ language, which the engine has no business owning. A tool needing real
|
|
|
396
415
|
validation registers through `runtime.options.mcp` with zod, which is
|
|
397
416
|
what every tool in that plugin does.
|
|
398
417
|
|
|
399
|
-
**`explain`, `
|
|
418
|
+
**`explain`, `audit_output`, `sources` and `build_report` are the engine's own**,
|
|
400
419
|
registered in `src/builtin-tools.js` rather than by a plugin. `sources`
|
|
401
420
|
is the reverse lookup — what produced this destination, each source
|
|
402
421
|
tagged with how it got there — reading the `refClosure` through
|
|
@@ -404,16 +423,16 @@ tagged with how it got there — reading the `refClosure` through
|
|
|
404
423
|
`mikser-io-mcp`'s `which`, which reports each occurrence's line and
|
|
405
424
|
whether the string begins it: a declaration usually does and a use
|
|
406
425
|
usually does not, which separates the two in any text format without a
|
|
407
|
-
per-language grammar. That is what makes `--tool
|
|
408
|
-
the same as `--
|
|
426
|
+
per-language grammar. That is what makes `--tool mikser_audit_output` work on a bare engine,
|
|
427
|
+
the same as `--audit-output` — before, the engine's diagnostics needed an agent
|
|
409
428
|
surface configured to be reachable as tools, which is backwards.
|
|
410
429
|
|
|
411
|
-
`--explain` and `--
|
|
430
|
+
`--explain` and `--audit-output` stay as flags rather than becoming `--tool`
|
|
412
431
|
invocations. They are not a second implementation: the flag and the tool
|
|
413
|
-
both call `explain()` and `manifest.
|
|
432
|
+
both call `explain()` and `manifest.auditOutput()`. Routing the flag through
|
|
414
433
|
the tool would add a JSON serialize-and-reparse for nothing. What the
|
|
415
434
|
flags carry that the tool cannot is presentation and exit status —
|
|
416
|
-
`formatExplain`'s aligned columns are for a person, and `--
|
|
435
|
+
`formatExplain`'s aligned columns are for a person, and `--audit-output` exits
|
|
417
436
|
`0` / `1` / `2` for OK / WARN / FAIL, a CI gate contract that `--tool`'s
|
|
418
437
|
`0` / `1` cannot express without lying about one of the three.
|
|
419
438
|
|
|
@@ -423,7 +442,7 @@ Everything above assumes a shell on the machine. Three of these questions
|
|
|
423
442
|
are also answerable from a running server, which is what CI, a dashboard,
|
|
424
443
|
an SDK, or an agent speaking MCP actually has.
|
|
425
444
|
|
|
426
|
-
**MCP** — `mikser_explain`, `mikser_build_report`, `
|
|
445
|
+
**MCP** — `mikser_explain`, `mikser_build_report`, `mikser_audit_output`,
|
|
427
446
|
alongside the existing `mikser_refs_*`, `mikser_layouts_inspect` and the
|
|
428
447
|
`mikser://logs/recent` resource. Four more answer the questions a shell
|
|
429
448
|
would otherwise be needed for: `mikser_search` finds a string across
|
|
@@ -613,7 +632,7 @@ What was rendered and whether it needs redoing.
|
|
|
613
632
|
| `sourcesBehind(snapshot)` | the source entities that fed one render, each with `via` naming HOW it got there (layout, partial, ref, or the recorded query it matched) |
|
|
614
633
|
| `sourcesOf(destination)` | the same across every entity claiming a destination, unioned — what `mikser_which` answers from |
|
|
615
634
|
| `affectedBy(entity)` | which destinations would re-render if this entity changed, each with the same `reason` the build report uses |
|
|
616
|
-
| `verify({outputFolder})` | `{ verdict, missing, mismatched, unverifiable, orphaned, collisions }` — what `--
|
|
635
|
+
| `verify({outputFolder})` | `{ verdict, missing, mismatched, unverifiable, orphaned, collisions }` — what `--audit-output` reports; pure, no mutations |
|
|
617
636
|
| `collisions()` | destinations claimed by more than one entity, with the ids claiming each |
|
|
618
637
|
| `writerOf(destination, outputHash)` | which of several claimants wrote the bytes now on disk, when the hashes can tell them apart |
|
|
619
638
|
| `size()` | snapshot count |
|
|
@@ -764,10 +783,10 @@ There is no opt-out. A flag for running a second engine on a held folder
|
|
|
764
783
|
only ever enabled the accident this surface prevents, and stopping the
|
|
765
784
|
instance serves every case it was reached for.
|
|
766
785
|
|
|
767
|
-
`--tool`, `--tools`, `--
|
|
786
|
+
`--tool`, `--tools`, `--audit-output` and `--explain` forward as well, and for a
|
|
768
787
|
different reason than builds do. They only read, so running one locally never
|
|
769
788
|
damaged anything — it just could not be trusted: on a large site a local
|
|
770
|
-
`--
|
|
789
|
+
`--audit-output` reads a catalogue the instance is halfway through writing and
|
|
771
790
|
reports drift that is a cycle in progress. The instance has the settled state
|
|
772
791
|
and the config that produced it. Exit codes cross the socket unchanged, so
|
|
773
792
|
`--explain` still answers 3 for an entity that is not there.
|
package/docs/getting-started.md
CHANGED
|
@@ -112,7 +112,7 @@ mikser [options]
|
|
|
112
112
|
-f, --force Rebuild everything; disable incremental dispatch
|
|
113
113
|
-R, --resume Continue from journal entries left by a previous
|
|
114
114
|
interrupted run; skip the initial filesystem scan
|
|
115
|
-
--
|
|
115
|
+
--audit-output Audit output against recorded snapshots; report
|
|
116
116
|
drift instead of building
|
|
117
117
|
-d, --debug Show debug log statements
|
|
118
118
|
-t, --trace Show trace log statements
|
package/package.json
CHANGED
package/src/builtin-tools.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// The engine's own diagnostics, registered as tools.
|
|
2
2
|
//
|
|
3
|
-
// `explain()`, `manifest.
|
|
3
|
+
// `explain()`, `manifest.auditOutput()` and `buildReport()` are engine functions,
|
|
4
4
|
// but the tools wrapping them were registered by the mcp plugin — so
|
|
5
5
|
// `--tool mikser_explain` needed an agent surface configured while `--explain`
|
|
6
6
|
// did not. That is backwards for the engine's own diagnostics, and it is why
|
|
7
|
-
// `--
|
|
7
|
+
// `--audit-output` and `--explain` could not simply become `--tool` invocations.
|
|
8
8
|
//
|
|
9
9
|
// Registering them here fixes the direction: they exist on a bare engine, the
|
|
10
10
|
// CLI flags become renderings of them rather than parallel implementations,
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
//
|
|
13
13
|
// Names are BARE — `explain`, not `mikser_explain`. The prefix exists because
|
|
14
14
|
// MCP tool names share one flat namespace across every server a client has
|
|
15
|
-
// connected to, so an unprefixed `
|
|
15
|
+
// connected to, so an unprefixed `audit_output` would collide with any other server
|
|
16
16
|
// offering one. That is the protocol's constraint, not the engine's: on the CLI
|
|
17
17
|
// `mikser --tool mikser_explain` says mikser twice. So the registry holds the
|
|
18
18
|
// bare name and `mikser-io-mcp` adds the prefix when it binds a tool into a
|
|
@@ -70,7 +70,7 @@ export function registerBuiltinTools() {
|
|
|
70
70
|
)
|
|
71
71
|
|
|
72
72
|
registerTool(
|
|
73
|
-
'
|
|
73
|
+
'audit_output',
|
|
74
74
|
{
|
|
75
75
|
description:
|
|
76
76
|
'Check the output folder against what the manifest recorded: files missing, files whose bytes no '
|
|
@@ -81,14 +81,14 @@ export function registerBuiltinTools() {
|
|
|
81
81
|
},
|
|
82
82
|
async () => {
|
|
83
83
|
try {
|
|
84
|
-
if (!runtime.manifest?.
|
|
84
|
+
if (!runtime.manifest?.auditOutput) return fail('No manifest available — nothing to audit against')
|
|
85
85
|
// The verdict comes FROM the manifest, which is the single
|
|
86
86
|
// place that rule lives. Three consumers deriving it from
|
|
87
87
|
// counts is how one of them silently stopped counting
|
|
88
88
|
// collisions.
|
|
89
89
|
return ok({
|
|
90
90
|
snapshots: runtime.manifest.size?.() ?? null,
|
|
91
|
-
...(await runtime.manifest.
|
|
91
|
+
...(await runtime.manifest.auditOutput()),
|
|
92
92
|
})
|
|
93
93
|
} catch (err) {
|
|
94
94
|
return fail(err.message)
|
package/src/catalog.js
CHANGED
|
@@ -356,7 +356,7 @@ onFinalize(async () => {
|
|
|
356
356
|
await applyJournalMutations()
|
|
357
357
|
|
|
358
358
|
// Checkpoint the WAL so the main file size stays representative
|
|
359
|
-
// and external tools (mikser --
|
|
359
|
+
// and external tools (mikser --audit-output on a separate run, debug
|
|
360
360
|
// scripts) see committed state. PASSIVE never blocks readers or
|
|
361
361
|
// writers; it only catches up what it can.
|
|
362
362
|
if (!db?.isOpen) return
|
package/src/engine.js
CHANGED
|
@@ -231,7 +231,7 @@ export async function runReportOnly(request = {}) {
|
|
|
231
231
|
toolArgs = runtime.options.toolArgs,
|
|
232
232
|
json = runtime.options.json,
|
|
233
233
|
explain = runtime.options.explain,
|
|
234
|
-
|
|
234
|
+
auditOutput = runtime.options.auditOutput,
|
|
235
235
|
} = request
|
|
236
236
|
|
|
237
237
|
if (tools) {
|
|
@@ -292,7 +292,7 @@ export async function runReportOnly(request = {}) {
|
|
|
292
292
|
if (explain) {
|
|
293
293
|
// Exit codes:
|
|
294
294
|
// 0 — the entity was found and described
|
|
295
|
-
// 3 — not in the catalog (distinct from --
|
|
295
|
+
// 3 — not in the catalog (distinct from --audit-output's 1/2, which are
|
|
296
296
|
// about output drift; "no such entity" is neither clean nor
|
|
297
297
|
// corrupt, it is a question that could not be answered)
|
|
298
298
|
const { explain: explainEntity, formatExplain } = await import('./explain.js')
|
|
@@ -301,13 +301,13 @@ export async function runReportOnly(request = {}) {
|
|
|
301
301
|
return report.found ? 0 : 3
|
|
302
302
|
}
|
|
303
303
|
|
|
304
|
-
if (
|
|
304
|
+
if (auditOutput) {
|
|
305
305
|
if (!runtime.manifest) {
|
|
306
306
|
logger.error('Verify: no manifest available — nothing to check against')
|
|
307
307
|
return 2
|
|
308
308
|
}
|
|
309
309
|
const { verdict, missing, mismatched, unverifiable, orphaned, collisions } =
|
|
310
|
-
await runtime.manifest.
|
|
310
|
+
await runtime.manifest.auditOutput()
|
|
311
311
|
const total = runtime.manifest.size()
|
|
312
312
|
|
|
313
313
|
for (const e of missing) logger.error('Missing: %s (entity %s)', e.destination, e.id)
|
|
@@ -325,8 +325,28 @@ export async function runReportOnly(request = {}) {
|
|
|
325
325
|
// reads as success at a glance even though the exit code is right.
|
|
326
326
|
const report = verdict === 'FAIL' ? logger.error : verdict === 'WARN' ? logger.warn : logger.notice
|
|
327
327
|
report.call(logger,
|
|
328
|
-
'
|
|
328
|
+
'Audit %s: %d snapshots, %d missing, %d mismatched, %d unverifiable, %d orphaned, %d collisions',
|
|
329
329
|
verdict, total, missing.length, mismatched.length, unverifiable.length, orphaned.length, collisions.length)
|
|
330
|
+
|
|
331
|
+
// Say what a pass means, because the name promises more than the check
|
|
332
|
+
// can deliver.
|
|
333
|
+
//
|
|
334
|
+
// This compares each output file against the hash its OWN render
|
|
335
|
+
// recorded. Every render rewrites that snapshot, so a render whose
|
|
336
|
+
// output changed records the new bytes and then matches them: a
|
|
337
|
+
// rendering regression verifies clean, by construction, and no amount
|
|
338
|
+
// of care in the comparison changes that. It is a tampering check —
|
|
339
|
+
// files edited, truncated or removed outside mikser — not a
|
|
340
|
+
// regression check.
|
|
341
|
+
//
|
|
342
|
+
// Said on a PASS only. On a failure the listed differences are the
|
|
343
|
+
// message, and this would bury them.
|
|
344
|
+
if (verdict === 'OK') {
|
|
345
|
+
logger.info(
|
|
346
|
+
'Audit compares each output against the hash its own render recorded, so it catches files '
|
|
347
|
+
+ 'changed or removed outside mikser — not a render that changed. A render that changed is '
|
|
348
|
+
+ 'reported as it happens, under `output-drift`.')
|
|
349
|
+
}
|
|
330
350
|
return verdict === 'FAIL' ? 2 : verdict === 'WARN' ? 1 : 0
|
|
331
351
|
}
|
|
332
352
|
|
|
@@ -373,8 +393,8 @@ export async function setup(options) {
|
|
|
373
393
|
}
|
|
374
394
|
runtime.state = {}
|
|
375
395
|
// The engine's own diagnostics, as tools. Registered here rather than by a
|
|
376
|
-
// plugin so they exist on a bare engine — `--tool
|
|
377
|
-
// need an agent surface configured when `--
|
|
396
|
+
// plugin so they exist on a bare engine — `--tool mikser_audit_output` must not
|
|
397
|
+
// need an agent surface configured when `--audit-output` does not.
|
|
378
398
|
registerBuiltinTools()
|
|
379
399
|
|
|
380
400
|
// One engine per working folder: publish the control socket when this
|
|
@@ -392,7 +412,7 @@ export async function setup(options) {
|
|
|
392
412
|
.option('-w --watch', 'watch entities for changes', false)
|
|
393
413
|
.option('-f --force', 'rebuild everything; disable incremental dispatch', false)
|
|
394
414
|
.option('-R --resume', 'continue from journal entries left by a previous interrupted run; skip the initial filesystem scan', false)
|
|
395
|
-
.option('--
|
|
415
|
+
.option('--audit-output', 'audit the output folder against the snapshots the last build recorded — detects files changed or removed outside mikser, NOT a render that changed', false)
|
|
396
416
|
.option('--explain <entity>', 'explain one entity — layout, destination, hashes, refClosure, and whether a build would re-render it. Accepts an id, a meta.href, or an id without its extension. Reports instead of building.')
|
|
397
417
|
.option('--json', 'machine-readable output (with --explain, --tool, and for a build\'s render/skip/warning report)', false)
|
|
398
418
|
.option('--tools', 'list the tools this build exposes, then exit', false)
|
|
@@ -541,7 +561,7 @@ export async function setup(options) {
|
|
|
541
561
|
// Listing from there returned exactly one, the tool the mcp substrate
|
|
542
562
|
// creates for itself. By `import` every onLoaded has run and the
|
|
543
563
|
// registry is complete. Nothing is imported, because this exits first,
|
|
544
|
-
// the same way --explain and --
|
|
564
|
+
// the same way --explain and --audit-output do.
|
|
545
565
|
if (runtime.options.tools || runtime.options.tool) {
|
|
546
566
|
const code = await runReportOnly()
|
|
547
567
|
if (code !== null) process.exit(code)
|
|
@@ -552,7 +572,7 @@ export async function setup(options) {
|
|
|
552
572
|
const logger = useLogger()
|
|
553
573
|
logger.debug(runtime.options, 'Mikser options')
|
|
554
574
|
|
|
555
|
-
// --
|
|
575
|
+
// --audit-output is a standalone read-only mode. Manifest has already
|
|
556
576
|
// loaded in its own onLoaded (registered earlier at module
|
|
557
577
|
// import). We diff disk against snapshots, print the report,
|
|
558
578
|
// and exit. No build phases run.
|
|
@@ -563,16 +583,16 @@ export async function setup(options) {
|
|
|
563
583
|
// corruption, but state is messy)
|
|
564
584
|
// 2 — errors (missing or mismatched files — output is
|
|
565
585
|
// actually wrong on disk)
|
|
566
|
-
// --explain: report on one entity and exit, like --
|
|
586
|
+
// --explain: report on one entity and exit, like --audit-output. Placed
|
|
567
587
|
// before it because a caller reaching for both means the explain.
|
|
568
588
|
//
|
|
569
589
|
// Exit codes:
|
|
570
590
|
// 0 — the entity was found and described
|
|
571
|
-
// 3 — not in the catalog (distinct from --
|
|
591
|
+
// 3 — not in the catalog (distinct from --audit-output's 1/2, which are
|
|
572
592
|
// about output drift; "no such entity" is neither clean nor
|
|
573
593
|
// corrupt, it is a question that could not be answered)
|
|
574
594
|
// The same three commands the instance answers over the socket —
|
|
575
|
-
// one implementation, so a forwarded --
|
|
595
|
+
// one implementation, so a forwarded --audit-output cannot disagree with a
|
|
576
596
|
// local one about what it checked.
|
|
577
597
|
const code = await runReportOnly()
|
|
578
598
|
if (code !== null) process.exit(code)
|
|
@@ -969,13 +989,13 @@ export async function setup(options) {
|
|
|
969
989
|
|
|
970
990
|
// Two entities writing one destination in the same cycle: one
|
|
971
991
|
// silently overwrote the other, and every other signal reads clean.
|
|
972
|
-
// Reported per cycle rather than only by --
|
|
992
|
+
// Reported per cycle rather than only by --audit-output because this is
|
|
973
993
|
// the moment it happened, and because a build that discards half its
|
|
974
994
|
// output must not report warnings: 0.
|
|
975
995
|
//
|
|
976
996
|
// Derived from the destinations THIS cycle rendered, so an
|
|
977
997
|
// established collision the operator already knows about does not
|
|
978
|
-
// re-warn on every unrelated build; --
|
|
998
|
+
// re-warn on every unrelated build; --audit-output is where the standing
|
|
979
999
|
// state lives.
|
|
980
1000
|
for (const [destination, ids] of renderedTo) {
|
|
981
1001
|
if (ids.size < 2) continue
|
|
@@ -1249,7 +1269,7 @@ export async function setup(options) {
|
|
|
1249
1269
|
// document is the only thing on it and can be piped to jq.
|
|
1250
1270
|
emitReport()
|
|
1251
1271
|
|
|
1252
|
-
// Non-zero for a one-shot build, so `mikser && mikser --
|
|
1272
|
+
// Non-zero for a one-shot build, so `mikser && mikser --audit-output` cannot
|
|
1253
1273
|
// pass with every page in the site stale. `exitCode` rather than
|
|
1254
1274
|
// process.exit so the report above is flushed and shutdown runs.
|
|
1255
1275
|
//
|
|
@@ -1259,7 +1279,7 @@ export async function setup(options) {
|
|
|
1259
1279
|
// past between two green builds — so the exit code is precisely the
|
|
1260
1280
|
// signal CI needs and the one interactive use must not have.
|
|
1261
1281
|
//
|
|
1262
|
-
// 1, not 2: --
|
|
1282
|
+
// 1, not 2: --audit-output already uses 2 for output drift and --explain 3
|
|
1263
1283
|
// for not-found. "The build ran and some renders threw" is its own
|
|
1264
1284
|
// thing.
|
|
1265
1285
|
if (failed && !runtime.options.watch) process.exitCode = 1
|
package/src/explain.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// read plugin source and hand-query runtime/mikser.sqlite — which works, and
|
|
8
8
|
// needs knowledge a user of the tool should not need.
|
|
9
9
|
//
|
|
10
|
-
// Follows --
|
|
10
|
+
// Follows --audit-output's shape: report and exit, no build phases run.
|
|
11
11
|
import { inputHashOf, inputPartsOf, diffInputParts, lookupKeys, checksum as fileChecksum } from './utils.js'
|
|
12
12
|
import { filterKey } from './track.js'
|
|
13
13
|
import { findEntity, findEntities, findById } from './catalog.js'
|
package/src/instance.js
CHANGED
|
@@ -70,7 +70,7 @@ export function socketPath(workingFolder) {
|
|
|
70
70
|
// ship together, so there is nothing to negotiate and no version to carry.
|
|
71
71
|
//
|
|
72
72
|
// → { type: 'build', config, clear }
|
|
73
|
-
// → { type: 'report', config, tool, tools, toolArgs, explain,
|
|
73
|
+
// → { type: 'report', config, tool, tools, toolArgs, explain, auditOutput, json }
|
|
74
74
|
// ← { type: 'log', chunk } (zero or more, in order)
|
|
75
75
|
// ← { type: 'done', code }
|
|
76
76
|
// ← { type: 'refused', reason, detail }
|
|
@@ -255,7 +255,7 @@ async function configStale() {
|
|
|
255
255
|
// Report-only commands, answered from the live catalogue.
|
|
256
256
|
//
|
|
257
257
|
// These read; they do not write, so running them locally was safe for the
|
|
258
|
-
// FILES. It was not safe for the ANSWER. A local --
|
|
258
|
+
// FILES. It was not safe for the ANSWER. A local --audit-output at ten thousand
|
|
259
259
|
// pages reads a catalogue the instance is in the middle of writing and reports
|
|
260
260
|
// drift that is a half-finished cycle, and a local --tool answers from
|
|
261
261
|
// whatever the last build left rather than from what is true now.
|
package/src/manifest.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// own bytes are identical to last cycle AND none of its tracked
|
|
8
8
|
// dependencies (layout, partials, $-refs, queries) mutated, the
|
|
9
9
|
// render is short-circuited
|
|
10
|
-
// - `mikser --
|
|
10
|
+
// - `mikser --audit-output` walks output folder against recorded snapshots
|
|
11
11
|
//
|
|
12
12
|
// Storage: `mikser_snapshots` table in the engine's sqlite database,
|
|
13
13
|
// alongside `mikser_entities` and `mikser_refs`. Composite primary key
|
|
@@ -106,7 +106,7 @@ registerSchema('mikser_snapshots', SNAPSHOTS_SCHEMA)
|
|
|
106
106
|
// consequences all follow from that one absence: the entity is gated at
|
|
107
107
|
// import next cycle (its own source did not change), so it is never
|
|
108
108
|
// re-dispatched; the manifest still describes the last good render, so
|
|
109
|
-
// --
|
|
109
|
+
// --audit-output is clean; and --explain reports `[current]` and `would be
|
|
110
110
|
// SKIPPED` for a page whose render is throwing — the one tool whose job is
|
|
111
111
|
// "why is this not rebuilding", answering "because there is nothing to do".
|
|
112
112
|
//
|
|
@@ -1063,8 +1063,8 @@ export function createManifest(db) {
|
|
|
1063
1063
|
|
|
1064
1064
|
// Walk the output folder against recorded snapshots, returning
|
|
1065
1065
|
// a diff describing missing / mismatched / orphaned /
|
|
1066
|
-
// unverifiable. Backs `mikser --
|
|
1067
|
-
async
|
|
1066
|
+
// unverifiable. Backs `mikser --audit-output`. Pure: no mutations.
|
|
1067
|
+
async auditOutput({ outputFolder } = {}) {
|
|
1068
1068
|
outputFolder = outputFolder || runtime.options.outputFolder
|
|
1069
1069
|
const missing = []
|
|
1070
1070
|
const mismatched = []
|
|
@@ -1160,6 +1160,7 @@ export function createManifest(db) {
|
|
|
1160
1160
|
_stmtSelectByDestination: stmtSelectByDestination,
|
|
1161
1161
|
_stmtDeleteByDestination: stmtDeleteByDestination,
|
|
1162
1162
|
_stmtDeleteByPK: stmtDeleteByPK,
|
|
1163
|
+
_stmtLookup: stmtLookup,
|
|
1163
1164
|
_stmtUpsert: stmtUpsert,
|
|
1164
1165
|
}
|
|
1165
1166
|
return manifest
|
|
@@ -1284,7 +1285,7 @@ onFinalize(async () => {
|
|
|
1284
1285
|
// `index.yml` — and deleting one of them was taking the shared output
|
|
1285
1286
|
// with it: the file vanished while the survivor's snapshot still said it
|
|
1286
1287
|
// was there, the survivor's own source had not changed so nothing
|
|
1287
|
-
// re-rendered it, and --
|
|
1288
|
+
// re-rendered it, and --audit-output reported it missing.
|
|
1288
1289
|
//
|
|
1289
1290
|
// That made "resolve the collision by deleting the stub" delete the
|
|
1290
1291
|
// homepage, which is the opposite of what the operator asked for and the
|
|
@@ -1304,7 +1305,7 @@ onFinalize(async () => {
|
|
|
1304
1305
|
// yet, and the file goes — with `Rendered: 1`, a green build and no
|
|
1305
1306
|
// warning. A second build does not fix it (nothing changed), nor does
|
|
1306
1307
|
// touch (the input hash is the same); only a real content edit re-renders
|
|
1307
|
-
// it. Only --
|
|
1308
|
+
// it. Only --audit-output ever said so, and renaming an extension is the most
|
|
1308
1309
|
// common action there is during a migration.
|
|
1309
1310
|
const claimedByThisCycle = new Set()
|
|
1310
1311
|
for (const { entity } of renderedEntries) {
|
|
@@ -1366,6 +1367,9 @@ onFinalize(async () => {
|
|
|
1366
1367
|
// `parent` is checked too, mirroring _stmtDeleteByIdOrParent — a deleted
|
|
1367
1368
|
// parent takes its paginated children's snapshots with it.
|
|
1368
1369
|
const recordedSnapshots = []
|
|
1370
|
+
// Entities whose output moved while their inputs did not — collected
|
|
1371
|
+
// inside the transaction, reported by the caller once it commits.
|
|
1372
|
+
const drifted = []
|
|
1369
1373
|
for (const { entity, deps, metaReads, consumedReads } of renderedEntries) {
|
|
1370
1374
|
if (deleted.has(entity.id) || (entity.parent && deleted.has(entity.parent))) continue
|
|
1371
1375
|
const outputHash = await hashOutputFile(entity.destination)
|
|
@@ -1383,8 +1387,59 @@ onFinalize(async () => {
|
|
|
1383
1387
|
m._stmtDeleteByPK.run(id, destination)
|
|
1384
1388
|
}
|
|
1385
1389
|
// 3c. Record successful renders.
|
|
1390
|
+
//
|
|
1391
|
+
// Drift: the same inputs produced different output.
|
|
1392
|
+
//
|
|
1393
|
+
// The row about to be replaced is still readable here, and it carries
|
|
1394
|
+
// both hashes — so an output that moved while its inputHash stood
|
|
1395
|
+
// still is knowable at the moment it happens, for the cost of one
|
|
1396
|
+
// lookup. That is a rendering change nobody asked for: an upgraded
|
|
1397
|
+
// renderer, a changed helper, a dependency that shifted under the
|
|
1398
|
+
// build. Every entity input is in inputHash by construction, so if
|
|
1399
|
+
// that did not move, the cause was not the content.
|
|
1400
|
+
//
|
|
1401
|
+
// Recorded here rather than checked later because later is too late —
|
|
1402
|
+
// the render rewrites its own snapshot, so by the time anything asks,
|
|
1403
|
+
// the evidence has been replaced by the new bytes agreeing with
|
|
1404
|
+
// themselves. That is why --audit-output reported OK on a regression.
|
|
1405
|
+
//
|
|
1406
|
+
// Only entities that actually rendered can drift. Under an ordinary
|
|
1407
|
+
// build the unchanged ones are skipped and never reach here, so this
|
|
1408
|
+
// reports on what moved; under --force everything re-renders with
|
|
1409
|
+
// unchanged inputs, which makes it a full sweep.
|
|
1386
1410
|
for (const snap of recordedSnapshots) {
|
|
1411
|
+
const prior = m._stmtLookup.get(snap.id, snap.destination)
|
|
1412
|
+
if (prior
|
|
1413
|
+
&& prior.inputHash
|
|
1414
|
+
&& prior.inputHash === snap.inputHash
|
|
1415
|
+
&& prior.outputHash
|
|
1416
|
+
&& snap.outputHash
|
|
1417
|
+
&& prior.outputHash !== snap.outputHash) {
|
|
1418
|
+
drifted.push({ id: snap.id, destination: snap.destination })
|
|
1419
|
+
}
|
|
1387
1420
|
m._stmtUpsert.run(snapToRow(snap))
|
|
1388
1421
|
}
|
|
1389
1422
|
})
|
|
1423
|
+
|
|
1424
|
+
// Reported after the transaction, so the log never describes a commit
|
|
1425
|
+
// that did not happen.
|
|
1426
|
+
//
|
|
1427
|
+
// A warning, not an error: identical inputs CAN legitimately produce
|
|
1428
|
+
// different bytes — a template that prints a timestamp, an id drawn at
|
|
1429
|
+
// random. Those are worth knowing about once, and then worth fixing,
|
|
1430
|
+
// because a render that is not a function of its inputs cannot be
|
|
1431
|
+
// cached, compared or trusted to be reproducible.
|
|
1432
|
+
if (drifted.length) {
|
|
1433
|
+
const SHOWN = 10
|
|
1434
|
+
for (const { id, destination } of drifted.slice(0, SHOWN)) {
|
|
1435
|
+
logger.warn({ code: 'output-drift', entity: id, destination },
|
|
1436
|
+
'Output changed with unchanged inputs: %s → %s', id, destination)
|
|
1437
|
+
}
|
|
1438
|
+
logger.warn({ code: 'output-drift-summary', drifted: drifted.length },
|
|
1439
|
+
'%d render(s) produced different bytes from the same inputs%s. Nothing about the content '
|
|
1440
|
+
+ 'moved, so the cause is outside it — an upgraded renderer, a changed helper, a dependency '
|
|
1441
|
+
+ 'that shifted under the build. This is the check --audit-output structurally cannot make: '
|
|
1442
|
+
+ 'a render rewrites its own snapshot, so afterwards the new bytes agree with themselves.',
|
|
1443
|
+
drifted.length, drifted.length > SHOWN ? `, ${SHOWN} shown` : '')
|
|
1444
|
+
}
|
|
1390
1445
|
})
|
package/src/plugins/api.js
CHANGED
|
@@ -964,10 +964,10 @@ export function api(options = {}) {
|
|
|
964
964
|
|
|
965
965
|
// ── Diagnostics ────────────────────────────────────────
|
|
966
966
|
//
|
|
967
|
-
// The three questions --explain, --json and --
|
|
967
|
+
// The three questions --explain, --json and --audit-output answer, for
|
|
968
968
|
// a RUNNING server: CI asking "did that deploy actually rebuild
|
|
969
969
|
// anything", a dashboard, an SDK, anything not speaking MCP.
|
|
970
|
-
// `mikser && mikser --
|
|
970
|
+
// `mikser && mikser --audit-output` becomes a request against the
|
|
971
971
|
// instance that is actually serving.
|
|
972
972
|
//
|
|
973
973
|
// Gated on their own `diagnostics` operation, which is in NEITHER
|
|
@@ -1006,9 +1006,9 @@ export function api(options = {}) {
|
|
|
1006
1006
|
}
|
|
1007
1007
|
})
|
|
1008
1008
|
|
|
1009
|
-
router.get('/
|
|
1009
|
+
router.get('/audit-output', auth, allow('diagnostics'), async (req, res) => {
|
|
1010
1010
|
try {
|
|
1011
|
-
if (!runtime.manifest?.
|
|
1011
|
+
if (!runtime.manifest?.auditOutput) {
|
|
1012
1012
|
return res.status(503).json({ error: 'No manifest available — nothing to verify against' })
|
|
1013
1013
|
}
|
|
1014
1014
|
// 200 either way — the check ran and this is its answer. A
|
|
@@ -1017,7 +1017,7 @@ export function api(options = {}) {
|
|
|
1017
1017
|
// would conflate "drift found" with "request failed". The
|
|
1018
1018
|
// verdict comes from the manifest so this route cannot
|
|
1019
1019
|
// disagree with the CLI about what counts as a failure.
|
|
1020
|
-
const diff = await runtime.manifest.
|
|
1020
|
+
const diff = await runtime.manifest.auditOutput()
|
|
1021
1021
|
return res.json({ snapshots: runtime.manifest.size?.() ?? null, ...diff })
|
|
1022
1022
|
} catch (err) {
|
|
1023
1023
|
logger.error('Api verify error: %s', err.message)
|
package/src/references.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// never asks whether that file exists. A wrong preset name, a wrong extension,
|
|
7
7
|
// or a source whose derivative silently failed to render all produce a
|
|
8
8
|
// well-formed url pointing at nothing, and every existing surface stays green:
|
|
9
|
-
// nothing threw, --
|
|
9
|
+
// nothing threw, --audit-output compares snapshots against what was rendered rather
|
|
10
10
|
// than against what those renders point at, and mikser_refs_broken tracks
|
|
11
11
|
// document-to-document refs, not urls.
|
|
12
12
|
//
|
package/src/report.js
CHANGED
|
@@ -80,7 +80,7 @@ export function whenCycleCompletes(id) {
|
|
|
80
80
|
// and cannot tell whether the work happened.
|
|
81
81
|
if (isReportOnlyRun()) {
|
|
82
82
|
return Promise.reject(new Error(
|
|
83
|
-
'No build runs in a --tool/--explain/--
|
|
83
|
+
'No build runs in a --tool/--explain/--audit-output invocation, so there is no cycle to wait '
|
|
84
84
|
+ 'for. The write itself has landed. Run `mikser` to build it, or use --server with the '
|
|
85
85
|
+ 'MCP endpoint, where a watch cycle exists to await.'))
|
|
86
86
|
}
|
package/src/tools.js
CHANGED
|
@@ -82,7 +82,7 @@ export function toolSchemas() {
|
|
|
82
82
|
export async function invokeTool(name, args = {}) {
|
|
83
83
|
// Names in this registry are BARE. The `mikser_` prefix belongs to MCP,
|
|
84
84
|
// where tool names share one flat namespace across every connected server
|
|
85
|
-
// and an unprefixed `
|
|
85
|
+
// and an unprefixed `audit_output` would collide with anyone else's — a
|
|
86
86
|
// constraint of that protocol, not of the engine. `mikser --tool
|
|
87
87
|
// mikser_explain` says mikser twice.
|
|
88
88
|
//
|
|
@@ -129,5 +129,5 @@ export function toolResultFailed(result) {
|
|
|
129
129
|
// settle and Node drained the loop and left.
|
|
130
130
|
export function isReportOnlyRun() {
|
|
131
131
|
const options = runtime.options ?? {}
|
|
132
|
-
return Boolean(options.explain || options.
|
|
132
|
+
return Boolean(options.explain || options.auditOutput || options.tool || options.tools)
|
|
133
133
|
}
|