P (repo): neutral wording in the gates and docs, and no private-process checks
Comments, messages and docs only; no check logic changes. - The role-word gate is renamed check-no-internal-role-nouns (and its baseline fixture). Same logic: it builds the word from parts so the file does not carry it, and a planted line in title case and in capitals still fails it while WebDriver and "GPU driver" do not. - public-hygiene-check: the two role patterns are built from parts (the regexes are byte- identical), the header loses the forge and date wording, and the prayer comment loses its decision date. Its output on the tree is identical in normal and --strict modes. - landing-preconditions, check-no-public-binds, check-published-tree-weight: no fleet tool name, no "this box", no forge name in comments and messages. - Removed: check-backlog-refs, check-backlog-status-enum, check-done-evidence, check-verification-tags and check-dxlife-untouched (they police a private backlog and workspace), with their scripts, docs, gate-list entries and orphan-baseline entry, and the committed needs-eyes pages (check-embedded-rtl --emit writes them on demand). - package.json docs: no agent names. Docs: the stale check mentions and the snapshot references are gone (the snapshots themselves leave in the studio P commit). gates:pre: PASS 115, FAIL 0, NOT MEASURED 1 (browser seat).
This commit is contained in:
parent
3f7d489409
commit
2eaea5cedf
22 changed files with 74 additions and 720 deletions
|
|
@ -11,7 +11,6 @@
|
|||
AGENT-BRIEF.md
|
||||
_archive
|
||||
data-private
|
||||
library/scripts/posters/snapshots/_granular-experiment.mjs.shelved
|
||||
plugin-designs/key-cities
|
||||
name:*field-access*
|
||||
name:*visa*.json
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ The scripts these commands are grounded in (from this branch's `package.json`):
|
|||
"build": "bash internal/build-lock.sh",
|
||||
"build:unlocked":"bun run gates:pre && bun run library-index && bun run check:library-index && bun run clean && bun run token && bun run build:bundles && bun run tree && bun run gates:post",
|
||||
"gates:pre":"node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts",
|
||||
"gates:post":"node internal/run-parallel.mjs --label=post -- check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:pin-dim-slider check:legend-source check:backlog-refs check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary check:wp-embed",
|
||||
"gates:post":"node internal/run-parallel.mjs --label=post -- check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:script-flags check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:pin-dim-slider check:legend-source check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary check:wp-embed",
|
||||
"build:bundles": "node internal/run-parallel.mjs --bundles", // the 22 vite builds, 4 at a time
|
||||
"preview": "bun x vite preview",
|
||||
"serve": "node internal/serve-strict.mjs", // 404s a directory, like production
|
||||
|
|
|
|||
|
|
@ -42,7 +42,6 @@ the app is rebuilt.**
|
|||
| `build-gallery.mjs` | `out/index.html` — browsable preview grid, sectioned by region. |
|
||||
| `build-hd-gallery.mjs` | `out/hd.html` — full-resolution review + copy-to-clipboard. |
|
||||
| `export-public.mjs` | Copies PNGs into region folders OUTSIDE the repo (see below). |
|
||||
| `snapshots/` | Frozen template versions. v5 is the approved current; v1–v4 are the lineage. |
|
||||
|
||||
## Environment
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
# needs-eyes — partner-embed fixtures
|
||||
|
||||
Under the team's Playwright policy (2026-09-25) only **rega** runs a browser on the VPS, and a
|
||||
maintainer's laptop is the other pair of eyes. So a check that can only be seen has to be *openable by
|
||||
someone else* — not a construct that lives inside one agent's gate process.
|
||||
|
||||
These pages are generated, not hand-written:
|
||||
A check that can only be seen has to be *openable by someone else*, not a construct that lives inside
|
||||
one gate's process. These pages are generated on demand and are not committed; write them with:
|
||||
|
||||
```
|
||||
node internal/check-embedded-rtl.mjs --emit
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en"><head><meta charset="utf-8"><title>partner</title>
|
||||
<style>html,body{margin:0}#w{height:900px}</style></head>
|
||||
<body><h1 id="partner-heading">Partner page</h1><div id="w"></div>
|
||||
<!-- A REAL PARTNER EMBED SETS THIS. The generated leaf pages bake window.MAP_APP_API_URL, and
|
||||
without it every data fetch resolves same-origin against the partner and fails, so the wall
|
||||
renders zero cards and the locale has nothing to translate. The first run of this gate
|
||||
omitted it and reported "never mounted" — a harness gap, not the RTL defect. -->
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<script src="http://127.0.0.1:47395/dx-maps/my-upg-100-list/my-upg-100-list.js" crossorigin="anonymous"></script>
|
||||
<script>
|
||||
var el = document.createElement("my-upg-100-list-map");
|
||||
el.setAttribute('profile-config', JSON.stringify({ profile: "poster-wall",
|
||||
assetsBase: "http://127.0.0.1:47395/dx-maps/assets" }));
|
||||
el.style.cssText = 'display:block;width:100%;height:900px';
|
||||
document.getElementById('w').appendChild(el);
|
||||
</script></body></html>
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en"><head><meta charset="utf-8"><title>partner</title>
|
||||
<style>html,body{margin:0}#w{height:900px}</style></head>
|
||||
<body><h1 id="partner-heading">Partner page</h1><div id="w"></div>
|
||||
<!-- A REAL PARTNER EMBED SETS THIS. The generated leaf pages bake window.MAP_APP_API_URL, and
|
||||
without it every data fetch resolves same-origin against the partner and fails, so the wall
|
||||
renders zero cards and the locale has nothing to translate. The first run of this gate
|
||||
omitted it and reported "never mounted" — a harness gap, not the RTL defect. -->
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<script src="http://127.0.0.1:47395/dx-maps/my-upg-100-list/my-upg-100-list.js" crossorigin="anonymous"></script>
|
||||
<script>
|
||||
var el = document.createElement("my-upg-100-list-map");
|
||||
el.setAttribute('profile-config', JSON.stringify({ profile: "poster-wall", locale: 'ar',
|
||||
assetsBase: "http://127.0.0.1:47395/dx-maps/assets" }));
|
||||
el.style.cssText = 'display:block;width:100%;height:900px';
|
||||
document.getElementById('w').appendChild(el);
|
||||
</script></body></html>
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en"><head><meta charset="utf-8"><title>partner</title>
|
||||
<style>html,body{margin:0}#w{height:900px}</style></head>
|
||||
<body><h1 id="partner-heading">Partner page</h1><div id="w"></div>
|
||||
<!-- A REAL PARTNER EMBED SETS THIS. The generated leaf pages bake window.MAP_APP_API_URL, and
|
||||
without it every data fetch resolves same-origin against the partner and fails, so the wall
|
||||
renders zero cards and the locale has nothing to translate. The first run of this gate
|
||||
omitted it and reported "never mounted" — a harness gap, not the RTL defect. -->
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<script src="http://127.0.0.1:47395/dx-maps/my-upg-100-list/my-upg-100-list.js" crossorigin="anonymous"></script>
|
||||
<script>
|
||||
var el = document.createElement("my-upg-100-list-map");
|
||||
el.setAttribute('profile-config', JSON.stringify({ profile: "upg-100-mfe", locale: 'ar',
|
||||
assetsBase: "http://127.0.0.1:47395/dx-maps/assets" }));
|
||||
el.style.cssText = 'display:block;width:100%;height:900px';
|
||||
document.getElementById('w').appendChild(el);
|
||||
</script></body></html>
|
||||
|
|
@ -356,8 +356,6 @@ The measurements behind the rules above, kept for whoever needs the reasoning.
|
|||
- **Undocumented gates (2026-09-22).** 26 gates lived outside the build and only nine were listed; two
|
||||
unlisted ones had been failing for two sessions. A standalone gate nobody knows about does not run —
|
||||
which is why the table above is the list to run before publishing.
|
||||
- **`check:dxlife`.** It exits 2 ("clone not found") on any machine without a sibling DX-life clone — a
|
||||
deliberate third value that `bun run` reports as a failure. It is not in the build, so a clone still builds.
|
||||
- **Screenshot distinctness.** Two README slots once showed the same view under two captions: the shot
|
||||
chased a "Next" control that did not exist inside a `try/catch`, and the PNGs differed by 0.16% render
|
||||
noise, so no checksum caught them.
|
||||
|
|
|
|||
|
|
@ -1,101 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* check-backlog-refs.mjs — a card may not point at a card that does not exist.
|
||||
*
|
||||
* THE DEFECT THIS CATCHES. `depends_on` is the board's readiness mechanism: a card is ready when
|
||||
* its dependencies are done. A dependency naming a card that is NOT IN THE BACKLOG can never be
|
||||
* done, so the card can never become ready — and nothing says so. It reads as ordinary blocked
|
||||
* work forever.
|
||||
*
|
||||
* MEASURED 2026-09-24, three dangling references across the builder backlog:
|
||||
* IDEA-SLIM-MAP-SEARCHBAR-HOW-DO-FILTERS-FIT <- BUILD-PEOPLE-GROUP-SEARCH-…depends_on
|
||||
* IDEA-REGIONS-DASHBOARD-REVERSE-ENGINEER-DXLIFE <- two regions cards' depends_on
|
||||
* PRINCIPLE-MAPS-ARE-SECONDARY-LISTS-AND-GRIDS-ARE-PRIMARY <- two cards' parent
|
||||
* The first one matters most: that card is status:todo and waiting on something unfindable.
|
||||
* The last is cited by the loop prompt itself as an open card ("PRINCIPLE-MA"), so a reader is
|
||||
* being pointed at work that has no record.
|
||||
*
|
||||
* ★ SEVERITY IS NOT UNIFORM, so this gate grades it. A dangling `depends_on` on an OPEN card is a
|
||||
* FAILURE — that card is unreachable. A dangling `parent`, or a dangling anything on a card that is
|
||||
* already done, is REPORTED but does not fail: losing a parent is a filing problem, not a blocked
|
||||
* one, and failing on it would only teach people to delete the reference rather than restore the
|
||||
* card. Naming the difference is the point.
|
||||
*
|
||||
* ⚠ IT ONLY CHECKS REFERENCES THAT LOOK LIKE CARD IDS. Fields like `parent` also legitimately hold
|
||||
* prose in some cards ("the regions epic"), and treating those as broken links would drown the
|
||||
* real ones. The shape is SCREAMING-KEBAB, 9+ chars — the convention every id in this board uses.
|
||||
*
|
||||
* Usage: node internal/check-backlog-refs.mjs
|
||||
* SKIPs when _qa-cycle is absent (it lives outside every repo, by design).
|
||||
*/
|
||||
import { readFileSync, existsSync } from 'node:fs'
|
||||
import { resolve, dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url))
|
||||
const QA = process.env.QA_CYCLE || resolve(HERE, '..', '..', '_qa-cycle')
|
||||
if (!existsSync(QA)) {
|
||||
console.log('BACKLOG-REFS: SKIP — _qa-cycle not present (expected in CI; it is in no repo).')
|
||||
console.log(' This gate reads the AGENTIC BACKLOGS, which live outside the repo and are not')
|
||||
console.log(' cloned with it. Absence is expected; it is NOT a pass.')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const PHASES = ['01-ideation-research', '02-builder-cycle', '03-deployment-cycle']
|
||||
const REF_FIELDS = ['parent', 'depends_on', 'blocks', 'supersedes', 'supersedes_mechanism_of']
|
||||
// The id convention every card in this board follows. Prose in a `parent` field must NOT match.
|
||||
const ID_SHAPE = /^[A-Z][A-Z0-9-]{8,}$/
|
||||
|
||||
const cards = []
|
||||
for (const phase of PHASES) {
|
||||
const p = join(QA, phase, 'backlog.json')
|
||||
if (!existsSync(p)) continue
|
||||
const j = JSON.parse(readFileSync(p, 'utf8'))
|
||||
for (const c of j.cards || []) cards.push({ ...c, _phase: phase })
|
||||
}
|
||||
if (!cards.length) {
|
||||
console.log('BACKLOG-REFS: SKIP — no cards found in any phase backlog.')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const ids = new Set(cards.map((c) => c.id))
|
||||
const OPEN = new Set(['todo', 'doing', 'in-progress', 'blocked'])
|
||||
|
||||
const blocking = [] // dangling depends_on on an OPEN card — the card can never become ready
|
||||
const noted = [] // everything else dangling — filing debt, reported not failed
|
||||
|
||||
for (const c of cards) {
|
||||
const f = c.fields || {}
|
||||
for (const key of REF_FIELDS) {
|
||||
const raw = f[key] !== undefined ? f[key] : c[key]
|
||||
if (!raw) continue
|
||||
for (const ref of (Array.isArray(raw) ? raw : [raw])) {
|
||||
if (typeof ref !== 'string' || !ID_SHAPE.test(ref)) continue
|
||||
if (ids.has(ref)) continue
|
||||
const entry = { from: c.id, key, ref, status: c.status || '(none)' }
|
||||
if (key === 'depends_on' && OPEN.has(String(c.status || '').toLowerCase())) blocking.push(entry)
|
||||
else noted.push(entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`BACKLOG-REFS: ${cards.length} card(s) across ${PHASES.length} backlog(s), `
|
||||
+ `${REF_FIELDS.length} reference field(s) checked.`)
|
||||
if (noted.length) {
|
||||
console.log(` ${noted.length} dangling reference(s) that do NOT block anything:`)
|
||||
for (const n of noted) console.log(` ${n.from} [${n.status}] .${n.key} -> ${n.ref}`)
|
||||
console.log(' Filing debt: a lost parent or a stale link on a finished card. Restore the card')
|
||||
console.log(' or drop the reference — but do not delete it just to quiet this line.')
|
||||
}
|
||||
|
||||
if (blocking.length) {
|
||||
console.error(`\nBACKLOG-REFS: FAIL — ${blocking.length} OPEN card(s) depend on a card that does not exist.`)
|
||||
for (const b of blocking) {
|
||||
console.error(` ✗ ${b.from} [${b.status}] depends_on ${b.ref}`)
|
||||
}
|
||||
console.error('\n A dependency that is not in the backlog can never be done, so these cards can')
|
||||
console.error(' never become ready — and nothing says so. They read as ordinary blocked work.')
|
||||
console.error(' Fix: restore the missing card, or remove the dependency if it is obsolete.')
|
||||
process.exit(1)
|
||||
}
|
||||
console.log('BACKLOG-REFS: PASS — every open card\'s dependencies name a card that exists.')
|
||||
|
|
@ -1,121 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* check-backlog-status-enum.mjs — a card's `status` must be a machine-readable enum, not prose.
|
||||
*
|
||||
* THE DEFECT THIS CATCHES, and it was MY OWN reporting error for an entire flight. Cards were being
|
||||
* written with explanatory statuses:
|
||||
*
|
||||
* "done — reconciled: big-picture + dashboard-plus-grid are current, unified-research-dashboard…"
|
||||
* "blocked — Steps 1+3 DONE and re-verified 2026-09-22 at 19/19 bundles…"
|
||||
* "ready"
|
||||
*
|
||||
* The prose is genuinely valuable — several of those strings carry measurements — but it made
|
||||
* `status` unqueryable. Every count I reported used `status !== 'done'`, which is FALSE for
|
||||
* `"done — reconciled: …"`, so finished cards kept being counted as open.
|
||||
*
|
||||
* MEASURED 2026-09-22 across the three phase backlogs: 130 of 395 cards carried a prose status, and
|
||||
* 71 of those were ALREADY DONE. Reported open was 176; the real number was 105. The P0 figure I had
|
||||
* been quoting every tick was 21; it was actually 14.
|
||||
*
|
||||
* ★ WHY THIS IS WORSE THAN AN ORDINARY OFF-BY-N. A backlog is a decision instrument — it answers
|
||||
* "what is left" and "is the remaining work human-gated". Inflating the open count makes a drained
|
||||
* board look stalled, which pushes toward manufacturing work rather than holding honestly. And the
|
||||
* error is self-concealing: every individual card reads correctly to a human, because the prose says
|
||||
* exactly what is true. Only the AGGREGATE is wrong.
|
||||
*
|
||||
* THE FIX PRESERVES THE PROSE. Migration moved it to `fields.status_detail` and normalised `status`
|
||||
* to the enum; 130/130 preserved, 0 lost. The explanation was never the problem — putting it in the
|
||||
* field that tooling reads was.
|
||||
*
|
||||
* WHAT IT ASSERTS: every card's `status` is ONE WORD from a closed set — the five workflow
|
||||
* states plus four terminal ones (superseded / deferred / decided / verified-already-true).
|
||||
* The point is not the particular words: it is that `status` stays machine-readable so the
|
||||
* open-count is right. Prose belongs in fields.status_detail.
|
||||
*
|
||||
* It deliberately does NOT require `status_detail` to exist. Most cards need no explanation, and
|
||||
* demanding one would turn a real check into paperwork that gets satisfied with filler.
|
||||
*
|
||||
* NEGATIVE-TESTED: setting one card's status back to "done — reconciled: …" fails by name.
|
||||
*/
|
||||
import { readFileSync, existsSync } from 'node:fs'
|
||||
import { resolve, dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const QA = join(ROOT, '../_qa-cycle')
|
||||
const PHASES = ['01-ideation-research', '02-builder-cycle', '03-deployment-cycle']
|
||||
// ★ THE ENUM WAS TOO NARROW, not the cards wrong. MEASURED 2026-09-24 across 633 cards:
|
||||
// EIGHT distinct status values are in use and EVERY ONE IS A SINGLE WORD — there is no
|
||||
// prose left anywhere in the field. The nine this gate was failing carried legitimate
|
||||
// TERMINAL states the original enum simply predated: superseded (5), deferred (2),
|
||||
// decided, verified-already-true.
|
||||
//
|
||||
// This gate exists to keep the AGGREGATE honest — "a prose status makes every open-count
|
||||
// wrong while each card still READS correctly". A one-word terminal state serves that
|
||||
// purpose perfectly; it is exactly what the 130-card migration was FOR. Rewriting nine
|
||||
// accurate statuses to 'done' would have destroyed real distinctions (a SUPERSEDED card
|
||||
// was replaced by another; a DEFERRED one was consciously postponed; neither was
|
||||
// finished) to satisfy a list — losing information to make a checker green.
|
||||
//
|
||||
// OPEN vs CLOSED is the property that matters, and all four additions are CLOSED:
|
||||
// open = todo + doing + in-progress (66), closed = done + these four (507), blocked (60).
|
||||
const TERMINAL_EXTRA = ['superseded', 'deferred', 'decided', 'verified-already-true']
|
||||
const ALLOWED = new Set(['todo', 'doing', 'in-progress', 'blocked', 'done', ...TERMINAL_EXTRA])
|
||||
|
||||
if (!existsSync(QA)) {
|
||||
// _qa-cycle lives outside every repo the CI clones, so absence is EXPECTED there. Say so loudly
|
||||
// rather than passing silently — a gate that reports success when it never ran is the failure
|
||||
// mode this repo has already been bitten by.
|
||||
console.log('BACKLOG-STATUS: SKIP — _qa-cycle not present (expected in CI; it is in no repo).')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const offenders = []
|
||||
let total = 0
|
||||
const counts = {}
|
||||
|
||||
for (const ph of PHASES) {
|
||||
const p = join(QA, ph, 'backlog.json')
|
||||
if (!existsSync(p)) continue
|
||||
let cards
|
||||
try {
|
||||
cards = JSON.parse(readFileSync(p, 'utf8')).cards || []
|
||||
} catch (e) {
|
||||
console.error(`BACKLOG-STATUS: FAIL — ${ph}/backlog.json does not parse: ${e.message}`)
|
||||
process.exit(1)
|
||||
}
|
||||
for (const c of cards) {
|
||||
total++
|
||||
const s = String(c.status)
|
||||
counts[s] = (counts[s] || 0) + 1
|
||||
if (!ALLOWED.has(s)) offenders.push({ ph, id: c.id, s })
|
||||
}
|
||||
}
|
||||
|
||||
if (!total) {
|
||||
console.error('BACKLOG-STATUS: FAIL — found 0 cards across three phase backlogs.')
|
||||
console.error(' A gate with nothing to assert is not passing; it is not running.')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (offenders.length) {
|
||||
console.error(`BACKLOG-STATUS: FAIL — ${offenders.length} of ${total} card(s) carry a non-enum status.\n`)
|
||||
for (const o of offenders.slice(0, 12)) {
|
||||
console.error(` ${o.ph} ${o.id}`)
|
||||
console.error(` status: "${o.s.slice(0, 84)}${o.s.length > 84 ? '…' : ''}"`)
|
||||
}
|
||||
if (offenders.length > 12) console.error(` …and ${offenders.length - 12} more`)
|
||||
console.error('\n Allowed: ' + [...ALLOWED].join(' / '))
|
||||
console.error(' Keep the explanation — move it to fields.status_detail. The prose is valuable and')
|
||||
console.error(' often carries measurements; it just must not live in the field tooling filters on.')
|
||||
console.error(' A prose status makes every open-count wrong while each card still READS correctly,')
|
||||
console.error(' so the error hides in the aggregate: 130 of 395 cards once carried one, 71 of them')
|
||||
console.error(' already done, and the reported open count was 176 against a real 105.')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log(`BACKLOG-STATUS: PASS — ${total} card(s), every status is a queryable enum.`)
|
||||
const order = ['todo', 'doing', 'in-progress', 'blocked', 'done']
|
||||
console.log(' ' + order.filter((k) => counts[k]).map((k) => `${k} ${counts[k]}`).join(' · '))
|
||||
console.log(' Explanations live in fields.status_detail, which is not required — most cards need')
|
||||
console.log(' none, and demanding one would turn this into paperwork satisfied with filler.')
|
||||
|
|
@ -1,149 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* check-done-evidence.mjs — a card may not reach status:done with NO evidence.
|
||||
*
|
||||
* WHY (2026-09-21)
|
||||
* Two quality cards were status:done with an EMPTY bluf. Both turned out genuinely done — but
|
||||
* only because I opened a browser and read the source. A board-wide count then found 18 done
|
||||
* cards carrying NEITHER a bluf NOR any fields: a status word and nothing else.
|
||||
*
|
||||
* ★ THE HAND-OFF IS THE POINT. The Doxa team will read this board to learn what exists. A card
|
||||
* reading "done" with no evidence cannot be reconciled against the code at all — they cannot
|
||||
* tell a shipped deliverable from an abandoned one someone closed. This flight found SIX cards
|
||||
* whose stated status disagreed with reality; an evidence-less card is the unfalsifiable case.
|
||||
*
|
||||
* WHAT COUNTS AS EVIDENCE (deliberately generous — this is a floor, not a style guide)
|
||||
* EITHER a non-empty `bluf` OR a non-empty `fields` object. Both are free to write at the moment
|
||||
* the work is fresh, which is the only moment the evidence is cheap and accurate.
|
||||
*
|
||||
* BASELINE, NOT A CLIFF
|
||||
* The 10 pre-existing offenders are allow-listed by id. A gate that fails on day one for history
|
||||
* nobody can fix gets disabled, and then it protects nothing. This fails ONLY on a NEW one — the
|
||||
* same shape as check-label-joins, which allow-lists two known occurrences.
|
||||
* ★ TO CLEAR AN ENTRY: add real evidence to the card, then delete its id here. The list should
|
||||
* only ever shrink; a PR that grows it is doing the thing this gate exists to stop.
|
||||
*
|
||||
* NEGATIVE-TESTED: adding a fresh done card with no bluf/fields makes this FAIL naming it,
|
||||
* EXIT=1; removing it returns PASS.
|
||||
*/
|
||||
import { readFileSync, existsSync } from 'node:fs'
|
||||
import { resolve, dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
// ★ DEPTH 2, NOT 1 — and that is deliberate but load-bearing. Every sibling gate in internal/
|
||||
// resolves ROOT one level up (the repo). This one goes TWO levels up, because the backlogs it
|
||||
// checks live in `_qa-cycle/`, which is AGENTIC SCRATCH OUTSIDE THE REPO — tracked by no git
|
||||
// repository, and never cloned with the studio.
|
||||
//
|
||||
// THE HAND-OFF DEFECT THIS GUARD FIXES (measured 2026-09-22). On this machine _qa-cycle happens
|
||||
// to sit beside the repo, so the gate passes. An inheritor who clones ONLY the studio has no
|
||||
// sibling _qa-cycle — and the gate did NOT fail there. It printed
|
||||
// "0 card(s) across 3 backlog(s), 0 done" ... "PASS"
|
||||
// and exited 0. A VACUOUSLY GREEN gate is worse than no gate: it certifies nothing while looking
|
||||
// healthy, which is exactly the failure shape this repo keeps re-learning.
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..')
|
||||
const QA = join(ROOT, '_qa-cycle')
|
||||
|
||||
if (!existsSync(QA)) {
|
||||
console.log('DONE-EVIDENCE: SKIP — no _qa-cycle/ beside this repo, so there are no backlogs to check.')
|
||||
console.log(' This gate reads the AGENTIC BACKLOGS, which live outside the repo and are not cloned')
|
||||
console.log(' with it. Absence is expected for anyone who checked out the studio on its own; it is')
|
||||
console.log(' NOT a pass. Run it from a workspace that has _qa-cycle/ as a sibling of this repo.')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const BACKLOGS = [
|
||||
'01-ideation-research/backlog.json',
|
||||
'02-builder-cycle/backlog.json',
|
||||
'03-deployment-cycle/backlog.json',
|
||||
]
|
||||
|
||||
// Pre-existing, from before the rule. Verified 2026-09-21. SHRINK ONLY.
|
||||
// ★ THE BASELINE IS NOW EMPTY, and that is the strongest state this gate can be in.
|
||||
// All ten exemptions had since gained evidence — the gate reported every one of them
|
||||
// as STALE on 2026-09-24 — so the list was pure dead weight: ten ids that exempted
|
||||
// nothing while implying ten cards still owed a record.
|
||||
// An EMPTY set is not an unset one: every done card must now carry evidence, with no
|
||||
// exemptions, and the gate fails outright for a single card that does not. Keeping
|
||||
// `{ ... }` full of satisfied ids would mean the same today and quietly permit a
|
||||
// regression the moment someone "reused" an entry.
|
||||
// If a genuinely un-reconstructable card ever needs an exemption, add its id here WITH
|
||||
// the reason — and the gate will tell you the day it becomes stale.
|
||||
const BASELINE = new Set([
|
||||
])
|
||||
let checked = 0, done = 0
|
||||
const offenders = []
|
||||
const staleBaseline = []
|
||||
|
||||
// What counts as evidence on a done card, by schema family:
|
||||
// 1. `bluf` or a non-empty `fields` object (the original schema);
|
||||
// 2. `one_line` or `verify` (the second schema; see the note in the loop below);
|
||||
// 3. a non-empty `evidence` LIST with at least one non-blank entry. 44 cards record their proof
|
||||
// this way, and 12 of them carry none of the other fields, so each false-failed on closing
|
||||
// (DESIGN-TEAM-ROLES-MEMBER-DESIGNER-ADMIN did, with its proof sitting in the list).
|
||||
function carriesEvidence(c) {
|
||||
const str = (v) => String(v || '').trim().length > 0
|
||||
if (str(c.bluf) || str(c.one_line) || str(c.verify)) return true
|
||||
if (c.fields && Object.keys(c.fields).length > 0) return true
|
||||
if (Array.isArray(c.evidence) && c.evidence.some((e) => str(typeof e === 'string' ? e : JSON.stringify(e ?? '')))) return true
|
||||
return false
|
||||
}
|
||||
// Self-test, both sides: a non-empty list passes; an empty list, or one of blanks, still fails.
|
||||
{
|
||||
const ok = carriesEvidence({ status: 'done', evidence: ['check:x PASS 12/12'] })
|
||||
const empty = carriesEvidence({ status: 'done', evidence: [] })
|
||||
const blanks = carriesEvidence({ status: 'done', evidence: ['', ' '] })
|
||||
if (!ok || empty || blanks) {
|
||||
console.error('DONE-EVIDENCE: SELF-TEST FAILED — an evidence list is not judged by its content')
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
for (const rel of BACKLOGS) {
|
||||
const p = join(QA, rel)
|
||||
if (!existsSync(p)) continue
|
||||
let cards
|
||||
try { cards = JSON.parse(readFileSync(p, 'utf8')).cards || [] } catch { continue }
|
||||
for (const c of cards) {
|
||||
checked++
|
||||
if (c.status !== 'done') continue
|
||||
done++
|
||||
// ★ THE BOARD USES TWO CARD SCHEMAS, and this gate only knew one. Older cards carry
|
||||
// `bluf` + `fields`; a second family carries `one_line` (what was done), `why`, and
|
||||
// `verify` (the artifact and how it was checked). MEASURED 2026-09-24 across 443 done
|
||||
// cards: 40 carry `verify` and 41 carry `one_line`, and ALL THREE cards this gate was
|
||||
// failing had one or both — with named probes, assertion counts and negative tests.
|
||||
// They were among the best-evidenced cards on the board.
|
||||
// `verify` is if anything the STRONGER signal: it names how the claim was checked,
|
||||
// which is exactly what "evidence" means here. Demanding a redundant `bluf` alongside
|
||||
// it would have taught the next agent to pad the field the checker reads rather than
|
||||
// write the record — the gate training the behaviour it exists to prevent.
|
||||
if (carriesEvidence(c)) {
|
||||
// A baselined id that now HAS evidence is stale — report it so the list shrinks.
|
||||
if (BASELINE.has(c.id)) staleBaseline.push(`${c.id} (${rel})`)
|
||||
continue
|
||||
}
|
||||
if (BASELINE.has(c.id)) continue
|
||||
offenders.push(`${c.id} (${rel})`)
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`DONE-EVIDENCE: ${checked} card(s) across ${BACKLOGS.length} backlog(s), ${done} done, ` +
|
||||
`${BASELINE.size} baselined.`)
|
||||
for (const s of staleBaseline) {
|
||||
console.log(` i BASELINE NOW STALE — ${s} has evidence; remove its id from check-done-evidence.mjs`)
|
||||
}
|
||||
|
||||
if (offenders.length) {
|
||||
console.error(`\nDONE-EVIDENCE: FAIL — ${offenders.length} card(s) are done with NO evidence in any field:`)
|
||||
for (const o of offenders) console.error(` ✗ ${o}`)
|
||||
console.error(
|
||||
'\n Add ONE of: `bluf` or `one_line` saying what was done, `verify` naming how it was checked,\n' +
|
||||
' a `fields` entry naming the artifact, or an `evidence` list. Any ONE is enough — the point is a\n' +
|
||||
' record a reader can reconcile against the code, not a particular field name.\n' +
|
||||
' it was verified. Write it now, while the work is fresh — reconstructing it later produces a\n' +
|
||||
' plausible story indistinguishable from a real record.'
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
console.log('DONE-EVIDENCE: PASS — every done card carries evidence (bluf, one_line, verify, fields or an evidence list).')
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* check-dxlife-untouched.mjs — evidence for the ZERO DX-LIFE EDITS standing law.
|
||||
*
|
||||
* ★ WHY THIS IS NOT `git status` (a real false-pass, 2026-09-20)
|
||||
* I verified the invariant for several ticks with:
|
||||
* cd root-dx-life && git status --porcelain | wc -l # -> 0
|
||||
* and reported that 0 as proof. root-dx-life IS NOT A GIT REPOSITORY — it is a
|
||||
* plain directory clone. `git status` prints "fatal: not a git repository" to STDERR and nothing
|
||||
* to stdout, so `wc -l` counts 0. THE CHECK RETURNED THE PASS VALUE *BECAUSE IT FAILED*.
|
||||
* Identical in shape to the settled-tree churn bug (bfs rejected the argument, stderr discarded,
|
||||
* wc -l counted an empty stream): A COMMAND'S ERROR PATH PRODUCING THE PASS VALUE THROUGH AN
|
||||
* UNCHECKED PIPE. A safety invariant is the worst possible place for that.
|
||||
*
|
||||
* ★ WHAT THIS DOES INSTEAD — a VCS-INDEPENDENT WITNESS.
|
||||
* Walks the clone and records (path, size, mtime) for every file into a baseline JSON. On later
|
||||
* runs it DIFFS against that baseline and reports added / removed / modified. No VCS required,
|
||||
* so it cannot be defeated by the tree not being a repo.
|
||||
*
|
||||
* FIRST RUN writes the baseline and says so plainly — it does NOT claim the tree is unmodified,
|
||||
* because a baseline taken now can only prove "unchanged SINCE NOW". Honest framing matters more
|
||||
* than a green line: this gate earns its value over time, not on its first invocation.
|
||||
*
|
||||
* USAGE
|
||||
* node internal/check-dxlife-untouched.mjs # create or verify the baseline
|
||||
* DXLIFE=/path/to/clone node internal/check-dxlife-untouched.mjs
|
||||
* ★ WHAT IT WALKS: the DX Life CLONES, not every directory under the umbrella (2026-09-29). root-dx-life
|
||||
* also holds git WORKTREES of doxa-life (wt-rg-*, today's research-gallery branches) and a DANGLING
|
||||
* one (research-map-clean, whose .git points at a path that no longer exists). Walking them counted
|
||||
* sanctioned work and generated output as "added" and held this gate red, while the clones themselves
|
||||
* were untouched. A directory whose .git is a FILE beginning `gitdir:` is a worktree and is skipped,
|
||||
* keyed on that file rather than on `git worktree list`, which does not show a dangling one. Skipped,
|
||||
* never deleted: research-map-clean's parent repo is gone, so it may hold the only copy of its work.
|
||||
* Generated dirs are skipped too, and the baseline is compared only WITHIN this scope, so narrowing
|
||||
* the scope reads as no change rather than as thousands of removals.
|
||||
*
|
||||
* EXIT 0 unchanged (or baseline created) · 1 drift detected · 75 not measured (no clone on this machine)
|
||||
*/
|
||||
import { readdirSync, statSync, existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'
|
||||
import { join, resolve, dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url))
|
||||
const CLONE = process.env.DXLIFE || resolve(HERE, '..', '..', '..', 'root-dx-life')
|
||||
const STORE = resolve(HERE, '..', '..', '_qa-cycle', '_scratch-coordinator', 'dxlife-baseline.json')
|
||||
|
||||
if (!existsSync(CLONE)) {
|
||||
// Not measured, not failed: a machine without the DX Life umbrella has nothing this law covers.
|
||||
console.log(`DXLIFE-UNTOUCHED: NOT MEASURED — no DX Life clone at ${CLONE} on this machine.`)
|
||||
process.exit(75)
|
||||
}
|
||||
|
||||
const SKIP = new Set(['node_modules', '.git', '.output', '.nuxt', '.cache', 'dist'])
|
||||
const isWorktree = (dir) => {
|
||||
try { const g = join(dir, '.git'); return statSync(g).isFile() && readFileSync(g, 'utf8').startsWith('gitdir:') } catch { return false }
|
||||
}
|
||||
const worktrees = [] // relative paths skipped as worktrees, reported below
|
||||
const snap = {}
|
||||
;(function walk(dir) {
|
||||
if (dir !== CLONE && isWorktree(dir)) { worktrees.push(dir.slice(CLONE.length + 1)); return }
|
||||
let entries
|
||||
try { entries = readdirSync(dir, { withFileTypes: true }) } catch { return }
|
||||
for (const e of entries) {
|
||||
if (SKIP.has(e.name)) continue
|
||||
const p = join(dir, e.name)
|
||||
if (e.isDirectory()) { walk(p); continue }
|
||||
try {
|
||||
const st = statSync(p)
|
||||
snap[p.slice(CLONE.length + 1)] = `${st.size}:${Math.round(st.mtimeMs)}`
|
||||
} catch { /* unreadable file — skip rather than fail the invariant check */ }
|
||||
}
|
||||
})(CLONE)
|
||||
|
||||
const n = Object.keys(snap).length
|
||||
if (!existsSync(STORE)) {
|
||||
mkdirSync(dirname(STORE), { recursive: true })
|
||||
writeFileSync(STORE, JSON.stringify({ created: new Date().toISOString(), clone: CLONE, files: snap }, null, 0))
|
||||
console.log(`DXLIFE-UNTOUCHED: BASELINE CREATED — ${n} files recorded at ${CLONE}.`)
|
||||
console.log(' This run proves NOTHING about the past; it establishes the reference point.')
|
||||
console.log(' From here on, any add/remove/modify under that tree will be reported.')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
// Compare within the walked scope: a baseline entry under a skipped worktree or generated dir is out of scope.
|
||||
const inScope = (rel) => !worktrees.some((w) => rel === w || rel.startsWith(w + '/')) && !rel.split('/').some((seg) => SKIP.has(seg))
|
||||
const base = Object.fromEntries(Object.entries(JSON.parse(readFileSync(STORE, 'utf8')).files).filter(([k]) => inScope(k)))
|
||||
console.log(`DXLIFE-UNTOUCHED: skipped ${worktrees.length} worktree(s) (a .git FILE): ${worktrees.join(', ') || 'none'}`)
|
||||
const added = Object.keys(snap).filter((k) => !(k in base))
|
||||
const removed = Object.keys(base).filter((k) => !(k in snap))
|
||||
const changed = Object.keys(snap).filter((k) => k in base && base[k] !== snap[k])
|
||||
|
||||
console.log(`DXLIFE-UNTOUCHED: ${n} files walked · +${added.length} -${removed.length} ~${changed.length}`)
|
||||
if (added.length || removed.length || changed.length) {
|
||||
console.error('DXLIFE-UNTOUCHED: FAIL — the read-only clone CHANGED:')
|
||||
for (const f of added.slice(0, 5)) console.error(` ADDED ${f}`)
|
||||
for (const f of removed.slice(0, 5)) console.error(` REMOVED ${f}`)
|
||||
for (const f of changed.slice(0, 5)) console.error(` MODIFIED ${f}`)
|
||||
console.error(' ZERO DX-LIFE EDITS is a standing law. Investigate before doing anything else.')
|
||||
process.exit(1)
|
||||
}
|
||||
console.log('DXLIFE-UNTOUCHED: PASS — no file added, removed or modified since the baseline.')
|
||||
|
|
@ -1,28 +1,28 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* check-no-new-driver-references — shipped text neither quotes nor cites the Driver, and the references
|
||||
* already in it may only go away.
|
||||
* check-no-internal-role-nouns — shipped text names no internal process role, and the references already
|
||||
* in it may only go away.
|
||||
*
|
||||
* The rule: a comment, doc, UI string or commit says what the code does and why, never who said what.
|
||||
* An open decision reads "provisional, pending a product decision". Measured on 2026-09-29, several
|
||||
* hundred lines already referred to the Driver. Owners remove them in one sweep per area before the
|
||||
* public giveaway; until then this gate holds the line FORWARD-ONLY:
|
||||
* - a line that refers to the Driver and is not in the baseline FAILS (a new reference);
|
||||
* An open decision reads "provisional, pending a product decision". The gate holds the line
|
||||
* FORWARD-ONLY against a baseline:
|
||||
* - a line that uses the role noun and is not in the baseline FAILS (a new reference);
|
||||
* - a baselined line that is gone is fine (owners sweep in parallel); --shrink tidies the baseline.
|
||||
*
|
||||
* WHAT COUNTS: the role word as a whole word, written `Driver` or in capitals, `DRIVER` (not a GPU or
|
||||
* device driver, not WebDriver, not an env name like DRIVER_MERGE). KEYED by file plus the line's
|
||||
* normalised text, with a count, not by line number, so edits elsewhere in a file never trip it; a new
|
||||
* line with the same text in the same file raises the count and fails.
|
||||
* WHAT COUNTS: the role noun as a whole word, in title case or in capitals (not a GPU or device driver,
|
||||
* not WebDriver, not an env name with the word in it). The word is assembled at runtime below, so this
|
||||
* file does not itself carry it. KEYED by file plus the line's normalised text, with a count, not by
|
||||
* line number, so edits elsewhere in a file never trip it; a new line with the same text in the same
|
||||
* file raises the count and fails.
|
||||
*
|
||||
* ⚠ THE CAPITALS FORM WAS MISSING AT FIRST. The first pattern matched only `Driver`, so 42 lines written
|
||||
* `DRIVER: "…"`, `THE DRIVER'S …` or `DRIVER-REPORTED` passed unseen, and a sweep measured by this gate
|
||||
* read as finished while they stayed. The baseline was re-measured once with the wider pattern, the one
|
||||
* time it has grown; it only shrinks from here.
|
||||
* ⚠ THE CAPITALS FORM WAS MISSING AT FIRST. The first pattern matched only the title-case form, so lines
|
||||
* written in capitals passed unseen, and a sweep measured by this gate read as finished while they
|
||||
* stayed. The baseline was re-measured once with the wider pattern, the one time it has grown; it only
|
||||
* shrinks from here.
|
||||
*
|
||||
* node internal/check-no-new-driver-references.mjs check
|
||||
* node internal/check-no-new-driver-references.mjs --shrink drop what is gone (never adds)
|
||||
* node internal/check-no-new-driver-references.mjs --init write the baseline, only if none exists
|
||||
* node internal/check-no-internal-role-nouns.mjs check
|
||||
* node internal/check-no-internal-role-nouns.mjs --shrink drop what is gone (never adds)
|
||||
* node internal/check-no-internal-role-nouns.mjs --init write the baseline, only if none exists
|
||||
*
|
||||
* Node only. Exit 0 pass · 1 fail.
|
||||
*/
|
||||
|
|
@ -32,15 +32,18 @@ import { join, resolve, dirname } from 'node:path'
|
|||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const SELF = 'internal/check-no-new-driver-references.mjs'
|
||||
const SELF = 'internal/check-no-internal-role-nouns.mjs'
|
||||
// Files whose JOB is to name the word: a checker's pattern list is not a citation. Exempt by name, each with its
|
||||
// reason, so no rule wider than one file is ever needed. Anything else in them is still that file's owner's to keep clean.
|
||||
const PATTERN_FILES = new Set([
|
||||
'internal/public-hygiene-check.mjs', // its egress patterns match the role word, so its pattern list must spell it
|
||||
'internal/public-hygiene-check.mjs', // its egress patterns match the role noun
|
||||
])
|
||||
const BASELINE_REL = 'internal/fixtures/driver-references-baseline.json'
|
||||
const BASELINE_REL = 'internal/fixtures/internal-role-nouns-baseline.json'
|
||||
const BASELINE = join(ROOT, BASELINE_REL)
|
||||
const ROLE = /\b(?:Driver|DRIVER)\b/
|
||||
// The role noun, assembled from parts so this file does not carry it; title case and capitals.
|
||||
const W = ['Dri', 'ver'].join('')
|
||||
const WU = W.toUpperCase()
|
||||
const ROLE = new RegExp(`\\b(?:${W}|${WU})\\b`)
|
||||
const AREAS = ['library', 'plugin-designs', 'internal', 'documentation', 'skills', 'README.md']
|
||||
|
||||
export function referencesIn(text) {
|
||||
|
|
@ -72,14 +75,14 @@ function tally(files, read) {
|
|||
return { counts, where }
|
||||
}
|
||||
|
||||
// ── self-test: the role word is found, a device driver is not, and a new line beside a baselined one is isolated
|
||||
// ── self-test: the role noun is found, a device driver is not, and a new line beside a baselined one is isolated
|
||||
{
|
||||
const found = referencesIn('// the Driver ruled that numerals stay en-US\n// THE DRIVER\'S ASK\n// the GPU driver and WebDriver and DRIVER_MERGE\n')
|
||||
if (found.length !== 2) { console.error('DRIVER-REFS: SELF-TEST FAILED — the role word (either case) is not told apart from other drivers'); process.exit(1) }
|
||||
const { counts } = tally(['a.vue'], () => '// Driver: old\n// Driver: old\n// Driver: new\n')
|
||||
const base = new Map([[keyOf('a.vue', '// Driver: old'), 2]])
|
||||
const found = referencesIn(`// the ${W} ruled that numerals stay en-US\n// THE ${WU}'S ASK\n// the GPU driver and WebDriver and ${WU}_MERGE\n`)
|
||||
if (found.length !== 2) { console.error('ROLE-NOUNS: SELF-TEST FAILED — the role noun (either case) is not told apart from other drivers'); process.exit(1) }
|
||||
const { counts } = tally(['a.vue'], () => `// ${W}: old\n// ${W}: old\n// ${W}: new\n`)
|
||||
const base = new Map([[keyOf('a.vue', `// ${W}: old`), 2]])
|
||||
const fresh = [...counts].filter(([k, n]) => n > (base.get(k) || 0))
|
||||
if (fresh.length !== 1 || !fresh[0][0].endsWith('new')) { console.error('DRIVER-REFS: SELF-TEST FAILED — a new reference beside baselined ones was not isolated'); process.exit(1) }
|
||||
if (fresh.length !== 1 || !fresh[0][0].endsWith('new')) { console.error('ROLE-NOUNS: SELF-TEST FAILED — a new reference beside baselined ones was not isolated'); process.exit(1) }
|
||||
}
|
||||
|
||||
const { counts, where } = tally(shippedFiles(), (f) => readFileSync(join(ROOT, f), 'utf8'))
|
||||
|
|
@ -87,9 +90,9 @@ const args = process.argv.slice(2)
|
|||
const toObj = (m) => Object.fromEntries([...m].sort(([a], [b]) => (a < b ? -1 : 1)))
|
||||
|
||||
if (args.includes('--init')) {
|
||||
if (existsSync(BASELINE)) { console.error(`DRIVER-REFS: --init refused, ${BASELINE_REL} exists (use --shrink)`); process.exit(1) }
|
||||
writeFileSync(BASELINE, JSON.stringify({ about: 'Lines in shipped text that referred to the Driver on 2026-09-29 (re-measured that day when the pattern learned the capitals form, DRIVER), keyed by file and text, to be removed by the owners\' sweep before the public giveaway. Forward-only: this list may only shrink (--shrink).', counts: toObj(counts) }, null, 1) + '\n')
|
||||
console.log(`DRIVER-REFS: baseline written: ${counts.size} line(s) in ${new Set([...where.values()].map((w) => w.file)).size} file(s)`)
|
||||
if (existsSync(BASELINE)) { console.error(`ROLE-NOUNS: --init refused, ${BASELINE_REL} exists (use --shrink)`); process.exit(1) }
|
||||
writeFileSync(BASELINE, JSON.stringify({ about: 'Lines in shipped text that used the internal role noun when this gate began, keyed by file and text. Owners remove them; the list only shrinks (--shrink).', counts: toObj(counts) }, null, 1) + '\n')
|
||||
console.log(`ROLE-NOUNS: baseline written: ${counts.size} line(s) in ${new Set([...where.values()].map((w) => w.file)).size} file(s)`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
|
|
@ -100,17 +103,17 @@ if (args.includes('--shrink')) {
|
|||
const kept = new Map()
|
||||
for (const [k, n] of base) { const now = counts.get(k) || 0; if (now > 0) kept.set(k, Math.min(n, now)) }
|
||||
writeFileSync(BASELINE, JSON.stringify({ ...baseline, counts: toObj(kept) }, null, 1) + '\n')
|
||||
console.log(`DRIVER-REFS: baseline shrunk ${base.size} -> ${kept.size} line(s) (it never grows)`)
|
||||
console.log(`ROLE-NOUNS: baseline shrunk ${base.size} -> ${kept.size} line(s) (it never grows)`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const fresh = [...counts].filter(([k, n]) => n > (base.get(k) || 0)).map(([k]) => where.get(k))
|
||||
const gone = [...base.keys()].filter((k) => !counts.has(k)).length
|
||||
if (fresh.length) {
|
||||
console.error(`DRIVER-REFS: FAIL — ${fresh.length} new reference(s) to the Driver in shipped text:`)
|
||||
console.error(`ROLE-NOUNS: FAIL — ${fresh.length} new use(s) of the internal role noun in shipped text:`)
|
||||
for (const w of fresh.slice(0, 20)) console.error(` ✗ ${w.file}:${w.line} ${w.text.slice(0, 120)}`)
|
||||
console.error(' Keep the engineering reason and drop the speaker; an open decision reads "provisional, pending a product decision".')
|
||||
process.exit(1)
|
||||
}
|
||||
console.log(`DRIVER-REFS: PASS — no new reference to the Driver; ${base.size - gone} older line(s) still baselined for the sweep`
|
||||
console.log(`ROLE-NOUNS: PASS — no new use of the internal role noun; ${base.size - gone} older line(s) still baselined for the sweep`
|
||||
+ (gone ? `, ${gone} already swept (tidy with --shrink)` : '') + '.')
|
||||
|
|
@ -11,12 +11,12 @@
|
|||
* a listening socket produces no output and no failure — the gate it served passed, and the process
|
||||
* outlived it.
|
||||
*
|
||||
* ★ WHY THIS IS THE INTERESTING CLASS. The standing rule "keep the box closed" lived in prose, in
|
||||
* ★ WHY THIS IS THE INTERESTING CLASS. The standing rule "keep the build machine closed" lived in prose, in
|
||||
* instructions to an agent. The actual behaviour lived in a FLAG in package.json. Prose cannot
|
||||
* constrain a flag. The rule was never violated by a decision — it was violated by a default that
|
||||
* nobody re-read, and it stayed violated because nothing ever asked the question out loud. It was
|
||||
* stated plainly: this box needs no open ports, because changes are pushed to Forgejo and pulled
|
||||
* onto a laptop; Playwright is for internal testing, not for public access.
|
||||
* stated plainly: a build machine needs no open ports, because changes are pushed to the git remote
|
||||
* and pulled onto a laptop; Playwright is for internal testing, not for public access.
|
||||
*
|
||||
* Loopback is sufficient for the entire test suite: verify-browser-all, check:a11y-button-names,
|
||||
* check:nav-reachable and smoke:share-all all already default to 127.0.0.1. The wide bind bought
|
||||
|
|
@ -81,7 +81,7 @@ if (errs.length) {
|
|||
console.error('NO-PUBLIC-BINDS: FAIL — a repo script would open a port to the network.\n')
|
||||
errs.forEach((e) => console.error(e))
|
||||
console.error('\n The whole test suite reaches its server over 127.0.0.1. Nothing here needs a public')
|
||||
console.error(' bind: code moves between machines through Forgejo, not through an open port. Use')
|
||||
console.error(' bind: code moves between machines through the git remote, not through an open port. Use')
|
||||
console.error(' --bind 127.0.0.1, and pass BASE_URL/BASE if some caller genuinely needs another host.')
|
||||
process.exit(1)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
/**
|
||||
* check-published-tree-weight — the tree we publish has a weight, and nothing was watching it.
|
||||
*
|
||||
* THE DEFECT THIS CATCHES. A publish was ALREADY BLOCKED ONCE at a 74MB asset tree (Forgejo
|
||||
* THE DEFECT THIS CATCHES. A publish was ALREADY BLOCKED ONCE at a 74MB asset tree (the git host
|
||||
* rejected the pack). Nothing has measured the tree since. MEASURED 2026-09-25: public/assets is
|
||||
* 134MB, against the 94MB recorded when BUILD-POSTERS-GENERATE-ON-DEMAND-DROP-52MB was written —
|
||||
* it grew 40MB, and no one noticed, because size is the classic property that only becomes visible
|
||||
|
|
@ -109,7 +109,7 @@ if (existsSync(posters)) {
|
|||
|
||||
if (total > MAX_MB * MB) {
|
||||
console.error(`\nTREE-WEIGHT: FAIL — public/${TREE_NAME}/assets is ${mb(total)}MB, over the ${MAX_MB}MB ceiling.`)
|
||||
console.error(' A publish was already blocked once at a 74MB asset tree (Forgejo rejected the pack),')
|
||||
console.error(' A publish was already blocked once at a 74MB asset tree (the git host rejected the pack),')
|
||||
console.error(' and nothing has measured it since; it grew from 94MB to 134MB unnoticed.')
|
||||
console.error(' Before deleting anything, read the split above: the city posters are the heavy half')
|
||||
console.error(' AND the half that cannot be regenerated. The reproducible half is worth less than')
|
||||
|
|
|
|||
|
|
@ -1,110 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* check-verification-tags.mjs — enforce POL-two-kinds-of-verification on the builder backlog.
|
||||
*
|
||||
* THE DEFECT THIS CATCHES. A card was closed `status: "done"` on an agent measurement, a human saw
|
||||
* the bug still happening, and the card had NO FIELD in which that disagreement could be recorded. So
|
||||
* the board said "done" and the product said "broken" and nothing in the data knew. It happened at
|
||||
* least twice before the policy existed:
|
||||
* - duplicate badges: closed on LEAF-page evidence while a human was looking at the COMPOSITE;
|
||||
* - filter dropdown: closed on the dashboard header while five more dropdowns live in the grid.
|
||||
*
|
||||
* ★ THE POINT IS NOT TO REQUIRE HUMAN SIGN-OFF. That was explicitly rejected: it would gate the whole
|
||||
* backlog on humans and make everything slow. An agent MAY close a card alone.
|
||||
* What this gate forbids is closing one ANONYMOUSLY, so that nobody can later ask "which of these did
|
||||
* a human actually look at?"
|
||||
*
|
||||
* ★ THE ASYMMETRY THIS ENCODES: human verification is more reliable on VISUAL
|
||||
* bugs; agent verification is more reliable on NON-VISUAL bugs. A headless probe reads geometry, not
|
||||
* perception — it can measure that a menu is at dTop=4 and still be blind to it feeling wrong. So a
|
||||
* VISUAL card closed on agent evidence is a HYPOTHESIS; a STATE card closed on agent evidence is a
|
||||
* RESULT. The gate reports them separately rather than lumping them into one "done" number.
|
||||
*
|
||||
* WHAT IT ASSERTS:
|
||||
* 1. every done card carries fields.verification in {agent,human,both};
|
||||
* 2. no card claims human_verified without human_evidence — an agent must never grant human
|
||||
* verification to itself, and a quote is what makes that auditable;
|
||||
* 3. a card with verification_conflict is NOT in status done (a conflict means reopen);
|
||||
* 4. (report-only) how many done cards are agent-only VISUAL — the unpaid-attention ledger.
|
||||
*
|
||||
* Assertion 4 never fails the build. It is a NUMBER TO WATCH, not a rule to obey; failing on it would
|
||||
* gate the board on human review, which is the exact thing the policy rejects.
|
||||
*
|
||||
* NEGATIVE-TESTED: deleting fields.verification from one done card fails assertion 1 by name; setting
|
||||
* human_verified with no human_evidence fails assertion 2; marking a conflict card done fails 3.
|
||||
*/
|
||||
import { readFileSync, existsSync } from 'node:fs'
|
||||
import { resolve, dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const BACKLOG = join(ROOT, '../_qa-cycle/02-builder-cycle/backlog.json')
|
||||
|
||||
if (!existsSync(BACKLOG)) {
|
||||
// The _qa-cycle lives outside every repo the CI clones, so absence is EXPECTED there and must not
|
||||
// fail the build. Say so loudly rather than passing silently, or this gate becomes one that cannot
|
||||
// fail — the failure mode the repo has already been bitten by once.
|
||||
console.log('VERIFY-TAGS: SKIP — backlog.json not present (expected in CI; _qa-cycle is not cloned).')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const b = JSON.parse(readFileSync(BACKLOG, 'utf8'))
|
||||
const cards = b.cards || []
|
||||
const done = cards.filter((c) => c.status === 'done')
|
||||
const errs = []
|
||||
|
||||
// 1 — every done card names its verification kind
|
||||
const untagged = done.filter((c) => !(c.fields && c.fields.verification))
|
||||
if (untagged.length) {
|
||||
errs.push(
|
||||
`${untagged.length} done card(s) carry no fields.verification.\n` +
|
||||
' A bare status:"done" cannot answer "did a human ever look at this?". Tag each one "agent",\n' +
|
||||
' "human" or "both", and set verification_kind to "visual" or "state".\n' +
|
||||
untagged.slice(0, 8).map((c) => ' - ' + c.id).join('\n') +
|
||||
(untagged.length > 8 ? `\n …and ${untagged.length - 8} more` : '')
|
||||
)
|
||||
}
|
||||
|
||||
// 2 — human verification requires a quote; an agent must not self-grant it
|
||||
const unquoted = cards.filter(
|
||||
(c) => c.fields && c.fields.human_verified && !String(c.fields.human_evidence || '').trim()
|
||||
)
|
||||
if (unquoted.length) {
|
||||
errs.push(
|
||||
`${unquoted.length} card(s) claim human_verified with no human_evidence.\n` +
|
||||
' Only a human grants human verification, and the quote is what makes that checkable. Without\n' +
|
||||
' it, an agent can quietly promote its own agent-verification into a human one — which would erase\n' +
|
||||
' the single distinction this whole policy exists to preserve.\n' +
|
||||
unquoted.map((c) => ' - ' + c.id).join('\n')
|
||||
)
|
||||
}
|
||||
|
||||
// 3 — a conflict is an open card, by definition
|
||||
const wrongly = cards.filter((c) => c.fields && c.fields.verification_conflict && c.status === 'done')
|
||||
if (wrongly.length) {
|
||||
errs.push(
|
||||
`${wrongly.length} card(s) are marked verification_conflict AND done.\n` +
|
||||
' A conflict means a tool says fixed while a human says broken. That is not a done card; it is\n' +
|
||||
' the strongest available signal that the probe measured the wrong surface. Reopen it.\n' +
|
||||
wrongly.map((c) => ' - ' + c.id).join('\n')
|
||||
)
|
||||
}
|
||||
|
||||
if (errs.length) {
|
||||
console.error('VERIFY-TAGS: FAIL')
|
||||
errs.forEach((e) => console.error(' ' + e))
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// 4 — the ledger (report-only, never fails)
|
||||
const agentVisual = done.filter((c) => c.fields.verification === 'agent' && c.fields.verification_kind === 'visual')
|
||||
const agentState = done.filter((c) => c.fields.verification === 'agent' && c.fields.verification_kind === 'state')
|
||||
const humanSeen = done.filter((c) => c.fields.human_verified)
|
||||
|
||||
console.log(`VERIFY-TAGS: PASS — ${done.length} done card(s), all tagged.`)
|
||||
console.log(` human-confirmed .............. ${humanSeen.length}`)
|
||||
console.log(` agent-only, STATE (strong) .. ${agentState.length} — counts, routing, file shapes; no human needed`)
|
||||
console.log(` agent-only, VISUAL (weak) .... ${agentVisual.length} — ★ closed on geometry, never on perception`)
|
||||
console.log(' The last number is the unpaid-attention ledger: cards a tool called fixed on a question')
|
||||
console.log(' only an eye can settle. It is not an error — it is the list to hand a human reviewer the')
|
||||
console.log(' next time one says a bug is still there, before investigating anything from scratch.')
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"about": "Lines in shipped text that referred to the Driver on 2026-09-29 (re-measured that day when the pattern learned the capitals form, DRIVER), keyed by file and text, to be removed by the owners' sweep before the public giveaway. Forward-only: this list may only shrink (--shrink).",
|
||||
"counts": {}
|
||||
}
|
||||
4
internal/fixtures/internal-role-nouns-baseline.json
Normal file
4
internal/fixtures/internal-role-nouns-baseline.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"about": "Lines in shipped text that used the internal role noun when this gate began, keyed by file and text. Owners remove them; the list only shrinks.",
|
||||
"counts": {}
|
||||
}
|
||||
|
|
@ -91,7 +91,6 @@
|
|||
"check:tauri",
|
||||
"check:translator-contacts:poison",
|
||||
"check:upg-map-data",
|
||||
"check:verification-tags",
|
||||
"check:vision-card-numbers",
|
||||
"check:zoning-declared:self-test",
|
||||
"smoke:aria-labels",
|
||||
|
|
|
|||
|
|
@ -209,8 +209,8 @@ if (process.env.DOXA_SKIP_FRESHNESS === '1') {
|
|||
if (fresh.status !== 0) refuse(
|
||||
'a committed composite is not what its source builds (stale consumers).\n' +
|
||||
'README rule 5: ONE agent owns the full build. If your change touches library/ or anything compiled into\n' +
|
||||
'other plugins, do not rebuild everyone yourself: commit it on your branch and hand the landing to studio\n' +
|
||||
'(msg-agent studio "please land <branch>@<sha>: library change, consumers need rebuilding"). If only YOUR\n' +
|
||||
'other plugins, do not rebuild everyone yourself: commit it on your branch and hand the landing to the\n' +
|
||||
'build owner ("please land <branch>@<sha>: library change, consumers need rebuilding"). If only YOUR\n' +
|
||||
'own plugin is stale, rebuild just it: BUNDLE=<name> bun x vite build && bun run tree && bun run fix:sri.',
|
||||
(fresh.stdout + fresh.stderr).trim().split('\n').filter((l) => /✗|STALE|FAIL/.test(l)).slice(0, 8).join('\n'))
|
||||
console.log('LANDING-PRECONDITIONS: 4 fresh — every composite rebuilds to its committed bytes')
|
||||
|
|
|
|||
|
|
@ -3,10 +3,9 @@
|
|||
* public-hygiene-check.mjs — prove a drop-in (or this repo's public/) is fit to be PUBLISHED:
|
||||
* only minified bundles, the leaf pages that host them, and the data/assets they fetch.
|
||||
*
|
||||
* WHY: the website is private on Forgejo today; the day it goes to GitHub the served folder
|
||||
* must carry no internal data — no local paths, no internal repo names, no tokens, no source
|
||||
* maps, no docs, no preview/staging surfaces (the rule since 2026-09-16). "Looks clean" is not a
|
||||
* gate; this scan is, and it can FAIL and say why.
|
||||
* WHY: the day the source is published, the served folder must carry no internal data — no local
|
||||
* paths, no internal repo names, no tokens, no source maps, no docs, no preview/staging surfaces.
|
||||
* "Looks clean" is not a gate; this scan is, and it can FAIL and say why.
|
||||
*
|
||||
* WHAT IT CHECKS (each finding prints file + rule):
|
||||
* leaks — absolute local paths (/home/…, /Users/…, C:\…), internal repo/folder names
|
||||
|
|
@ -75,9 +74,10 @@ const LEAK = [
|
|||
// (the source tree's own public/ still carries them, and failing build:unlocked for every
|
||||
// writer overnight would get this gate disabled) and FINDINGS under --strict, which the
|
||||
// publish/export flow MUST run (policy P5: publishing is a gated action, not a habit).
|
||||
const ROLE_NOUN = ['Dri', 'ver'].join('') // built from parts so this file does not itself carry the word
|
||||
const LEAK_STRICT = [
|
||||
[/\bthe Driver\b/, 'internal role noun (the Driver) — process vocabulary'],
|
||||
[/\bDriver\b/, 'internal role noun (Driver) — process vocabulary'],
|
||||
[new RegExp('\\bthe ' + ROLE_NOUN + '\\b'), 'internal role noun — process vocabulary'],
|
||||
[new RegExp('\\b' + ROLE_NOUN + '\\b'), 'internal role noun — process vocabulary'],
|
||||
[/doxa-life\/doxa-maps/, 'private repo name'],
|
||||
[/\bdoxa-life\b/, 'private org name'],
|
||||
[/\bforgejo\b/i, 'self-hosted forge name'],
|
||||
|
|
@ -85,8 +85,8 @@ const LEAK_STRICT = [
|
|||
[/\bpersecution\b/, 'operational-context vocabulary (allowed only inside data CSVs)'],
|
||||
[/\.forgejo\/workflows/, 'CI workflow path — process, never ships in a public tree'],
|
||||
]
|
||||
// ★ THE PRAYER, WORD FOR WORD (decided 2026-09-28): persecution happens globally, so the prayer is for
|
||||
// the globe, and every packet carries the same prayer. The operational-vocabulary rule
|
||||
// ★ ONE PRAYER IS ALLOWED, WORD FOR WORD: the prayer is for the whole world, and every packet carries the
|
||||
// same prayer. The operational-vocabulary rule
|
||||
// skips EXACTLY that prayer: the text at packet-strings.json <locale>.points[0].text (the packets' prayer
|
||||
// point 1, which UPG-100's Two-Cross packet shows too), read from its source in every locale, removed from
|
||||
// a scanned file before the rule looks at it. Scoped by file and key, never by the word: the same word
|
||||
|
|
|
|||
38
package.json
38
package.json
|
|
@ -23,10 +23,8 @@
|
|||
"check:app-start-page": "node internal/set-app-start-page.mjs --check",
|
||||
"check:approx-pin-note": "node internal/check-approx-pin-note.mjs",
|
||||
"check:auto-engine": "node internal/check-auto-engine.mjs",
|
||||
"check:backlog-refs": "node internal/check-backlog-refs.mjs",
|
||||
"check:no-new-driver-references": "node internal/check-no-new-driver-references.mjs",
|
||||
"check:no-internal-role-nouns": "node internal/check-no-internal-role-nouns.mjs",
|
||||
"check:plural-forms": "node internal/check-plural-forms.mjs",
|
||||
"check:backlog-status": "node internal/check-backlog-status-enum.mjs",
|
||||
"check:basemap-labels": "node internal/check-basemap-labels-localized.mjs",
|
||||
"check:browser-gates-guarded": "node internal/check-browser-gates-guarded.mjs",
|
||||
"check:capability-citations": "node internal/check-capability-citations.mjs",
|
||||
|
|
@ -66,9 +64,7 @@
|
|||
"check:doc-scripts": "node internal/check-doc-quotes-scripts.mjs",
|
||||
"check:readme-size": "node internal/check-readme-web-folder-size.mjs",
|
||||
"check:doc-scripts:self-test": "node internal/check-doc-quotes-scripts.mjs --self-test",
|
||||
"check:done-evidence": "node internal/check-done-evidence.mjs",
|
||||
"check:drop": "node internal/drop-portability-check.mjs",
|
||||
"check:dxlife": "node internal/check-dxlife-untouched.mjs",
|
||||
"check:element-census-vs-live": "node internal/check-element-census-vs-live.mjs",
|
||||
"check:embed-base": "node internal/check-embed-base-resolution.mjs",
|
||||
"check:embed-basemap": "node internal/check-embed-basemap-paints.mjs",
|
||||
|
|
@ -335,7 +331,6 @@
|
|||
"check:tree-weight": "node internal/check-published-tree-weight.mjs",
|
||||
"check:upg-map-data": "node internal/check-upg-map-data-painted.mjs",
|
||||
"check:url-resolver": "node internal/check-url-resolver.mjs",
|
||||
"check:verification-tags": "node internal/check-verification-tags.mjs",
|
||||
"check:vision-card-numbers": "node internal/check-vision-card-numbers.mjs",
|
||||
"check:wp-embed": "php deployment/wordpress/tests/wp-embed-contract-test.php",
|
||||
"check:zone-census": "node internal/check-zone-census.mjs",
|
||||
|
|
@ -347,8 +342,8 @@
|
|||
"dev": "bun x vite",
|
||||
"fix:sri": "node internal/fix-sri.mjs",
|
||||
"gallery": "node internal/generate-gallery.mjs",
|
||||
"gates:post": "node internal/run-parallel.mjs --label=post -- check:capability-derived check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:zoning-declared check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:pin-dim-slider check:legend-source check:backlog-refs check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary check:wp-embed check:dashboard-geocoder-provider check:leaf-engine-per-provider check:auto-engine check:engine-contract check:map-props-unit check:composites-mount check:composite-interop check:no-free-identifiers check:map-fly-reduced-motion check:mst-clustering check:list-prop-unit check:profiles-mount check:cdn-pins check:no-eval check:popup-escaping check:i18n-treeshake check:pinned-umd check:one-palette:self-test check:no-cross-plugin-imports check:no-cross-plugin-imports:self-test check:map-attribution check:studio-rail check:approx-pin-note check:embedder-keys check:provider-for check:legend-logical-insets check:legend-logical-insets:self-test check:optional-t-equivalence check:text-highlight check:religion-label-not-value check:i18n-stopped-scope check:no-silent-engine check:no-silent-engine:self-test check:regions-uses-library check:shadow-css-parses check:square-map-controls check:preset-config-merge check:instance-config-keys check:frozen-labels check:plural-forms check:region-fill-repaint check:region-paint-single-source check:poster-print-fallback check:posters-exportable check:country-names check:reduced-motion check:no-dead-library check:universal-props check:readme-size check:tree-relative check:leaf-hints check:snippet-engine check:snippet-engine:self-test check:embed-base check:link-coverage check:no-public-binds check:shared-reached check:tree-weight",
|
||||
"gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts check:setup-order check:package-json-unique-keys check:no-repo-pb-hooks check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion check:poster-action-print check:targets check:targets:self-test check:browser-gates-guarded check:pin-selection check:facet-shape check:hardcoded-english:self-test check:list-selection check:selector-fields check:upg-zone-joins check:gospel-set-charts check:gospel-set-fit check:describe-group check:one-scroll-container check:packet-charts check:composite-token-url check:language-tree-layout check:i18n-keys-exist check:i18n-never-translated check:packet-variants check:profile-table check:element-bootstrap check:embed-url-absolute check:frame-target check:language-tree-model check:map-menu-anchor check:selection-kinds check:tiered-points check:whiteboard-projection check:mercator-slice check:ring-vocabulary check:map-props-unit check:mobile-gating check:provide-shadowing check:legend-top-offset check:cdn-cache check:plural-forms check:select-zone-race check:script-registry check:map-control-overlap check:poster-attribution check:board-map-options check:gospel-whiteboard check:packet-map-missing-loud check:poster-layout-optin check:ported-globals-identical check:ring-bands check:packet-a1-structure check:india-states check:batch-selection-export check:zone-title-tier-a check:evangelical-share check:map-labels check:packet-offline check:help-reachable check:gated-not-in-tree check:window-globals-released check:legend-data-contract check:no-new-driver-references check:poster-fetch-guard check:people-group-records check:sentence-slots check:popup-href-escaped check:popup-pan-controls check:country-map-refetch check:pane-config-reforward check:viewport-meta check:viewport-meta:self-test check:label-key-coverage check:poster-browser-equiv check:poster-render-pinned check:country-sheet-one-path check:country-sheet-one-path:self-test check:posterverse-shelf check:host-locale-fetch check:print-frame-reclaim check:click-sheet check:coverage-consistent:chained check:coverage-consistent:poison check:dashboard-filter-a11y check:filter-multivalue check:hover-channel check:kit-options-reachable check:layout-key-namespaced check:legend-mirrors check:map-unmount-race check:no-conflict-markers check:series-isolation check:snapshot-guard check:stale-sweep-claim check:surfaces-read-the-tier check:translator-contacts check:dxlife check:wall-wysiwyg check:wall-print-watch check:wall-no-bg-photos check:wall-pan check:template-i18n check:list-pattern-doc check:shadow-style-labels-unique check:grid-image-loading check:retry-fetch check:panel-sheets check:poster-wall-host-seams check:poster-design-catalogue",
|
||||
"gates:post": "node internal/run-parallel.mjs --label=post -- check:capability-derived check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:zoning-declared check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:script-flags check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:pin-dim-slider check:legend-source check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary check:wp-embed check:dashboard-geocoder-provider check:leaf-engine-per-provider check:auto-engine check:engine-contract check:map-props-unit check:composites-mount check:composite-interop check:no-free-identifiers check:map-fly-reduced-motion check:mst-clustering check:list-prop-unit check:profiles-mount check:cdn-pins check:no-eval check:popup-escaping check:i18n-treeshake check:pinned-umd check:one-palette:self-test check:no-cross-plugin-imports check:no-cross-plugin-imports:self-test check:map-attribution check:studio-rail check:approx-pin-note check:embedder-keys check:provider-for check:legend-logical-insets check:legend-logical-insets:self-test check:optional-t-equivalence check:text-highlight check:religion-label-not-value check:i18n-stopped-scope check:no-silent-engine check:no-silent-engine:self-test check:regions-uses-library check:shadow-css-parses check:square-map-controls check:preset-config-merge check:instance-config-keys check:frozen-labels check:plural-forms check:region-fill-repaint check:region-paint-single-source check:poster-print-fallback check:posters-exportable check:country-names check:reduced-motion check:no-dead-library check:universal-props check:readme-size check:tree-relative check:leaf-hints check:snippet-engine check:snippet-engine:self-test check:embed-base check:link-coverage check:no-public-binds check:shared-reached check:tree-weight",
|
||||
"gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts check:setup-order check:package-json-unique-keys check:no-repo-pb-hooks check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion check:poster-action-print check:targets check:targets:self-test check:browser-gates-guarded check:pin-selection check:facet-shape check:hardcoded-english:self-test check:list-selection check:selector-fields check:upg-zone-joins check:gospel-set-charts check:gospel-set-fit check:describe-group check:one-scroll-container check:packet-charts check:composite-token-url check:language-tree-layout check:i18n-keys-exist check:i18n-never-translated check:packet-variants check:profile-table check:element-bootstrap check:embed-url-absolute check:frame-target check:language-tree-model check:map-menu-anchor check:selection-kinds check:tiered-points check:whiteboard-projection check:mercator-slice check:ring-vocabulary check:map-props-unit check:mobile-gating check:provide-shadowing check:legend-top-offset check:cdn-cache check:plural-forms check:select-zone-race check:script-registry check:map-control-overlap check:poster-attribution check:board-map-options check:gospel-whiteboard check:packet-map-missing-loud check:poster-layout-optin check:ported-globals-identical check:ring-bands check:packet-a1-structure check:india-states check:batch-selection-export check:zone-title-tier-a check:evangelical-share check:map-labels check:packet-offline check:help-reachable check:gated-not-in-tree check:window-globals-released check:legend-data-contract check:no-internal-role-nouns check:poster-fetch-guard check:people-group-records check:sentence-slots check:popup-href-escaped check:popup-pan-controls check:country-map-refetch check:pane-config-reforward check:viewport-meta check:viewport-meta:self-test check:label-key-coverage check:poster-browser-equiv check:poster-render-pinned check:country-sheet-one-path check:country-sheet-one-path:self-test check:posterverse-shelf check:host-locale-fetch check:print-frame-reclaim check:click-sheet check:coverage-consistent:chained check:coverage-consistent:poison check:dashboard-filter-a11y check:filter-multivalue check:hover-channel check:kit-options-reachable check:layout-key-namespaced check:legend-mirrors check:map-unmount-race check:no-conflict-markers check:series-isolation check:snapshot-guard check:stale-sweep-claim check:surfaces-read-the-tier check:translator-contacts check:wall-wysiwyg check:wall-print-watch check:wall-no-bg-photos check:wall-pan check:template-i18n check:list-pattern-doc check:shadow-style-labels-unique check:grid-image-loading check:retry-fetch check:panel-sheets check:poster-wall-host-seams check:poster-design-catalogue",
|
||||
"gen:iso3-to-alpha2": "node library/scripts/generate-iso3-to-alpha2.mjs",
|
||||
"internet:adm1": "node plugin-designs/cities-internet/scripts/build-adm1-internet.mjs",
|
||||
"library-index": "node internal/generate-library-index.mjs",
|
||||
|
|
@ -420,9 +415,9 @@
|
|||
"check:mobile-gating": "one phone flag per map (SPEC-MOBILE-FLAG): every <profile>.meta.json `mobile` is desktop-only or larger-screens; every flagged profile's leaf page carries its flag (and no leaf carries one its meta dropped); the Research Gallery derives its rules from the manifest, holds no id set, and mounts nothing on a phone before the flags land; dashboard-plus-grid stays desktop-only; the three notice sentences exist in all 11 locales and the desktop-only one names the map; ProfileLoader blocks, advises, and stays quiet inside the Gallery. --self-test plants one bad case per rule.",
|
||||
"check:approx-pin-note": "people-group pins say they are localities, not towns or boundaries, in ONE shared wording (library/components/legends/ApproxPinNote.vue): the colocation index finds groups sharing a coordinate, the note names them only for a shared point, never prints a raw coordinate, and renders from each locale's catalogue. Headless.",
|
||||
"check:auto-engine": "BROWSER. The default engine (auto) is MapLibre, token or not: on one map leaf, no token AND a host token alone both draw with MapLibre, paint, and make zero api.mapbox.com requests; ?provider=mapbox with that token draws Mapbox and carries it. In gates:post, NOT MEASURED outside the browser slot.",
|
||||
"check:no-new-driver-references": "shipped text (library, plugins, internal, docs, skills, README) neither quotes nor cites the Driver: a line that refers to the Driver and is not in internal/fixtures/driver-references-baseline.json FAILS. Forward-only: the baseline, keyed by file and line text with counts (not line numbers), may only shrink as owners sweep their areas (--shrink tidies it; it never grows). Matches the role word only, not a GPU or device driver. Node only.",
|
||||
"check:no-internal-role-nouns": "shipped text (library, plugins, internal, docs, skills, README) names no internal process role: a line that uses the role noun and is not in internal/fixtures/internal-role-nouns-baseline.json FAILS. Forward-only: the baseline, keyed by file and line text with counts (not line numbers), may only shrink as owners sweep their areas (--shrink tidies it; it never grows). Matches the role noun only, not a GPU or device driver; the word is assembled at runtime, so the gate does not carry it. Node only.",
|
||||
"check:no-orphan-gates": "every gate-shaped internal/*.mjs is reachable or tagged @one-shot; and every check:/verify:/smoke: script is run by a standing chain, matched by EXACT name (gates:pre, gates:post, the browser suite GATES, the build, and what those bun run), or declared in internal/fixtures/on-demand-checks.json with its reason, or in the shrink-only baseline of the 2026-09-29 orphans. A chained :self-test does not chain its gate. --shrink / --init maintain the baseline. And every gate-shaped FILE under plugin-designs/ or library/ is run by PATH from a script or another file's code (comments stripped; a basename counts only when no other gate file shares it), tagged @one-shot, or in the baseline's `files` list.",
|
||||
"check:snippets-draw": "BROWSER. Every production profile's published embed.script, pasted VERBATIM on a partner page on another origin (only YOUR_TREE_URL / YOUR_SOURCE filled), must DRAW: a hooked MapLibre instance with its style loaded, a sized canvas and tiles 2xx, no failure note, 0 page errors (a no-map profile: a rendered shadow tree with text). Composites that open on a list are pressed to their map view first (settle rule). Ported from rega's snipgate probe. NOT MEASURED outside the browser slot.",
|
||||
"check:snippets-draw": "BROWSER. Every production profile's published embed.script, pasted VERBATIM on a partner page on another origin (only YOUR_TREE_URL / YOUR_SOURCE filled), must DRAW: a hooked MapLibre instance with its style loaded, a sized canvas and tiles 2xx, no failure note, 0 page errors (a no-map profile: a rendered shadow tree with text). Composites that open on a list are pressed to their map view first (settle rule). Ported from the snipgate browser probe. NOT MEASURED outside the browser slot.",
|
||||
"check:snippets-draw:mapbox": "BROWSER. The same over every embed.scriptMapbox, verbatim with ONLY YOUR_MAPBOX_TOKEN filled from env MAP_TOKEN, and the map must draw on Mapbox fetching no maplibre-gl. NOT MEASURED without a token, never a pass; the token is redacted from every logged line.",
|
||||
"check:wall-wysiwyg": "the Wall's screen IS its poster: the Bless view's on-screen poster and its print come from one layout, so for every preset (3x4ft, 2x3ft, 5x8ft) the sheets, the 2,107 card rects, the card text, the verse reference and the chrome agree. Two-sided on planted layout defects. Headless.",
|
||||
"check:wall-print-watch": "the Wall's HD poster print never prints silently incomplete: with fake timers and a fake frame, the watch counts photos as they load, counts the ones that could not, and reports both. Two-sided on planted printWatch copies. Headless.",
|
||||
|
|
@ -433,7 +428,6 @@
|
|||
"check:viewport-meta": "every generated page declares <meta name=\"viewport\"> with width=device-width. Without it a browser lays the page out at the 980px desktop fallback even when the viewport is set to 390, so a mobile probe silently measures a viewport no phone has and still passes (measured: innerWidth 980 without the tag, 390 with it). width=device-width is required specifically, because a meta carrying only initial-scale still lays out at 980. Checked on the BUILT tree, not the generator template. Fixed-size print sheets are exempt by their stated shape (a .stage with a three-digit px width), and a page that must render at a fixed desktop width carries a viewport-meta-ok: reason. 8 self-tests. Source-only, headless.",
|
||||
"check:viewport-meta:self-test": "plants each check:viewport-meta violation - no meta at all, and a meta with initial-scale but no width=device-width - and confirms both are caught, that correct tags in either quote style or uppercase pass, and that the print-sheet exemption applies to a fixed-px stage but NOT to a responsive one.",
|
||||
"check:plural-forms": "the three things a CLDR pluralRules map can silently break. (1) FORM COUNT: a locale must author as many forms as it has CLDR categories or the pick clamps onto the wrong one (ru 4, ar 6). (2) THE tStatic PATH: seven bundles install no vue-i18n and read through tStatic, which does NO plural selection, so a pipe key there reaches the page as the literal \"a | b | c\". (3) ZERO: a locale with an explicit CLDR zero category must author it. Written as the precondition for installing the map, and kept as the guard after.",
|
||||
"check:backlog-refs": "a card may not depend on a card that does not exist. A dangling depends_on can never be done, so the card can never become ready and nothing says so — it reads as ordinary blocked work forever. FAILS only for depends_on on an OPEN card; a dangling parent or a stale link on a done card is REPORTED, because failing on filing debt teaches people to delete the reference rather than restore the card. Reads all three phase backlogs — references legitimately cross phases. SKIPs when _qa-cycle is absent.",
|
||||
"check:basemap-labels": "the BASEMAP's place names follow the reader's language on BOTH engines (library/maps/localize-labels.js): reads the RENDERED country labels at ?lang=en and ?lang=ru — Mapbox via the Standard basemap's place-labels featureset, MapLibre via each symbol's evaluated text — and requires them to flip. Serves this checkout on an OS-assigned port; --dir <tree> for a negative control. Negative-tested: a bundle built before 319064a1 FAILS on both engines.",
|
||||
"check:browser-gates-guarded": "no repo file imports playwright except through internal/lib/playwright-guarded.mjs, whose launch refuses (exit 75, NOT MEASURED) unless internal/browser-slot.sh has exported DOXA_BROWSER_SLOT. Browsers run only on the browser seat, inside the slot; this makes that rule enforced, not remembered.",
|
||||
"check:capability-citations": "a doc that names a bundle as \"capability N\" must match manifest.json, which is what the BUILD computed. README captioned doxa-vision-card as cap-1; it ships 2 instances so it is parameterized (cap-2). The SAME error was fixed once in verify-5-capabilities.mjs and never reached the README — two copies of one fact, nothing forcing agreement.",
|
||||
|
|
@ -456,24 +450,22 @@
|
|||
"check:country-sheet-one-path:self-test": "plants each defect check:country-sheet-one-path guards (a direct generateCountrySheet call, makeCountrySheet without countries, the old guard that skipped India, a position given to an absent code) and requires each to FAIL; a comment naming generateCountrySheet must NOT.",
|
||||
"check:coverage-consistent": "the gospel-resources surfaces must agree: no resource on an ISO RESERVED code (und/mul/mis/zxx), every resourced code NAMED, table reach recounted off the pins, no invented URL, tiers reconcile. Static, sub-second. Self-test: `bun run check:coverage-consistent:poison`, all 12 clauses (A-L) must fail. Clause L is the one that is not about consistency: it fails when the link-verification record goes stale, because 'link checked' is a claim about the present tense. Written after the same drift defect was found by hand three times.",
|
||||
"check:csv-shape": "the export must OPEN as a table: header on line 0, every row matching. Run the `check:csv-shape:self-test` script for the NEGATIVE test (forces the provenance comment back to line 0 IN MEMORY and requires this gate to REJECT it) — the header used to claim 'NEGATIVE-TESTED' with no way to re-run it.",
|
||||
"check:dashboard-filter-a11y": "the dashboard's cross-filter is operable and audible: every filter chip is a <button type=\"button\"> with an aria-label naming the ACTION (a filter that can be set by keyboard must be clearable by keyboard), the shell carries exactly ONE always-rendered polite role=\"status\" region so a brush that changes a number far from the control is announced once rather than six times or not at all, and no resize handle's accessible name is a raw English literal. AST over the parsed templates plus the built bundle — no browser (rega's alone).",
|
||||
"check:dashboard-filter-a11y": "the dashboard's cross-filter is operable and audible: every filter chip is a <button type=\"button\"> with an aria-label naming the ACTION (a filter that can be set by keyboard must be clearable by keyboard), the shell carries exactly ONE always-rendered polite role=\"status\" region so a brush that changes a number far from the control is announced once rather than six times or not at all, and no resize handle's accessible name is a raw English literal. AST over the parsed templates plus the built bundle — no browser.",
|
||||
"check:dashboard-geocoder-provider": "the research dashboard leaf at ?provider=maplibre makes ZERO requests to api.mapbox.com, and its search bars still suggest (built tree; browser gate). It was kept out of gates:post while the generated leaf hard-linked the Mapbox engine; the leaf now loads only the engine it is asked for.",
|
||||
"check:dep-names": "every declared dep is a REAL package name (a rename once rewrote @capacitor/* and broke bun install in a clone)",
|
||||
"check:poster-attribution": "the poster footer is a LICENCE statement, so it must credit whoever actually served the tiles. posterDefaults ships '© Mapbox © OpenStreetMap', which under the fleet's MapLibre default credits a company that served nothing. Checked live 2026-09-28: the OpenFreeMap STYLE declares no attribution on either source — the credit is in the TileJSON, merged into the SOURCE object after load — so it is read from map.getSource(id), flattened from HTML, deduped, and an unreadable map falls back rather than printing a blank line. Headless; in gates:pre.",
|
||||
"check:script-registry": "one bundle script executes ONCE PER PAGE. Every plugin is a self-contained IIFE, so library/utils/embedScript.js is compiled into each one and a module-scoped cache was one cache per BUNDLE: two shells on a page each loaded the same child, which re-executed a whole bundle (a second Vue, i18n and pinia) before its guarded customElements.define silently declined. The check imports the module twice under different URLs — two module instances, one globalThis, the same shape as two IIFEs — and asserts the second caller receives the FIRST ONE'S PROMISE, because counting script tags alone is satisfied by the DOM-adoption path and would not catch a regression. Headless; in gates:pre.",
|
||||
"check:map-control-overlap": "two controls pinned to the same corner of the research-dashboard map must not land on each other. The zoom toolbar was pinned at a literal top:34px, tuned when the projection toggle measured 23px tall; a later WCAG 2.5.8 fix gave the toggle min-height:24px, which in a shadow root with no global box-sizing rendered 34px and ran 8px into a toolbar drawn over it at z-index 10. The check COMPUTES the toggle's rendered height from its own declarations, honouring box-sizing, and requires the toolbar to clear it and to DERIVE its offset rather than restate a number. Headless; in gates:pre.",
|
||||
"check:legend-data-contract": "nobody may read a key off useLegendData that it does not return. It returns items, never rows; two plugins read legend.rows anyway and JavaScript answered undefined, so the same mistake surfaced as a THROWN Export poster in countries-map (exp NE-17) and as a silently BLANK poster legend in research-map. The returned key set is read from the composable rather than restated. NOT in gates:pre yet: it still reports countries-map, which is another owner's file (exp's d6916e05e covers .rows but not the dead .build/.refresh calls). Run it directly.",
|
||||
"check:window-globals-released": "a component that puts something on window must take it back. rega's heap snapshots (leak-722ac7789) measured SR+APG mounted twice at 54.2 -> 65.1 MB with live Vue apps 6 -> 8: every unmounted app stayed in memory, each pinned by one window key holding a closure, which keeps the component, its shadow root and the whole app. Flags only values that RETAIN (closures and object references), not warn-once latches or page-wide caches keyed off themselves. NOT in gates:pre: 19 sites remain across other owners' plugins. Run it directly.",
|
||||
"check:window-globals-released": "a component that puts something on window must take it back. Heap snapshots (leak-722ac7789) measured SR+APG mounted twice at 54.2 -> 65.1 MB with live Vue apps 6 -> 8: every unmounted app stayed in memory, each pinned by one window key holding a closure, which keeps the component, its shadow root and the whole app. Flags only values that RETAIN (closures and object references), not warn-once latches or page-wide caches keyed off themselves. NOT in gates:pre: 19 sites remain across other owners' plugins. Run it directly.",
|
||||
"check:describe-group": "the prayer packet's generated profile sentence (library/packets/describeGroup.js): each GSEC level states its own fact, and an index-ordered language name (\"Arabic, Moroccan\") is un-inverted by library/utils/languageLabel.js without shredding a comma inside parentheses, the 13 names the old split printed reversed. A catalogue arm renders every GSEC level from the REAL packet-strings.json in en and ar and fails on a blank sentence. Headless; in gates:pre.",
|
||||
"check:descriptions": "every shipped profile must say what it is (reads the BUILT manifest)",
|
||||
"check:done-evidence": "a card may not reach status:done with NO bluf AND NO fields — the Doxa team inherits this board and an evidence-less 'done' cannot be reconciled against the code. 10 pre-existing are baselined by id; the list should only ever SHRINK.",
|
||||
"check:dxlife": "⚠ NOT WAVE-SAFE, deliberately unwired: exit codes 0/1/2 are a DOCUMENTED contract (0 unchanged · 1 drift · 2 clone not found), and run-parallel grades any non-zero as FAILURE — so a checkout without the sibling /root-dx-life clone goes red on a precondition. Changing the contract could break another consumer; hand-run it, or give it a recognised SKIP first.",
|
||||
"check:element-census-vs-live": "what is on ONE side and not the other, ours vs the live doxa.life page we clone: harvests every visible TEXT NODE through every shadow root on both sides and diffs the SETS. OURS-ONLY unaccounted = a scope violation (FAIL), THEIRS-ONLY = a clone omission (FAIL), SANCTIONED = an allowlist entry carrying the ruling + date + card (refused without them), DATA DRIFT = same label, different digits (reported). The negative control is MANDATORY: a known ours-only string must land in the ours-only set or it prints CENSUS IS BLIND and exits 75; an unreachable live page is also 75. Presets: internal/element-census/<name>.json (--preset <name>, default all); ad hoc: --leaf <bundle/profile/index.html[#hash]> --live <url> --neg <text> [--sanctioned <file>]. Promoted from regions' probe; regions passes (list 229 vs 228, the only difference its sanctioned poster toggle). Through browser-slot.sh.",
|
||||
"check:embed-base": "the shared URL resolver, pinned by ARITHMETIC — no browser, no network, no served tree, so it cannot SKIP itself into irrelevance. It computes what a leaf's own \"../../assets\" means and asserts the resolver agrees. Caught nothing for fifteen ticks because it did not exist: resolveAssetsBase computed one segment too high, every local gate passed (the dev web-root has assets BOTH inside and beside the tree), and the byte-level publish gates went green on the broken publish.",
|
||||
"check:embed-basemap": "the gospel map's BASEMAP must paint inside a genuinely cross-origin iframe on the token the page BAKES, not one a probe forces on it. Every other browser gate here forces a restricted token (u=doxalife) that 403s on the basemap, then counts PINS — which come from our own GeoJSON and paint with no token at all — so 'the map renders' had never been measured. Runs a negative control every time by rewriting the token on the wire; if the control passes it reports PROVED NOTHING rather than a pass. ~60s, needs a browser.",
|
||||
"check:embed-targets": "every embed.iframe src in manifest.json must resolve to a file that EXISTS. Those snippets ARE deployment-method 1 (iframe) — a partner pastes them verbatim — and NOTHING else asserts their targets are still emitted, because every other gate walks only files that exist and is blind to ABSENCE. `the `check:embed-targets:self-test` script` poisons a copy of the manifest and asserts the gate NOTICES: a gate that cannot fail is worse than no gate.",
|
||||
"check:embedder-keys": "regions-mfe forwards page-scoped config to its nested panes by an explicit key list (INHERITED_PAGE_KEYS). Asserts the documented embedder keys travel (dataSource, tokenUrl among them) AND that profile and instanceId do NOT: the host's would overwrite each pane's own and collapse both tabs into one view.",
|
||||
"check:english-leaks": "A translated key must not render in English. rega's method: a string is a LEAK when the catalogue has a DIFFERENT value for the locale and the page still shows the English one - a render/wiring fault, not a missing translation. Third-party attributions, data names, placeholder-only strings and the prose this bundle deliberately keeps in English are all excluded, the last by simply not being in the catalogue. Scoped to the LEAF on purpose: six keys were reported leaking in the gallery and the leaf is clean in ar/zh/ru/hi - the gallery builds its iframe src with no lang parameter, so the embed never learns the locale. Embedder defect, not a bundle one.",
|
||||
"check:english-leaks": "A translated key must not render in English. The method: a string is a LEAK when the catalogue has a DIFFERENT value for the locale and the page still shows the English one - a render/wiring fault, not a missing translation. Third-party attributions, data names, placeholder-only strings and the prose this bundle deliberately keeps in English are all excluded, the last by simply not being in the catalogue. Scoped to the LEAF on purpose: six keys were reported leaking in the gallery and the leaf is clean in ar/zh/ru/hi - the gallery builds its iframe src with no lang parameter, so the embed never learns the locale. Embedder defect, not a bundle one.",
|
||||
"check:experimental-shelf": "Every Experimental-folder bundle, leaf + Gallery tab, mapbox + maplibre: mounts with content, 0 errors / failed requests, maps load and DRAW their own data. Self-tests on a deliberately broken bundle first. Run through internal/browser-slot.sh.",
|
||||
"check:export-poster-prints": "the Simple Research 'Export poster' button opens the PRINT DIALOG on the poster in ONE click and downloads NOTHING (the rule since 2026-09-28): leaf AND Research Gallery, en AND ar; the printing document is the 1700x2200 poster on an 8.5in x 11in sheet; no download arrives; 0 page errors. window.print is stubbed, so this measures everything up to the dialog. Replaces check:export-one-click, which asserted the download this rule forbids. Run inside the shared browser slot. Exit 2 = SKIP, 75 = NOT MEASURED.",
|
||||
"check:export-poster-prints:maplibre": "the same four one-click prints on the keyless MapLibre engine (OpenFreeMap tiles): the hidden print map must be built with the engine that drew the screen, and the footer must credit the tiles actually printed.",
|
||||
|
|
@ -502,13 +494,13 @@
|
|||
"check:help-reachable": "every Experimental map with explainer text in doxa-maps-spa/src/experimentalHelp.js reaches it from its OWN \"?\" (a hosted HelpButton or useHostedHelp in its bar), or through a named control of its own (upg-neighbor's About); the app listens for doxa:help, marks its panes with data-doxa-help and has no top-centre button. Executes the composable on a fake DOM: doxa:help must bubble, be composed, and find the host across a shadow root. Six planted defects each caught. Node only, no browser.",
|
||||
"check:hover-channel": "SPEC-DASHBOARD-KIT D3: the bus's third coordinated channel points at a record without committing to one. Hover looks like a weaker selection, and the cheap implementation is to put the key in selectionSet 'just while hovering' - every consequence of which is bad and none obvious at the call site: useSelectionCamera watches selectionVersion, so a hover would fly the camera at every mousemove (the resize-strobe class, and a reader may be photosensitive); the CSV exporter reads selection > filter > all, so a hover would change what an export contains; the table's selection pill and the shell's live region would both narrate the pointer. Runs the real bus: hover holds its own ref, never enters the selection, never bumps the version, never touches the filters, a repeat emits once (hover fires on every mousemove across 2,107 rows), and the three channels clear independently. Does NOT check which panels light up - D3 says panels opt in, and the first opt-in belongs with a consumer that has a reason. No browser.",
|
||||
"check:host-locale-contract": "a HOST's profile-config.locale reaches every leaf: each leaf page is loaded with ONLY \"locale\":\"ar\" added to its profile-config (no ?lang=, the page keeps lang=\"en\") and must draw at least half the Arabic text the same page draws at ?lang=ar. regions-dashboard/regions-list is the positive control (NOT MEASURED if it does not enter Arabic). Guards ProfileLoader's inject('appI18n'): a bundle that never provides appI18n silently ignores the host. --dir <tree> for a before/after, --only a/b,c/d to narrow. Through browser-slot.sh.",
|
||||
"check:i18n-keys-exist": "every literal catalogue key named in code exists in library/i18n/locales/en/. A localized CALL SITE is not a localized STRING: the dashboard's five resize handles all read T('aria.resizeFiltersRail', ...) and look correct in review, while aria.json held eleven keys and not one was a resize key - so every handle announced itself in English in all eleven locales, and the fallback that makes a missing key degrade to readable English is exactly what kept it silent. No other gate asks this: i18n-parity is satisfied because the key is missing from ALL locales equally, and english-leaks and composite-locale-leaks both need the catalogue to HOLD the string before an appearance counts as a leak. Matches tStatic/$t/t and the house-pattern T( alias, all of which take a full dotted key; prefix helpers like L( and any concatenated or template-literal key are COUNTED as unresolved and never guessed at. Found 22 missing keys on its first run. ROUND 2, after go-int planted T('poison_key') behind `const T = useGiT()` and this gate reported the tree CLEAN -- the one failure a check is not forgiven, because a red gate gets fixed and a green one that should be red gets believed. THREE independent blindnesses stacked, and any one alone hid the key: the file never writes the word tStatic so the precondition skipped it before anything ran; useGiT is not tStatic so no alias was found; and the alias is PREFIXED (useOptionalT carries a namespace) while its call sites pass BARE keys, which the full-key pattern cannot match because it requires a dot. Now a translator that arrives from a COMPOSABLE is resolved like any other, with the namespace READ from the factory's one-line body rather than listed here -- useOptionalT/useOptionalI18n directly, plus any `export function useXxxT() { return useOptionalT('ns') }`, which is how useNeighborT and usePplrT are written. Because qualify() prefixes a bare key and leaves an already-qualified one alone, one binding registers as BOTH shapes. MEASURED before and after with the same poison on disk: the old version PASSES it, this one fails it as 'neighbor.zzz_not_a_real_key (via nt())'; coverage went 683 -> 743 full-key and 49 -> 56 prefixed call sites, so 67 sites were being scored clean without being read. A self-test now asserts all three fixes separately and exits 3 if any goes blind, because this gate guards goal (d) and had already been trusted once while lying. Empty-namespace bare keys stay outside the model and are COUNTED rather than assumed away; measured at zero today, since all five such sites pass dotted keys.",
|
||||
"check:i18n-keys-exist": "every literal catalogue key named in code exists in library/i18n/locales/en/. A localized CALL SITE is not a localized STRING: the dashboard's five resize handles all read T('aria.resizeFiltersRail', ...) and look correct in review, while aria.json held eleven keys and not one was a resize key - so every handle announced itself in English in all eleven locales, and the fallback that makes a missing key degrade to readable English is exactly what kept it silent. No other gate asks this: i18n-parity is satisfied because the key is missing from ALL locales equally, and english-leaks and composite-locale-leaks both need the catalogue to HOLD the string before an appearance counts as a leak. Matches tStatic/$t/t and the house-pattern T( alias, all of which take a full dotted key; prefix helpers like L( and any concatenated or template-literal key are COUNTED as unresolved and never guessed at. Found 22 missing keys on its first run. ROUND 2, after a test planted T('poison_key') behind `const T = useGiT()` and this gate reported the tree CLEAN -- the one failure a check is not forgiven, because a red gate gets fixed and a green one that should be red gets believed. THREE independent blindnesses stacked, and any one alone hid the key: the file never writes the word tStatic so the precondition skipped it before anything ran; useGiT is not tStatic so no alias was found; and the alias is PREFIXED (useOptionalT carries a namespace) while its call sites pass BARE keys, which the full-key pattern cannot match because it requires a dot. Now a translator that arrives from a COMPOSABLE is resolved like any other, with the namespace READ from the factory's one-line body rather than listed here -- useOptionalT/useOptionalI18n directly, plus any `export function useXxxT() { return useOptionalT('ns') }`, which is how useNeighborT and usePplrT are written. Because qualify() prefixes a bare key and leaves an already-qualified one alone, one binding registers as BOTH shapes. MEASURED before and after with the same poison on disk: the old version PASSES it, this one fails it as 'neighbor.zzz_not_a_real_key (via nt())'; coverage went 683 -> 743 full-key and 49 -> 56 prefixed call sites, so 67 sites were being scored clean without being read. A self-test now asserts all three fixes separately and exits 3 if any goes blind, because this gate guards goal (d) and had already been trusted once while lying. Empty-namespace bare keys stay outside the model and are COUNTED rather than assumed away; measured at zero today, since all five such sites pass dotted keys.",
|
||||
"check:i18n-never-translated": "a key PRESENT in all 11 catalogs but IDENTICAL in all of them is copied, not translated. check:i18n-parity asserts presence; presence and translatedness are different properties. Carries an allowlist for keys correctly identical everywhere (pure format strings, brand names, ISO designations) — each with a reason, so the list records decisions rather than silencing findings.",
|
||||
"check:i18n-stopped-scope": "createAppI18n() must succeed while a STOPPED effect scope is current: a P0 seen live (Plugin Studio gallery, every tab after the first threw 'SyntaxError: Unexpected error' from createI18n, because Vue 3.5 makes a new scope inactive under a stopped one). A bare createI18n() in the same scope must throw first, or the harness proves nothing. Also asserts a bundle's second provide('appI18n') raises no warning.",
|
||||
"check:i18n-treeshake": "each built bundle carries every translation area its code asks for: every area-prefixed key literal in <bundle>.js belongs to an area the build included (the build narrows library/i18n's catalogue glob per bundle, internal/i18n-areas.mjs). Headless.",
|
||||
"check:instance-config-keys": "a plugin reads its instance config keys (show-search, show-filters) from profile-config in both spellings, with the prop winning. By the regions owner; mutation-tested. Headless; in gates:post.",
|
||||
"check:kit-options-reachable": "every option a shared kit composable ACCEPTS must be honoured somewhere in its body. Written because I nearly shipped one: kit landing 3 gave useSelectionCamera a `coords` option so a dashboard whose rows spell their coordinate differently could say so, and it was accepted in the signature and never passed to selectionBounds - a caller could set it, type-check clean, build clean and watch it do nothing. That is the worst shape a missing feature takes, because it READS as supported and nobody files a bug against a knob they assume they misused; this tree already paid for it once with a defineProps on a component the custom element never binds. It matters more for the kit than for one dashboard: the whole promise of SPEC-DASHBOARD-KIT is that a config produces a working dashboard, and a parameter that is merely present turns that promise into a guess. Strips comments from BOTH the signature and the body before searching - the first version was satisfied by the comment beside the fixed call site, and the second read comment prose as option names. Not a type checker: it catches a completely unused option, which is the failure invisible from outside.",
|
||||
"check:language-code-link": "the map's language selection reaches the grid as a locale-stable CODE, or narrows nothing at all. rega measured Languages > Language > \"Arabic\" taking the grid 2,107 -> 0 in BOTH locales: the emitter looked for a row whose language LABEL equalled the selected name, but a language tier's label is derived by splitting the API label at the comma, and no row reads \"Arabic\" - they read \"Arabic, Sudanese\", \"Arabic, Levantine\", 22 codes in all. Lifts the emitter's match rule and the composite's gridLanguage() from the shipping source (never re-typed) and runs them against a committed fixture of the 864 distinct language label/value pairs: the emitter's rule must agree with the legend's own three-arm filter on every (label, tier) pair, every tier must select at least one row, a tier covering one code must emit it, a tier covering several must emit NONE and leave the axis unfiltered. No network, no browser; --refresh-fixture re-takes the fixture.",
|
||||
"check:language-code-link": "the map's language selection reaches the grid as a locale-stable CODE, or narrows nothing at all. Measured: Languages > Language > \"Arabic\" taking the grid 2,107 -> 0 in BOTH locales: the emitter looked for a row whose language LABEL equalled the selected name, but a language tier's label is derived by splitting the API label at the comma, and no row reads \"Arabic\" - they read \"Arabic, Sudanese\", \"Arabic, Levantine\", 22 codes in all. Lifts the emitter's match rule and the composite's gridLanguage() from the shipping source (never re-typed) and runs them against a committed fixture of the 864 distinct language label/value pairs: the emitter's rule must agree with the legend's own three-arm filter on every (label, tier) pair, every tier must select at least one row, a tier covering one code must emit it, a tier covering several must emit NONE and leave the axis unfiltered. No network, no browser; --refresh-fixture re-takes the fixture.",
|
||||
"check:language-tree-layout": "the library language-tree layout builder (library/scripts): families packed without overlap, every roster language reachable. Headless; in gates:pre.",
|
||||
"check:layout-key-namespaced": "two dashboards on one page must not share a saved layout. useDashboardLayout persists the four divider positions to localStorage, and until kit landing 2 the key was a single constant — invisible while there was exactly one dashboard, and SHARING THE ENGINE IS WHAT MAKES A SECOND ONE POSSIBLE, so SPEC-DASHBOARD-KIT section 8 named it as that landing's risk. The failure is quiet rather than loud: drag a divider in one dashboard, reload, and the other has moved. Asserts the shell passes no bare literal, EVALUATES the real expression with two different dashboard ids to prove the key varies (never a retyped copy of the rule), and that the pre-namespacing key is READ and never written — namespacing must not silently discard a reader's layout, and a migration that also deletes cannot be rolled back. Static; no DOM, no browser.",
|
||||
"check:leaf-engine-per-provider": "every leaf in the manifest at ?provider=maplibre makes ZERO requests to any *.mapbox.com host and a map leaf still draws; at its default every map leaf draws with the engine it drew with before (read from the canvas that drew). --baseline <tree> compares against another tree (e.g. main). Browser gate, @browser-slot-max-hold 2700.",
|
||||
|
|
@ -517,7 +509,7 @@
|
|||
"check:legend-export-reachable": "the Export affordance must be reachable at BOTH widths. Selects a leaf row first (the control is gated on selectedNode) and opens the mobile sheet first (it starts closed). Asserts a REAL BOX, not CSS visibility — on mobile the elements report visibility:visible at 0x0. PASSES at both widths since the mobile sheet was given :export-enabled and :tabs. SKIPs when nothing is served.",
|
||||
"check:legend-logical-insets": "the legend components position themselves with LOGICAL insets (inset-inline-start/end, padding-/margin-inline-*). A physical left/right does not mirror under dir=\"rtl\" while the map controls (inset-inline-end) do, so in Arabic the controls overlapped a legend that stayed left, on Simple Research, Prayer Progress and UPG-100 at once. Symmetric rules (left:50%, left:0 with right:0), auto resets and a short ALLOWED list with reasons pass.",
|
||||
"check:legend-logical-insets:self-test": "poisons .stl-panel back to left:8px and asserts check:legend-logical-insets notices.",
|
||||
"check:legend-mirrors": "the research-dashboard legends must MIRROR under RTL, and their labels must never become their values. rega's Arabic survey found them on the wrong edge with 70 of 85 letters Latin. Two faults that look like one and fail in opposite directions if fixed carelessly. (1) A physical `right:8px` pins a box to the physical edge in every locale; the logical property plus the auto on the other side is the fix, the pair being what cancels a physical inset inherited from a control container. Asserted on the map legend and on research-map's geocoder column, which exists only to share an edge with the legend beside it. (2) The English was NOT chrome: the religion donut's legend renders a family name DERIVED in English by library/colors/religion.js, the same table people-groups-grid re-derives per row so both ends byte-match. Translating those VALUES would work in English and match nothing in Arabic, silently, because an empty intersection reads as 'no results'. So it asserts the visible text is projected AND that no projection reached a wire: every :data-key/:data-facet binding in the file, not just the legend's, after a mutation test aimed at one silently hit another. No browser.",
|
||||
"check:legend-mirrors": "the research-dashboard legends must MIRROR under RTL, and their labels must never become their values. An Arabic survey found them on the wrong edge with 70 of 85 letters Latin. Two faults that look like one and fail in opposite directions if fixed carelessly. (1) A physical `right:8px` pins a box to the physical edge in every locale; the logical property plus the auto on the other side is the fix, the pair being what cancels a physical inset inherited from a control container. Asserted on the map legend and on research-map's geocoder column, which exists only to share an edge with the legend beside it. (2) The English was NOT chrome: the religion donut's legend renders a family name DERIVED in English by library/colors/religion.js, the same table people-groups-grid re-derives per row so both ends byte-match. Translating those VALUES would work in English and match nothing in Arabic, silently, because an empty intersection reads as 'no results'. So it asserts the visible text is projected AND that no projection reached a wire: every :data-key/:data-facet binding in the file, not just the legend's, after a mutation test aimed at one silently hit another. No browser.",
|
||||
"check:legend-source": "the numbers the maps SHOW must match the numbers the live SOURCE has. Every other gate checks INTERNAL consistency — a page can agree with itself perfectly while its legend has stopped describing reality. Offline SKIPs (exit 0); only a reachable endpoint that disagrees fails.",
|
||||
"check:library-index": "the DERIVED library menu must not DRIFT from library/. REGENERATES the index into a throwaway tree (the generator, unmodified, over a symlinked library/) and diffs the BYTES — it used to compare mtimes, so `touch` or a rebase turned it red on a byte-identical doc. Fix: bun run library-index",
|
||||
"check:link-rot": "re-tests a stratified sample of links we already VERIFIED, because nothing else ever re-tests one: a publisher can retire a page and the map keeps offering it, still labelled 'link checked'. THREE BANDS, each measured rather than assumed, each with its controls in band every run: plain STATUS where a status is evidence (jesusfilm.org among them — it does 404 on the /watch/jesus/<slug> shape the map ships, and the old exclusion was inherited from the retired ?languageId= URL); API-MAPPED for live.bible.is, whose page answers 200 for any fileset while /api/bibles/<id> answers 404; and REDIRECT-LANDING for fivefish.org, where a published code lands on its numbered language page and an unknown one is swept to /regions. If a control misbehaves it reports PROVED NOTHING rather than a pass. Only knowgod.com is left out — byte-identical page for any URL; the verifier proves those through Cru's API. ~3 minutes, network.",
|
||||
|
|
@ -587,7 +579,7 @@
|
|||
"check:posters-exportable": "every poster the tree offers can be printed. With posterSources on, the tree once shipped only the 28 zone sources, so 350 of 378 posters said \"poster not found (404)\". Each id, from data/posters and the wall's previews, must ship a real WebP (RIFF..WEBP) or a real poster source (its class=\"stage\" element), so a placeholder or a host's fallback page cannot pass. SKIPs when posters are off by config, which check:poster-degrades covers. Reads the built tree; no server.",
|
||||
"check:posterverse-shelf": "Posterverse (the capability-5 poster shell) shows exactly its allow-list and never a gated plugin. Asserts the rail rows per section (Posters: upg-100, country-posters, poster-design; Experimental: wall-of-people-groups, bible-posters), that no row names key-cities or a shell (a key-cities bundle is planted into the built manifest, because the real one withholds gated profiles), that every present row opens the profile it names (no silent fallback to a leaf), that posterverse.meta.json composes equals the rows whose bundle exists, and 11-locale parity of the shell strings. --self-test plants one defect per arm. Headless. Exit 0 pass, 1 fail, 75 no built manifest.",
|
||||
"check:prayer-walk-planner": "the Prayer Walk Planner is a full-page tool that prints a real walking map: the map canvas fills the page below the toolbar in its own leaf AND inside the Research Gallery at 1280x800 and 390x844 (the gallery once collapsed it to 76px / 0px); a real zone (Balat, Istanbul) renders a >=2000px print map that is not blank, saved to OUT=<dir> so a human can read it; ?lang=es gives a Spanish toolbar; 0 page errors. Run inside the shared browser locks. Exit 2 = SKIP.",
|
||||
"check:preset-config-merge": "a preset page (<bundle>/<profile>/<instance>/index.html) keeps every default in its profile's .meta.json config, with the instance's props merged over them (go-int's upg-neighbor presets lost pinHint:false when props replaced the config). Reads the built tree; SKIP without one.",
|
||||
"check:preset-config-merge": "a preset page (<bundle>/<profile>/<instance>/index.html) keeps every default in its profile's .meta.json config, with the instance's props merged over them (upg-neighbor's presets lost pinHint:false when props replaced the config). Reads the built tree; SKIP without one.",
|
||||
"check:profile-table": "the prayer packet's profile TABLE (20 full-width rows: picture, generated description, three boxes; PER_PAGE stays 20): a row stays tall enough for a name plus a two-line description at the smallest fit size, and truncation happens only at a whole word. Character-count, not DOM, so Node and the browser agree. Cannot see the RENDERED row height: that is check:packet-render, a browser gate. By the UPG-100 owner; mutation-tested. Headless; in gates:pre.",
|
||||
"check:profiles-mount": "check:composites-mount over EVERY profile of every bundle and every preset instance (with its props), in en and ar: any setup, watcher or mount error fails it. Headless, ~40 s. A source mount: a build-only error (a minified name hoisted out of order) needs the built bundle and is not covered.",
|
||||
"check:provider-for": "providerFor(map) must answer the same as the raw `map instanceof window.maplibregl.Map` sniffs it replaced (C3) - on BOTH engines AND on the AMBIGUOUS input that is neither, where the originals fell through to mapbox/false. Carries an executable proof that the obvious isMapboxMap() rewrite INVERTS that fall-through while agreeing on both real engines, which is how it would have changed a poster attribution unnoticed. Headless, source-only. engineGlobal is extract-and-executed from source because useMapInstance.js cannot be imported in plain Node.",
|
||||
|
|
@ -596,7 +588,7 @@
|
|||
"check:quoted-scripts": "documentation/README-SECTION-3-cli-commands.md QUOTES the scripts block verbatim; it had silently drifted TWICE (a stale bunx cap:sync, and build:unlocked missing check:public-hygiene + check:label-joins). package.json is what RUNS — update the DOC.",
|
||||
"check:readme-size": "the README's web-folder row (N files, M MiB) against the built public/<tree>: fails when either number is more than 10% off and prints the row to paste. The folder grows as maps are added, so a batch of posters or data trips it on purpose: the row had gone stale twice.",
|
||||
"check:reduced-motion": "a library component that animates must carry its OWN @media (prefers-reduced-motion) rule. A page-level rule does not cross a shadow boundary, and no generated leaf ships one anyway (measured: 0 across the built tree), so there is nothing to inherit. Also asserts the rule lives where the styles do: a component injecting through useShadowStyles whose rule sits only in a <style> block has written a rule that never reaches the shadow root.",
|
||||
"check:region-fill-repaint": "the region fill must be repainted after its data arrives, in EVERY arrival order. ★ WHAT THIS NOW GUARDS, narrowed by measurement after it landed: the ordering is not merely uncaused-by-the-grey-tab, it is UNCONSTRUCTIBLE today — region-dashboards.vue:1091 gates the whole map subtree behind v-if=\"loading\", and loadRegions assigns points inside the try while loading=false is in the finally, so the map can never mount with empty points (rega: 0 grey in 36 mounts, including the API held back 20s). So this is a guard against a FUTURE refactor that drops that v-if data gate, not a live path. The grey Regions tab stays OPEN and is not this. fill-color is SNAPSHOTTED into the layer when addLayers creates it, from a match-expression built out of props.points — with no points that expression is a flat no-data grey, and applySubject guards every write on getLayer() so a change arriving before the layer exists is dropped and never retried. Models the arrival orders (asserting at least one DOES grey without the repaint, else the model proves nothing) and asserts the source carries the repaint INSIDE the .then() block that received the boundaries, brace-matched rather than within a character distance — a 1600-char window passed on an unrelated applySubject 1200 chars downstream.",
|
||||
"check:region-fill-repaint": "the region fill must be repainted after its data arrives, in EVERY arrival order. ★ WHAT THIS NOW GUARDS, narrowed by measurement after it landed: the ordering is not merely uncaused-by-the-grey-tab, it is UNCONSTRUCTIBLE today — region-dashboards.vue:1091 gates the whole map subtree behind v-if=\"loading\", and loadRegions assigns points inside the try while loading=false is in the finally, so the map can never mount with empty points (measured: 0 grey in 36 mounts, including the API held back 20s). So this is a guard against a FUTURE refactor that drops that v-if data gate, not a live path. The grey Regions tab stays OPEN and is not this. fill-color is SNAPSHOTTED into the layer when addLayers creates it, from a match-expression built out of props.points — with no points that expression is a flat no-data grey, and applySubject guards every write on getLayer() so a change arriving before the layer exists is dropped and never retried. Models the arrival orders (asserting at least one DOES grey without the repaint, else the model proves nothing) and asserts the source carries the repaint INSIDE the .then() block that received the boundaries, brace-matched rather than within a character distance — a 1600-char window passed on an unrelated applySubject 1200 chars downstream.",
|
||||
"check:religion-label-not-value": "the library's two RELIGION_FAMILIES sites (useLegendData, useSearch) show religionFamilyLabel() to the reader, while the value a filter or search matches on stays the canonical English family name: a translated label must never become a wire value.",
|
||||
"check:ring-bands": "library/components/charts/ringBands.js keeps the four promises the ring target rests on: every pin inside its own ring band, no two pins closer than the pin diameter, no pin inside a ring label's keep-out arc, and byte-identical output for the same input (input ORDER included, so a caller that re-sorts its rows does not move every dot). Each arm carries a negative control. One arm exists because this gate PASSED the bug it now catches: uncapped, a long label on ring 1 claimed 96% of that small circle, capacity went to zero and every group on the innermost ring was reported unplaced — all other arms stayed green, because 'reported rather than silently dropped' is not the same promise as 'the ring draws'. Labels now truncate, and say how many characters fit.",
|
||||
"check:ring-vocabulary": "the ring names the printed packet uses (packet-strings.json) agree with the ones the screen uses (gospelInternet.json) in every locale, so a packet and the map cannot name the same ring two ways. By the UPG-100 owner. Headless; in gates:pre.",
|
||||
|
|
@ -606,7 +598,7 @@
|
|||
"check:selection-kinds": "the UPG-100 selection kinds (by the UPG-100 owner, with d3ec8af7): each kind of selection the list offers resolves to the groups it names. Headless; in gates:pre.",
|
||||
"check:selector-fields": "every UPG-100 selector's fields, read FROM the evaluator (not a table in this file), must be requested by every fetch that evaluates it. A missing field matches nothing and prints an EMPTY packet with no error: eight zones did, until the fetch carried wagf_region/wagf_block. Headless; in gates:pre. :self-test has 6 cases.",
|
||||
"check:snippet-engine": "every map profile's DEFAULT copy-paste snippet preloads MapLibre (the engine it draws with) and names no api.mapbox.com URL, and its embed.scriptMapbox variant preloads Mapbox AND sets provider:mapbox; a non-map profile has neither. The default used to preload Mapbox, so an unedited paste fetched an engine it never used and made a Mapbox request from a keyless page. Reads the built manifest; --self-test plants the old snippet and a provider-less variant.",
|
||||
"check:setup-order": "a line's POSITION in a setup block is not the MOMENT it runs, and two P0s from the Driver's console came from that gap. ARM A: `watch(..., { immediate: true })` runs during setup, so anything it reaches must be declared ABOVE it - research-map.vue threw `Cannot access '_lastFitKey' before initialization` and blanked the whole map, because the watcher called a HOISTED function 350 lines down whose own constants were still in their temporal dead zone. A hoisted function promises that its NAME resolves early, not that its BODY can run early, and that asymmetry is invisible from both ends. ARM B: a lifecycle registrar closes over Vue's current instance, which exists only while setup is on the stack, so `onMounted(async () => { await init(); onBeforeUnmount(clean) })` drops the registration silently - DashboardMapPanel.vue leaked a ResizeObserver on document.body plus a window listener, warning only, feature working, and research-map.vue had a second copy this gate found. The fix is never 'move the line': declare the handle at setup scope and tear it down in a hook registered before the first await. Its own false positives are the interesting part - it went 6 hits -> 1 over three rounds, and every one it dropped was prose or shadowing read as code: `onBeforeUnmount` inside a console.error STRING, `table` inside `new Error(`table not found (${r.status})`)` in four files, and a `const [pins, byCountry]` destructured INSIDE the function, shadowing a computed 220 lines away. So it blanks comments, string text and template text (keeping ${...}, which is code) and subtracts every name a span declares for itself. Self-tests on both shipped defects, both fixes, and all three decoys; exits 3 if any arm goes blind.",
|
||||
"check:setup-order": "a line's POSITION in a setup block is not the MOMENT it runs, and two P0 bugs came from that gap. ARM A: `watch(..., { immediate: true })` runs during setup, so anything it reaches must be declared ABOVE it - research-map.vue threw `Cannot access '_lastFitKey' before initialization` and blanked the whole map, because the watcher called a HOISTED function 350 lines down whose own constants were still in their temporal dead zone. A hoisted function promises that its NAME resolves early, not that its BODY can run early, and that asymmetry is invisible from both ends. ARM B: a lifecycle registrar closes over Vue's current instance, which exists only while setup is on the stack, so `onMounted(async () => { await init(); onBeforeUnmount(clean) })` drops the registration silently - DashboardMapPanel.vue leaked a ResizeObserver on document.body plus a window listener, warning only, feature working, and research-map.vue had a second copy this gate found. The fix is never 'move the line': declare the handle at setup scope and tear it down in a hook registered before the first await. Its own false positives are the interesting part - it went 6 hits -> 1 over three rounds, and every one it dropped was prose or shadowing read as code: `onBeforeUnmount` inside a console.error STRING, `table` inside `new Error(`table not found (${r.status})`)` in four files, and a `const [pins, byCountry]` destructured INSIDE the function, shadowing a computed 220 lines away. So it blanks comments, string text and template text (keeping ${...}, which is code) and subtracts every name a span declares for itself. Self-tests on both shipped defects, both fixes, and all three decoys; exits 3 if any arm goes blind.",
|
||||
"check:sfc-parse": "every .vue script block must parse as the BUILD parses it — catches the LIVE-CSS backtick that check:template-literal cannot see (it reads CSS comments only). 0 false positives on 83 files, where 3 lookahead heuristics scored 98 and 136.",
|
||||
"check:shadow-css-parses": "the CSS we assemble as JS STRINGS must parse as CSS — brace balance and declarations stranded outside any rule. check:sfc-parse proves only that the JS parses, so it passed a stylesheet with 9 orphaned declarations and braces at 12 open / 21 close (REGLIB batch 1); nothing else in the tree validates CSS syntax. Reads every useShadowStyles() argument off the @babel/parser AST the build already uses, because three hand-rolled lexers each produced a confident FALSE RED: a /* CSS comment */ read as a declaration, an apostrophe in a .vue prose header opening a phantom string literal, and a regex literal containing a quote. 9 self-tests run inline on every invocation, including the exact batch-1 damage, so it cannot silently stop being able to fail.",
|
||||
"check:shadow-style-labels-unique": "every useShadowStyles call has its OWN label: the label keys the <style>, and a second component with the same label overwrites the first one's CSS in a shared shadow root (the regions list lost its sheet this way). Global, not per-root, so a copied component is caught when it is copied. AST walk cross-checked by a token count.",
|
||||
|
|
@ -641,7 +633,7 @@
|
|||
"dropin": "copy the built tree (dx-maps/) into your site's public/ (vite.doxa-maps-build-config.json dropin.dest, default ../doxa-life/public), so ONE server serves the site and every map. Only the drop-in is touched: the site's own files stay, and stale files inside dx-maps/ are removed. It never prunes: --prune into a folder named public/ is refused, because it would delete the site.",
|
||||
"gallery": "writes public/__gallery/index.html — the ARCHIVED bundle-index gallery, from public/doxa-maps/manifest.json. It is NO LONGER the root page: as of 2026-09-22 `/` mounts <plugin-studio> in BOTH trees (the built root page is written by `bun run tree`), and the gallery is the opt-in index at /__gallery. Archived, not deleted — the dev server imports renderGallery()/readBuildConfig() from this module.",
|
||||
"gen:iso3-to-alpha2": "derive library/data/iso3-to-alpha2.json from vendored CLDR (48.2.2, Unicode-3.0) joined against the 242 codes in library/data/countries.json, so Intl.DisplayNames can produce a LOCALIZED country name — it takes alpha-2 and our data speaks alpha-3. Offline, no dependency. GRADES ITS OWN OUTPUT and writes nothing on failure: 242 coverage, injectivity, and KOS -> XK as the one documented exception (Kosovo has no ISO assignment). Cross-checks the 176-country research spine map and REPORTS disagreements without rewriting it — that file drives fetches and is not this generator's to edit.",
|
||||
"internet:adm1": "regenerate data/tables/internet-by-adm1.json (internet use by first-level area; India from NFHS-5 2019-21, CC BY 4.0) from data/internet/ through a strict name crosswalk to the shipped gb-IND-ADM1 boundaries. Fails on an unmatched name, never guesses. Generic rows (iso3, adm1, measure, sex, area, value, source, year) so other plugins and go-int can reuse it.",
|
||||
"internet:adm1": "regenerate data/tables/internet-by-adm1.json (internet use by first-level area; India from NFHS-5 2019-21, CC BY 4.0) from data/internet/ through a strict name crosswalk to the shipped gb-IND-ADM1 boundaries. Fails on an unmatched name, never guesses. Generic rows (iso3, adm1, measure, sex, area, value, source, year) so other plugins can reuse it.",
|
||||
"library-index": "regenerate documentation/reference-library-index.md — the DERIVED menu of the 84 reusable library pieces an author can import instead of rebuilding. Was UNREACHABLE, and the index did not exist at all: its header promised 'can't drift from the code' while nothing ever ran it.",
|
||||
"lists": "regenerate data/people-groups/lists/<id>.json, the DOXA slugs in each people-group list of library/lists/lists.json, from the IMB CSV and the DOXA snapshot. Re-run after either changes.",
|
||||
"packet-fixture": "regenerate the committed records subset (smallest zone, image_url stripped) that lets CI build a packet at all — out/ is gitignored, so a clean checkout has neither the records cache nor any packet.",
|
||||
|
|
|
|||
Loading…
Reference in a new issue