mikser-io-layouts 2.11.0 → 2.11.2
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/lib/mcp.js +55 -29
- package/package.json +1 -1
- package/test/check-entity.test.js +30 -6
package/lib/mcp.js
CHANGED
|
@@ -188,6 +188,9 @@ export function registerCheckTool({ runtime, findEntity, findEntities, useDataba
|
|
|
188
188
|
}
|
|
189
189
|
return
|
|
190
190
|
case 'array':
|
|
191
|
+
// The element path is NOT added as a key of its own. Requiring
|
|
192
|
+
// `tags[]` would claim the array must be non-empty, which no
|
|
193
|
+
// schema said — an empty list is a valid `z.array()`.
|
|
191
194
|
schemaKeys(def.element, `${prefix}[]`, required, optional, depth + 1)
|
|
192
195
|
return
|
|
193
196
|
default:
|
|
@@ -448,14 +451,23 @@ export function registerCheckTool({ runtime, findEntity, findEntities, useDataba
|
|
|
448
451
|
// must contain, where consumer reads only say what someone
|
|
449
452
|
// happened to look at. Without this a data document was the
|
|
450
453
|
// one kind that could not benefit from its own schema.
|
|
454
|
+
// Two questions, two comparisons — as on a page.
|
|
455
|
+
//
|
|
456
|
+
// `unused` and `untraceable` ask "does anything READ this",
|
|
457
|
+
// which only consumption answers. Running them against the
|
|
458
|
+
// schema instead reported keys as unread that the recorded
|
|
459
|
+
// reads plainly contain: a schema does not enumerate array
|
|
460
|
+
// elements, so every `x[]` in the document looked untouched.
|
|
461
|
+
const byReads = compare(entity, new Set(read.keys), new Set())
|
|
451
462
|
const declared = schemaFor(entity)
|
|
452
|
-
let cmp
|
|
463
|
+
let cmp = byReads
|
|
453
464
|
if (declared) {
|
|
454
465
|
const req = new Set(); const opt = new Set()
|
|
455
466
|
schemaKeys(declared.schema, '', req, opt)
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
cmp =
|
|
467
|
+
const bySchema = compare(entity, new Set([...req, ...opt]), opt)
|
|
468
|
+
// `missing` from the declaration, the rest from reality.
|
|
469
|
+
cmp = { ...byReads, missing: bySchema.missing,
|
|
470
|
+
missingOptional: bySchema.missingOptional }
|
|
459
471
|
}
|
|
460
472
|
// Matters most here: a data document is otherwise only
|
|
461
473
|
// checked against whatever its consumers happen to read, so
|
|
@@ -481,14 +493,9 @@ export function registerCheckTool({ runtime, findEntity, findEntities, useDataba
|
|
|
481
493
|
counts: { consumedKeys: read.keys.length, provided: cmp.provided.size,
|
|
482
494
|
missing: cmp.missing.length, unused: cmp.unused.length },
|
|
483
495
|
notes: [
|
|
484
|
-
`
|
|
485
|
-
read.
|
|
486
|
-
|
|
487
|
-
: 'No key-level reads are recorded yet, so only the consumer list is known. Build once with a current mikser and the fields each render takes off this entity get recorded.',
|
|
488
|
-
'untraceable means an ancestor WAS read but the members could not be followed: the value was handed to a template and provenance does not survive that hop. Treat them as consumed.',
|
|
489
|
-
'missingFrom says where `missing` came from: "schema" is declared and authoritative; "consumers" means it is derived from what renders actually read off this entity, which only covers what someone happened to look at.',
|
|
490
|
-
'peerGaps compares this record with the others declaring the same type — keys most of them carry that this one lacks. For a data document this is often the only check with teeth, since everything else here depends on what a consumer happened to read. Weak evidence, never a failure.',
|
|
491
|
-
'unused here means no render took the key. It may still be served to an API client or another process, which this tool cannot see — mikser is not always a static site generator.',
|
|
496
|
+
`Never renders — ${consumers.length} render(s) query it. Building will not make it appear, and a key it lacks shows up as a hole in those pages.`,
|
|
497
|
+
'missing: check missingFrom — "schema" is declared, "consumers" is only what someone happened to read off it.',
|
|
498
|
+
'Look, do not panic: consumedKeys (what renders actually took), peerGaps (most records of this type carry it), unused (may still be served to an API client), untraceable (an ancestor was read but its members could not be followed). None of these is a defect on its own.',
|
|
492
499
|
],
|
|
493
500
|
})
|
|
494
501
|
}
|
|
@@ -506,12 +513,11 @@ export function registerCheckTool({ runtime, findEntity, findEntities, useDataba
|
|
|
506
513
|
peerGroup: { type: orphanPeers.type, peers: orphanPeers.peers } }
|
|
507
514
|
: {}),
|
|
508
515
|
notes: [
|
|
516
|
+
'Never renders, declares no layout, and no render queried it. Not an error: the catalog is readable over the API, so a consumer outside this project may depend on it.',
|
|
509
517
|
...(orphanPeers?.gaps.length
|
|
510
|
-
? ['peerGaps is the only check
|
|
518
|
+
? ['peerGaps is the only check possible here — nothing reads this entity, so only the other records of its type say anything about it.']
|
|
511
519
|
: []),
|
|
512
|
-
'
|
|
513
|
-
'That is not an error. The catalog is readable over the API, so another process or an external consumer may depend on it, and this tool cannot see them.',
|
|
514
|
-
'If it was MEANT to be a page, it matched no layout — check the layouts match patterns or set meta.layout.',
|
|
520
|
+
'If it was meant to be a page it matched no layout: check the layouts match patterns, or set meta.layout.',
|
|
515
521
|
],
|
|
516
522
|
})
|
|
517
523
|
}
|
|
@@ -560,13 +566,36 @@ export function registerCheckTool({ runtime, findEntity, findEntities, useDataba
|
|
|
560
566
|
// declared key no layout reads.
|
|
561
567
|
const knows = (k) => req.has(k) || opt.has(k)
|
|
562
568
|
|| [...req, ...opt].some(d => d.startsWith(`${k}.`) || d.startsWith(`${k}[`))
|
|
563
|
-
|
|
564
|
-
|
|
569
|
+
|
|
570
|
+
// `declaredButNotRead` asks whether a declared key is read by
|
|
571
|
+
// ANY layout, so it is answered against the UNSCOPED contract —
|
|
572
|
+
// the opposite of `missing`, which asks what THIS page needs.
|
|
573
|
+
//
|
|
574
|
+
// Scoped, a shorter edition of a page looked broken: every
|
|
575
|
+
// section it does not use made the schema's keys for that
|
|
576
|
+
// section look dead, and one such page reported 118 of them
|
|
577
|
+
// while its longer sibling reported none. The document's only
|
|
578
|
+
// difference was using fewer sections.
|
|
579
|
+
const wide = await runtime.options.layouts.inspect(layoutId, { samples: 0 })
|
|
580
|
+
const anyLayout = new Set([
|
|
581
|
+
...(wide.references?.contract?.meta ?? []),
|
|
582
|
+
...observed,
|
|
583
|
+
])
|
|
584
|
+
const readAnywhere = (k) => anyLayout.has(k)
|
|
585
|
+
|| [...anyLayout].some(c => c.startsWith(`${k}.`) || c.startsWith(`${k}[`))
|
|
565
586
|
drift = {
|
|
566
587
|
schema: declaredSchema.name,
|
|
567
588
|
readButNotDeclared: [...consumed].filter(k => k && !knows(k)).sort(),
|
|
568
|
-
declaredButNotRead: [...req].filter(k => !
|
|
589
|
+
declaredButNotRead: [...req].filter(k => !readAnywhere(k)).sort(),
|
|
569
590
|
}
|
|
591
|
+
|
|
592
|
+
// And the same noise in the softer list: a key this page's
|
|
593
|
+
// layout cannot reach is not an omission, it is not applicable.
|
|
594
|
+
// Reporting 139 of them is the noise the scoping fix removed
|
|
595
|
+
// from `missing`, arriving by another route.
|
|
596
|
+
const reachable = (k) => consumed.has(k)
|
|
597
|
+
|| [...consumed].some(c => c.startsWith(`${k}.`) || c.startsWith(`${k}[`))
|
|
598
|
+
missingOptional = missingOptional.filter(reachable)
|
|
570
599
|
}
|
|
571
600
|
|
|
572
601
|
// A declared section that resolved to no template under this
|
|
@@ -605,17 +634,14 @@ export function registerCheckTool({ runtime, findEntity, findEntities, useDataba
|
|
|
605
634
|
contract: { complete: contract.complete, incomplete: contract.incomplete ?? [],
|
|
606
635
|
scopedToPartials: usedPartials?.length ?? null },
|
|
607
636
|
observedReads: observed.length,
|
|
637
|
+
// One line per field, saying what to DO with it. These are
|
|
638
|
+
// read on every call, so anything that is not actionable is
|
|
639
|
+
// cost without value.
|
|
608
640
|
notes: [
|
|
609
|
-
'missing
|
|
610
|
-
'
|
|
611
|
-
'
|
|
612
|
-
'
|
|
613
|
-
'peerGaps compares this record with the others declaring the same type: keys most of them carry that this one lacks, with the count as evidence. A layout contract cannot see this — a record missing a field its siblings all have renders perfectly and reads nothing that is absent. Weak evidence, and never a failure: records differ for good reasons.',
|
|
614
|
-
'unresolvedSections named a section that resolved to no template under THIS layout. Weak evidence: the section may render under a different layout, its template may not be written yet, or the data may be consumed outside the SSG entirely — the catalog is readable over the API. It never fails a check.',
|
|
615
|
-
'untraceable is NOT a list of problems. An ancestor of each key WAS read, but the value was handed on — a sidecar returning it to a template — and provenance does not survive that hop, so the members could not be followed. Treat them as consumed unless you have another reason to doubt it.',
|
|
616
|
-
'unused is weak evidence too, and weakest of all where mikser is not a static site generator. A key may be consumed by a DIFFERENT layout, or by an API client reading the catalog over HTTP, or by another process entirely — none of which this tool can see. Engine-owned keys (href, lang, layout, task, ...) are excluded outright.',
|
|
617
|
-
'The contract is scoped to the partials this entity ACTUALLY rendered with, not to every branch the layout could take. Unscoped, a page that uses two of twenty sections is checked against all twenty.',
|
|
618
|
-
'reliable is false unless the contract was fully read AND this entity has rendered. An absence is only evidence when it is true.',
|
|
641
|
+
'missing: the layout needs it and this document has no value for it. Check missingFrom — "schema" is declared and authoritative, "inferred" is read from templates and is strong evidence rather than proof.',
|
|
642
|
+
'likelyTypos: a key you wrote paired with one the layout wanted, within two edits. A typo produces both, so the pair is worth more than either list.',
|
|
643
|
+
'Look, do not panic: missingOptional (guarded by the layout, safe to omit), unused (may be read by another layout or an API client), untraceable (an ancestor was read but its members could not be followed), unresolvedSections (matched no template here), peerGaps (most records of this type carry it). None of these is a defect on its own.',
|
|
644
|
+
'reliable: false means the contract had gaps or this entity has not rendered. An absence is only evidence when it is true.',
|
|
619
645
|
],
|
|
620
646
|
})
|
|
621
647
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mikser-io-layouts",
|
|
3
|
-
"version": "2.11.
|
|
3
|
+
"version": "2.11.2",
|
|
4
4
|
"description": "SSG-flavor task-production policy for mikser-io. Multi-match layout assignment, per-layout destinations, postprocess chain attachment, layout inspection.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -198,7 +198,8 @@ describe('mikser_check_entity: a page', () => {
|
|
|
198
198
|
const r = await check('/documents/extra.md')
|
|
199
199
|
assert.ok(r.unused.includes('card'), `unused: ${r.unused.join(', ')}`)
|
|
200
200
|
assert.deepEqual(r.missing, [], 'an unused key is not a missing one')
|
|
201
|
-
assert.ok(r.notes.some(n => /
|
|
201
|
+
assert.ok(r.notes.some(n => /another layout or an API client/.test(n)),
|
|
202
|
+
'the caveat has to travel with the finding')
|
|
202
203
|
})
|
|
203
204
|
|
|
204
205
|
it('never reports engine-owned keys as unused', async () => {
|
|
@@ -214,7 +215,8 @@ describe('mikser_check_entity: a page', () => {
|
|
|
214
215
|
const r = await check('/documents/ghost.md')
|
|
215
216
|
assert.deepEqual(r.unresolvedSections, ['nosuchsection'])
|
|
216
217
|
assert.deepEqual(r.missing, [], 'an unresolved section is not a missing key')
|
|
217
|
-
assert.ok(r.notes.some(n => /unresolvedSections/.test(n) && /
|
|
218
|
+
assert.ok(r.notes.some(n => /unresolvedSections/.test(n) && /defect/.test(n)),
|
|
219
|
+
'it must be named as something to look at, not a failure')
|
|
218
220
|
})
|
|
219
221
|
|
|
220
222
|
it('uses the layout the entity actually rendered with', async () => {
|
|
@@ -233,7 +235,7 @@ describe('mikser_check_entity: not a page', () => {
|
|
|
233
235
|
assert.equal(r.kind, 'data')
|
|
234
236
|
assert.deepEqual(r.consumedBy, ['/documents/good.md'])
|
|
235
237
|
assert.ok(!JSON.stringify(r.notes).includes('build once'))
|
|
236
|
-
assert.ok(r.notes.some(n => /hole in
|
|
238
|
+
assert.ok(r.notes.some(n => /hole in those pages/.test(n)),
|
|
237
239
|
'it has to say where a mistake would actually show up')
|
|
238
240
|
})
|
|
239
241
|
|
|
@@ -260,7 +262,8 @@ describe('mikser_check_entity: not a page', () => {
|
|
|
260
262
|
const group = r.untraceable.find(g => g.under === 'enquiry')
|
|
261
263
|
assert.ok(group, `expected them collapsed under enquiry, got ${JSON.stringify(r.untraceable)}`)
|
|
262
264
|
assert.ok(group.keys.includes('enquiry.fields[].label'))
|
|
263
|
-
assert.ok(r.notes.some(n => /
|
|
265
|
+
assert.ok(r.notes.some(n => /untraceable/.test(n) && /could not be followed/.test(n)),
|
|
266
|
+
'the reason it cannot be judged has to travel with it')
|
|
264
267
|
})
|
|
265
268
|
|
|
266
269
|
it('still reports a key nothing read at all', async () => {
|
|
@@ -370,6 +373,27 @@ describe('mikser_check_entity: with a declared schema', () => {
|
|
|
370
373
|
`missingOptional: ${r.missingOptional.join(', ')}`)
|
|
371
374
|
})
|
|
372
375
|
|
|
376
|
+
it('answers declaredButNotRead against ANY layout, not just this page', async () => {
|
|
377
|
+
// The mirror of the scoping fix. `missing` asks what THIS page needs,
|
|
378
|
+
// so it is scoped; "is this key read anywhere" is the opposite question
|
|
379
|
+
// and must not be. Scoped, a shorter edition of a page reported every
|
|
380
|
+
// key of every section it does not use as dead — 118 of them on a page
|
|
381
|
+
// whose only difference was using fewer sections than its sibling.
|
|
382
|
+
const r = await checkWithSchema('/documents/schemed.md')
|
|
383
|
+
assert.ok(!r.drift.declaredButNotRead.some(k => k.startsWith('hero.')),
|
|
384
|
+
`hero keys are read by a layout, just not this render: ${r.drift.declaredButNotRead.join(', ')}`)
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
it('does not list an optional key this page could not use anyway', async () => {
|
|
388
|
+
// Same noise by another route: a key outside what this page renders is
|
|
389
|
+
// not an omission, it is not applicable.
|
|
390
|
+
const r = await checkWithSchema('/documents/schemed.md')
|
|
391
|
+
for (const k of r.missingOptional) {
|
|
392
|
+
assert.ok(!/^(specs|series|results)\./.test(k),
|
|
393
|
+
`${k} belongs to a section this page does not render`)
|
|
394
|
+
}
|
|
395
|
+
})
|
|
396
|
+
|
|
373
397
|
it('reports drift in both directions', async () => {
|
|
374
398
|
const r = await checkWithSchema('/documents/schemed.md')
|
|
375
399
|
assert.equal(r.drift.schema, 'article')
|
|
@@ -384,7 +408,7 @@ describe('mikser_check_entity: with a declared schema', () => {
|
|
|
384
408
|
it('labels an inferred contract as inferred when there is no schema', async () => {
|
|
385
409
|
const r = await check('/documents/good.md')
|
|
386
410
|
assert.equal(r.missingFrom, 'inferred')
|
|
387
|
-
assert.ok(r.notes.some(n => /
|
|
411
|
+
assert.ok(r.notes.some(n => /strong evidence rather than proof/.test(n)),
|
|
388
412
|
'the weaker source has to say that it is weaker')
|
|
389
413
|
})
|
|
390
414
|
})
|
|
@@ -460,7 +484,7 @@ describe('mikser_check_entity: peer comparison', () => {
|
|
|
460
484
|
it('never fails the check — it is a prompt, not a defect', async () => {
|
|
461
485
|
const r = await peerCheck('/documents/cat-gap.md')
|
|
462
486
|
assert.notEqual(r.kind, undefined)
|
|
463
|
-
assert.ok(r.notes.some(n => /
|
|
487
|
+
assert.ok(r.notes.some(n => /peerGaps/.test(n) && /defect|only check possible/.test(n)),
|
|
464
488
|
'the weakness has to travel with the finding')
|
|
465
489
|
})
|
|
466
490
|
|