mikser-io 9.21.1 → 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/package.json +1 -1
- package/src/explain.js +80 -3
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/package.json
CHANGED
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
|