mikser-io 9.21.0 → 9.22.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/docs/diagnostics.md +25 -6
- package/index.js +5 -5
- package/package.json +1 -1
- package/src/catalog.js +4 -2
- package/src/config.js +5 -7
- package/src/explain.js +80 -3
- package/src/manifest.js +5 -4
- package/src/plugins/api.js +3 -2
- package/src/plugins/render/preset.js +2 -2
- package/src/postprocess.js +6 -6
package/docs/diagnostics.md
CHANGED
|
@@ -59,20 +59,39 @@ snapshot: if you have edited a file and not yet built, the verdict is
|
|
|
59
59
|
`source differs from the catalog` — the edit has not been imported yet, so
|
|
60
60
|
there is nothing to attribute. Build, then ask.
|
|
61
61
|
|
|
62
|
-
Each `refClosure` edge shows
|
|
63
|
-
|
|
62
|
+
Each `refClosure` edge shows what was asked for and what answers to it,
|
|
63
|
+
and flags the ones nothing answers to:
|
|
64
64
|
|
|
65
65
|
```
|
|
66
|
-
refClosure
|
|
66
|
+
refClosure 6 edges
|
|
67
67
|
ref /hero.txt → /files/hero.txt 24dc5b87
|
|
68
68
|
ref /does-not-exist [UNRESOLVED — nothing answers to this name]
|
|
69
69
|
layout /layouts/page.hbs f274678c
|
|
70
70
|
lookup /contacts [UNRESOLVED — nothing answers to this name]
|
|
71
|
+
query {"meta.href":"/system/navigation"} → /documents/navigation.yml
|
|
72
|
+
query {"meta.href":"/cosmetics/celestetic"} [MATCHES NOTHING]
|
|
71
73
|
```
|
|
72
74
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
A dangling edge is usually the answer on its own, and `query` edges are
|
|
76
|
+
where most of it hides on a site whose references go through a sidecar's
|
|
77
|
+
`findEntity` — there, `ref` and `lookup` may be zero and `query`
|
|
78
|
+
everything.
|
|
79
|
+
|
|
80
|
+
Query counts are computed when you ask, not when the edge was recorded.
|
|
81
|
+
That is deliberate: an edge stores the filter WITHOUT its results, so an
|
|
82
|
+
entity appearing tomorrow still invalidates the page that links to it
|
|
83
|
+
today. An edge that recorded its bindings would record nothing for a query
|
|
84
|
+
matching nothing, and creating the target later would re-render nothing.
|
|
85
|
+
|
|
86
|
+
In `--json`, every query edge carries `matched` explicitly — an integer,
|
|
87
|
+
or `null` for a predicate that could not be serialized (`findEntities()`
|
|
88
|
+
with no argument, or a function filter), which invalidates on any mutation
|
|
89
|
+
by design. `matched: 1` also carries `sample` with the id. The field is
|
|
90
|
+
always present, so a consumer never has to read absence as meaning
|
|
91
|
+
anything.
|
|
92
|
+
|
|
93
|
+
Add `--json` for the whole report as a machine-readable object. Exits `3`
|
|
94
|
+
when the entity cannot be found.
|
|
76
95
|
|
|
77
96
|
### `--json`
|
|
78
97
|
|
package/index.js
CHANGED
|
@@ -50,11 +50,11 @@ export { renderHbs } from './src/plugins/render/hbs.js'
|
|
|
50
50
|
export { renderPreset } from './src/plugins/render/preset.js'
|
|
51
51
|
|
|
52
52
|
// Template helpers — these only install functions on `runtime` for templates
|
|
53
|
-
// to call. They render nothing,
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
// render throw.
|
|
53
|
+
// to call. They render nothing, and the names say so: two of the six
|
|
54
|
+
// factories in this folder are renderers and four are not, so each is named
|
|
55
|
+
// after the job it does rather than the object it concerns. Naming them all
|
|
56
|
+
// `render*` invites adding renderPreset() expecting a helper and watching
|
|
57
|
+
// every page render throw. There are no aliases for the older names.
|
|
58
58
|
export { assetUrlHelper } from './src/plugins/render/asset.js'
|
|
59
59
|
export { hrefUrlHelpers } from './src/plugins/render/href.js'
|
|
60
60
|
export { resourceUrlHelper } from './src/plugins/render/resource.js'
|
package/package.json
CHANGED
package/src/catalog.js
CHANGED
|
@@ -360,8 +360,10 @@ onFinalize(async () => {
|
|
|
360
360
|
}
|
|
361
361
|
})
|
|
362
362
|
|
|
363
|
-
// Snapshot the catalog as `{ version, entities: [...] }
|
|
364
|
-
//
|
|
363
|
+
// Snapshot the catalog as `{ version, entities: [...] }` — the schema
|
|
364
|
+
// version plus every entity body. O(N) in both time and memory, so it is
|
|
365
|
+
// for debugging and inspection, never a hot path; `iterateEntities`
|
|
366
|
+
// streams when the result set might be corpus-scale.
|
|
365
367
|
function exportCatalog() {
|
|
366
368
|
if (!db?.isOpen) return { version: null, entities: [] }
|
|
367
369
|
const rows = stmtAllData.all()
|
package/src/config.js
CHANGED
|
@@ -56,11 +56,9 @@ onLoad(async () => {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
// files have nothing to bind to. The loader was removed when the
|
|
65
|
-
// plugins list stopped carrying names.
|
|
59
|
+
// Nothing else is loaded. There is deliberately no `config/<plugin>
|
|
60
|
+
// .config.js` channel: plugin options arrive as factory arguments
|
|
61
|
+
// (ADR-0010), and an entry in `plugins` is a factory call result — a
|
|
62
|
+
// closure or a descriptor — carrying no name to bind an auxiliary file
|
|
63
|
+
// to. A plugin wanting file-based config reads it itself.
|
|
66
64
|
})
|
package/src/explain.js
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
//
|
|
10
10
|
// Follows --verify's shape: report and exit, no build phases run.
|
|
11
11
|
import { inputHashOf, inputPartsOf, diffInputParts, lookupKeys, checksum as fileChecksum } from './utils.js'
|
|
12
|
-
import {
|
|
12
|
+
import { filterKey } from './track.js'
|
|
13
|
+
import { findEntity, findEntities } from './catalog.js'
|
|
13
14
|
import runtime from './runtime.js'
|
|
14
15
|
|
|
15
16
|
const shortHash = (h) => (h ? String(h).slice(0, 8) : null)
|
|
@@ -28,6 +29,62 @@ async function resolve(reference) {
|
|
|
28
29
|
return like ?? null
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
// How many entities each recorded query filter matches RIGHT NOW.
|
|
33
|
+
//
|
|
34
|
+
// A query edge is a predicate — "any entity matching this" — and the
|
|
35
|
+
// recording layer deliberately stores the filter without its results, so
|
|
36
|
+
// that an entity appearing later still invalidates the render. That is the
|
|
37
|
+
// property that makes aggregate pages work and it must not change.
|
|
38
|
+
//
|
|
39
|
+
// Explaining is a different situation: read-only, reporting instead of
|
|
40
|
+
// building, with the catalog already open. So the count is computed here,
|
|
41
|
+
// at read time, where it costs nothing and answers "is this reference
|
|
42
|
+
// dangling?" for the edge kind that carries most of a real site's
|
|
43
|
+
// references.
|
|
44
|
+
//
|
|
45
|
+
// Run through findEntities rather than hand-rolled SQL: stored filters
|
|
46
|
+
// include $regex and anything else sift accepts, and one code path is the
|
|
47
|
+
// only way the count means the same thing the render meant.
|
|
48
|
+
//
|
|
49
|
+
// A null filter is the sentinel for a predicate that could not be
|
|
50
|
+
// serialized (a function filter, or findEntities() with no argument). It
|
|
51
|
+
// invalidates on any mutation by design, and there is nothing to evaluate,
|
|
52
|
+
// so it reports null rather than a misleading zero.
|
|
53
|
+
async function countQueryMatches(snapshots) {
|
|
54
|
+
const counts = new Map()
|
|
55
|
+
for (const snap of snapshots) {
|
|
56
|
+
for (const entry of snap.refClosure ?? []) {
|
|
57
|
+
if (entry.kind !== 'query') continue
|
|
58
|
+
const key = filterKey(entry.filter ?? null)
|
|
59
|
+
if (counts.has(key)) continue
|
|
60
|
+
if (entry.filter == null) {
|
|
61
|
+
counts.set(key, null)
|
|
62
|
+
continue
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
// recordQuery no-ops outside a render's queryContext, so
|
|
66
|
+
// explaining a page cannot record edges onto it.
|
|
67
|
+
const matches = await findEntities(entry.filter)
|
|
68
|
+
counts.set(key, { count: matches.length, sample: matches[0]?.id ?? null })
|
|
69
|
+
} catch {
|
|
70
|
+
// A stored filter the catalog can no longer evaluate is worth
|
|
71
|
+
// saying so about, not worth failing the whole report for.
|
|
72
|
+
counts.set(key, 'unevaluable')
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return counts
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Project a counted filter into the report's shape.
|
|
80
|
+
function queryCount(counts, filter) {
|
|
81
|
+
const hit = counts.get(filterKey(filter ?? null))
|
|
82
|
+
if (hit === null) return { matched: null }
|
|
83
|
+
if (hit === 'unevaluable') return { matched: null, unevaluable: true }
|
|
84
|
+
if (!hit) return { matched: null }
|
|
85
|
+
return { matched: hit.count, ...(hit.count === 1 && hit.sample ? { sample: hit.sample } : {}) }
|
|
86
|
+
}
|
|
87
|
+
|
|
31
88
|
// The verdict line names what moved when it can. That line is the one
|
|
32
89
|
// people read, so "the input hash differs" there is the answer stopping one
|
|
33
90
|
// step short of useful.
|
|
@@ -60,6 +117,7 @@ export async function explain(reference) {
|
|
|
60
117
|
const snapshots = runtime.manifest?.snapshotsFor(entity.id) ?? []
|
|
61
118
|
const currentHash = inputHashOf(entity)
|
|
62
119
|
const currentParts = inputPartsOf(entity)
|
|
120
|
+
const queryMatches = await countQueryMatches(snapshots)
|
|
63
121
|
|
|
64
122
|
// The catalog is as of the LAST BUILD. If the file has been edited since,
|
|
65
123
|
// nothing here knows it yet — the hashes would all agree and the verdict
|
|
@@ -141,7 +199,17 @@ export async function explain(reference) {
|
|
|
141
199
|
parent: snap.parent ?? null,
|
|
142
200
|
refClosure: (snap.refClosure ?? []).map(entry =>
|
|
143
201
|
entry.kind === 'query'
|
|
144
|
-
? {
|
|
202
|
+
? {
|
|
203
|
+
kind: 'query',
|
|
204
|
+
filter: entry.filter,
|
|
205
|
+
// An explicit integer, because a MISSING key is
|
|
206
|
+
// ambiguous to a consumer: an audit script reading
|
|
207
|
+
// absence as "unresolved" reports every query edge
|
|
208
|
+
// as dangling. `null` means the filter could not be
|
|
209
|
+
// evaluated (unserializable predicate); a number is
|
|
210
|
+
// a number.
|
|
211
|
+
...queryCount(queryMatches, entry.filter),
|
|
212
|
+
}
|
|
145
213
|
: {
|
|
146
214
|
kind: entry.kind,
|
|
147
215
|
target: entry.target,
|
|
@@ -222,7 +290,16 @@ export function formatExplain(report) {
|
|
|
222
290
|
row('refClosure', `${closure.length} edge${closure.length === 1 ? '' : 's'}`)
|
|
223
291
|
for (const e of closure) {
|
|
224
292
|
if (e.kind === 'query') {
|
|
225
|
-
|
|
293
|
+
// The count is the point: a query matching nothing is the
|
|
294
|
+
// dangling-reference case for the edge kind that carries most
|
|
295
|
+
// of a real site's references, and it printed identically to
|
|
296
|
+
// one that matched.
|
|
297
|
+
const state = e.unevaluable ? ' [FILTER CANNOT BE EVALUATED]'
|
|
298
|
+
: e.matched === null ? ' (untracked predicate — any mutation invalidates)'
|
|
299
|
+
: e.matched === 0 ? ' [MATCHES NOTHING]'
|
|
300
|
+
: e.matched === 1 ? ` → ${e.sample ?? '1 entity'}`
|
|
301
|
+
: ` → ${e.matched} entities`
|
|
302
|
+
out.push(` query ${JSON.stringify(e.filter)}${state}`)
|
|
226
303
|
continue
|
|
227
304
|
}
|
|
228
305
|
// Show the name asked for and the entity it bound to, since
|
package/src/manifest.js
CHANGED
|
@@ -225,10 +225,11 @@ function snapToRow(snap) {
|
|
|
225
225
|
// is the dominant shape. A page destination treated as a filesystem path
|
|
226
226
|
// would otherwise be looked for at the root of the disk.
|
|
227
227
|
//
|
|
228
|
-
//
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
//
|
|
228
|
+
// One definition for both callers. verify() and hashOutputFile ask the same
|
|
229
|
+
// question, and a private copy of the join in each is how one mistake
|
|
230
|
+
// becomes two symptoms — one loud (every asset reported missing, at its
|
|
231
|
+
// real and present path) and one silent (no outputHash recorded, leaving
|
|
232
|
+
// most snapshots presence-checked only).
|
|
232
233
|
export function resolveOutputPath(destination, outputFolder = runtime.options?.outputFolder) {
|
|
233
234
|
if (!destination) return undefined
|
|
234
235
|
const joined = path.join(outputFolder ?? '', destination)
|
package/src/plugins/api.js
CHANGED
|
@@ -607,8 +607,9 @@ export function api(options = {}) {
|
|
|
607
607
|
// credential declaring `api:delete` still cannot delete on an
|
|
608
608
|
// endpoint whose `operations` omits it, and a bare token (which
|
|
609
609
|
// declares nothing, capabilities === null) is bounded by the
|
|
610
|
-
// endpoint alone
|
|
611
|
-
//
|
|
610
|
+
// endpoint alone, which is what makes a plain token still a
|
|
611
|
+
// complete answer: it grants the endpoint's own ceiling and
|
|
612
|
+
// nothing more.
|
|
612
613
|
const allow = (op) => (req, res, next) => {
|
|
613
614
|
if (!allowedOps.has(op)) {
|
|
614
615
|
return res.status(403).json({
|
|
@@ -15,8 +15,8 @@ import path from 'node:path'
|
|
|
15
15
|
// assets() runs presets and produces derivatives (the work)
|
|
16
16
|
// renderPreset() renders a preset-authored layout (this file)
|
|
17
17
|
//
|
|
18
|
-
// The helpers
|
|
19
|
-
// renderPreset keeps its name because it really is a renderer.
|
|
18
|
+
// The URL helpers carry `Helper` in their names for exactly this reason;
|
|
19
|
+
// renderPreset keeps its bare name because it really is a renderer.
|
|
20
20
|
export async function load({ entity, runtime }) {
|
|
21
21
|
if (!entity?.preset?.uri) return
|
|
22
22
|
const preset = await import(`${entity.preset.uri}?stamp=${Date.now()}`)
|
package/src/postprocess.js
CHANGED
|
@@ -253,10 +253,10 @@ export default async ({ entity, options, config, context, state, logger, port })
|
|
|
253
253
|
}
|
|
254
254
|
|
|
255
255
|
// Cleanup the renderer's origin (first stage's input) when it's
|
|
256
|
-
// a different path from the chain's final destination. The
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
//
|
|
256
|
+
// a different path from the chain's final destination. The chain
|
|
257
|
+
// contract puts this on the dispatcher rather than on layouts'
|
|
258
|
+
// onComplete, which is what lets onComplete stay agnostic about
|
|
259
|
+
// whether a postprocess output was written to disk at all.
|
|
260
260
|
if (entity.origin && entity.origin !== entity.destination) {
|
|
261
261
|
try { await unlink(path.join(options.outputFolder, entity.origin)) } catch {}
|
|
262
262
|
}
|
|
@@ -264,7 +264,7 @@ export default async ({ entity, options, config, context, state, logger, port })
|
|
|
264
264
|
// Return undefined so the engine's onPostprocess loop doesn't
|
|
265
265
|
// try to thread `result` back through layouts' onComplete (which
|
|
266
266
|
// would attempt a writeFile against a non-bytes value). The
|
|
267
|
-
// postprocess plugins
|
|
268
|
-
// for the engine to flush.
|
|
267
|
+
// postprocess plugins have written directly to disk by this point;
|
|
268
|
+
// there is nothing for the engine to flush.
|
|
269
269
|
return undefined
|
|
270
270
|
}
|