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 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`, `--verify`, the sqlite tables, and every introspection surface, indexed by the question it answers |
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 --verify against a half-finished
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('--verify') ? { type: 'report', verify: true, json: has('--json') }
45
+ : has('--audit-output') ? { type: 'report', auditOutput: true, json: has('--json') }
46
46
  : { type: 'build', clear: has('--clear') }
47
47
 
48
48
  return {
@@ -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
- | `verify` | boolean | `false` | Verify output folder against manifest; report drift instead of building |
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 |
@@ -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` | `--verify` | boolean | `false` | Verify the output folder against the manifest snapshot — report drift instead of building. |
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. |
@@ -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? | [`--verify`](#--verify) |
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), [`--verify`](#--verify) |
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 --verify` would otherwise pass a
207
- build in which nothing rendered: `--verify` compares the output against the
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
- ### `--verify`
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 --verify || echo "output folder has drifted"
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`, `verify`, `sources`, `which`. The
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 `verify`
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`, `--verify`, `--tools`, `--tool` —
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`, `verify`, `sources` and `build_report` are the engine's own**,
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 mikser_verify` work on a bare engine,
408
- the same as `--verify` — before, the engine's diagnostics needed an agent
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 `--verify` stay as flags rather than becoming `--tool`
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.verify()`. Routing the flag through
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 `--verify` exits
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`, `mikser_verify`,
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 `--verify` reports; pure, no mutations |
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`, `--verify` and `--explain` forward as well, and for a
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
- `--verify` reads a catalogue the instance is halfway through writing and
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.
@@ -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
- --verify Verify output folder against manifest; report
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mikser-io",
3
- "version": "9.76.0",
3
+ "version": "9.78.0",
4
4
  "files": [
5
5
  "app.js",
6
6
  "index.js",
@@ -1,10 +1,10 @@
1
1
  // The engine's own diagnostics, registered as tools.
2
2
  //
3
- // `explain()`, `manifest.verify()` and `buildReport()` are engine functions,
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
- // `--verify` and `--explain` could not simply become `--tool` invocations.
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 `verify` would collide with any other server
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
- 'verify',
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?.verify) return fail('No manifest available — nothing to verify against')
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.verify()),
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 --verify on a separate run, debug
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
- verify = runtime.options.verify,
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 --verify's 1/2, which are
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 (verify) {
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.verify()
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
- 'Verify %s: %d snapshots, %d missing, %d mismatched, %d unverifiable, %d orphaned, %d collisions',
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 mikser_verify` must not
377
- // need an agent surface configured when `--verify` does not.
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('--verify', 'verify output folder against manifest; report drift instead of building', false)
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 --verify do.
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
- // --verify is a standalone read-only mode. Manifest has already
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 --verify. Placed
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 --verify's 1/2, which are
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 --verify cannot disagree with a
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 --verify because this is
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; --verify is where the standing
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 --verify` cannot
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: --verify already uses 2 for output drift and --explain 3
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 --verify's shape: report and exit, no build phases run.
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, verify, json }
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 --verify at ten thousand
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 --verify` walks output folder against recorded snapshots
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
- // --verify is clean; and --explain reports `[current]` and `would be
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 --verify`. Pure: no mutations.
1067
- async verify({ outputFolder } = {}) {
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 --verify reported it missing.
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 --verify ever said so, and renaming an extension is the most
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
  })
@@ -964,10 +964,10 @@ export function api(options = {}) {
964
964
 
965
965
  // ── Diagnostics ────────────────────────────────────────
966
966
  //
967
- // The three questions --explain, --json and --verify answer, for
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 --verify` becomes a request against the
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('/verify', auth, allow('diagnostics'), async (req, res) => {
1009
+ router.get('/audit-output', auth, allow('diagnostics'), async (req, res) => {
1010
1010
  try {
1011
- if (!runtime.manifest?.verify) {
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.verify()
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, --verify compares snapshots against what was rendered rather
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/--verify invocation, so there is no cycle to wait '
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 `verify` would collide with anyone else's — a
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.verify || options.tool || options.tools)
132
+ return Boolean(options.explain || options.auditOutput || options.tool || options.tools)
133
133
  }