diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 46f1ee7fe..c71a2e569 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -10,10 +10,10 @@ "source": { "source": "github", "repo": "dravensoft-dev/arena", - "ref": "v11.0.2" + "ref": "v11.0.3" }, "description": "Arena, Dravensoft's design system, as an Agent Skill: design tokens, React and Angular primitives, a shared Tailwind layer, guidelines, and a style plugin any project can replace. It carries the design language and not the skin, so any project brings its own palette and fonts.", - "version": "11.0.2", + "version": "11.0.3", "author": { "name": "Dravensoft" }, diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index ea38f5b1a..eaca50de0 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "arena", "displayName": "Arena — Dravensoft Design System", - "version": "11.0.2", + "version": "11.0.3", "description": "Build interfaces with Arena, a token-driven design system: design tokens, React and Angular component libraries, a shared Tailwind layer, guidelines, and a style plugin any project can replace. It carries the design language and not the skin, so any project brings its own palette and fonts.", "author": { "name": "Dravensoft" diff --git a/AGENTS.md b/AGENTS.md index f67616fd4..3964385f6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -237,7 +237,8 @@ two copies and never reads either for meaning. Verify with are allowed, in a gate's own reason string, or in the component's `.prompt.md`. **Somewhere a stale copy of it fails something.** -`bun run check:docs` holds the size rule, the punctuation rule and the comment rule, and +`bun run check:docs` holds the size rule, for a document and for a table cell, the punctuation +rule and the comment rule, and `bun run check:citations` holds every path that prose names to existing. **The present-tense rule is the one no gate holds**, because nothing mechanical can judge it. diff --git a/README.md b/README.md index 914ce490a..c46ff43d2 100644 --- a/README.md +++ b/README.md @@ -198,7 +198,7 @@ package publishes only when something it ships changed. that means for an upgrade. ## Latest project artifacts -- **Repo/Claude Code plugin**: 11.0.2 +- **Repo/Claude Code plugin**: 11.0.3 - [npm React package](https://www.npmjs.com/package/@dravensoft/arena-react?activeTab=versions) - [npm Angular package](https://www.npmjs.com/package/@dravensoft/arena-angular?activeTab=versions) - [npm contracts package](https://www.npmjs.com/package/@dravensoft/arena-contracts?activeTab=versions) diff --git a/scripts/AGENTS.md b/scripts/AGENTS.md index 43243812a..942d1068d 100644 --- a/scripts/AGENTS.md +++ b/scripts/AGENTS.md @@ -79,6 +79,11 @@ stdin or are imported. silently, because the wrong path still exists. That module is the one place that counts, which is why moving *it* is the one move needing care. +**Never spell what a walk skips because it is not this repository.** Compose +`withForeignTrees(...)` from `lib/arena/foreign-trees.ts` and add only what the walk skips for +its own reasons. A skip set spelled per walk goes short in some of them, and a walk missing +`.claude` reads a git worktree there as a second copy of the tree. + **A library never imports a gate.** `lib/` is the bottom of the graph: `arena/layers.ts`, `core/arena-tokens.ts` and the rest are there because more than one gate reads them, and a gate reaching down is the only direction allowed. Across domains the same holds in both diff --git a/scripts/build/arena/build-site.ts b/scripts/build/arena/build-site.ts index 7ff97bbcb..56366b0b9 100644 --- a/scripts/build/arena/build-site.ts +++ b/scripts/build/arena/build-site.ts @@ -28,6 +28,7 @@ import { HERO_SHEET, HERO_SOURCE, HERO_FILE, HERO_WIDTH, HERO_HEIGHT, heroPage, heroStyles, } from '../../lib/arena/hero-page.ts'; import { LLMS_INDEX, layerFile, index, corpus, servedDocs } from '../../lib/arena/llms-index.ts'; +import { withForeignTrees } from '../../lib/arena/foreign-trees.ts'; export const node = { name: 'build:site', @@ -50,7 +51,7 @@ export const OG_SOURCE = 'og.html'; export const OG_WIDTH = 1200; export const OG_HEIGHT = 630; -export const NEVER_COPIED = new Set(['node_modules', '.git']); +export const NEVER_COPIED = withForeignTrees(); export const BENCHES_REPOSITORY = 'https://github.com/dravensoft-dev/arena-web-benches'; diff --git a/scripts/check/arena/AGENTS.md b/scripts/check/arena/AGENTS.md index d29e0184d..422b7b15b 100644 --- a/scripts/check/arena/AGENTS.md +++ b/scripts/check/arena/AGENTS.md @@ -5,32 +5,32 @@ The domain for a gate that reads two or more layers at once, or the repository r | gate | fails when | | --- | --- | | `check-all.ts` | not a gate, but the runner: `GATES` is the registry, and `testStep()` is the **single authority** for how the test suite is invoked. Read the invocation there rather than reconstructing one; a narrowed run matching fewer files is indistinguishable from one matching all of them. | -| `check-docs.ts` | a `.md` exceeds its limit, 60,000 characters unless `SIZE_ALLOWANCE` raises it, a `.md` punctuates its prose with an em dash, or a hand-written source carries a comment it is not allowed: `scripts/` and tests may carry one header of at most 10 lines, everything else none. `SIZE_EXEMPT` names what is exempt from the limit by charter, `SIZE_ALLOWANCE` what holds to a higher one instead, and `PROSE_EXEMPT` the one tree exempt from the punctuation rule, each with its reason. **An allowance is not an exemption**: the document is still measured against the raised number, and one that falls back inside the shared limit fails as a **stale allowance**, as does one whose document has moved or gone. That is what keeps a raised ceiling from quietly becoming permanent. The measure is characters and never bytes, so reading a file's length by hand means `node -e "console.log(require('fs').readFileSync('X','utf8').length)"` and never `wc -m`, which reports a document of multi-byte characters hundreds over a limit it is comfortably under. Both rules read by lexing, so a `//` inside a string, a regex or a template literal is never mistaken for a comment, a `@ts-` or `eslint-` directive is a directive rather than the file's one allowance, an em dash inside a fence or a code span is the document quoting code, and a fence closes only on a run of its own character at least as long as the one that opened it; a `.generated.` file is skipped unread. It also holds the branch boundary, over every consumer document: a `.prompt.md` anywhere, an `INDEX.md` under `frameworks/`, and an `ENTRY.md` under `plugin-style-store/catalogue/`, which is a consumer document although it sits outside the skill tree because it is what a project reads to choose the appearance it starts from. That classification is also what keeps it out of `check-community.ts`'s contributor set, so Context7 hands it to a consumer asking what they can start from instead of hiding it as repository furniture. Citing a path under `scripts/`, a `AGENTS.md` under `contracts/` or `frameworks/`, or `frameworks/PACKAGING.md` sends a reader somewhere the router promised they would not have to go. `CONTRIBUTOR_PATHS` carries a reason per shape, and `BRANCH_SWITCH` carries the single carve-out: the root `SKILL.md`, which names the contributor branch because naming it is how it sends a contributor away. Nothing below it has anybody left to redirect, so a document there needing an exception is a rule stated on the wrong branch. **`RULE_OWNERS` is the same boundary run the other way, on CONCEPTS rather than paths**: a rule is assigned to one branch with its reason, and the gate fails the phrase appearing on the other. It is a register rather than a mechanism, so it covers what is declared and nothing else, and what is not declared is unheld and said to be; an entry no document on its own branch states any more fails as a stale registration. It exists because a `R4 violation` written into a contract's `description` reached a published `.d.ts`, where a consumer read a rule number with no way to learn what it meant. **`FOREIGN_CODE` runs the same boundary over what a document OFFERS rather than what it says**: a fenced example writing another design system's markup fails, and prose naming one passes, because a Don't saying what Arena is not teaches nobody to install it while an example is the half a reader copies. The two halves are the two the Markdown lexer already separates, so a code span is prose's neighbour and a fence is not. **It also holds the one count a consumer document carries**: each layer's `PACKAGE.md` states how many components the package ships, and `COMPONENT_COUNT_CLAIM` compares that number against `frameworks/Components.json`. The count stays because the npm page is where a reader meets it first, and it is held because the two pages had drifted four and nine components behind the tree with every gate green. | +| `check-docs.ts` | a `.md` exceeds its limit or holds a table cell over `MAX_CELL_CHARS`, prose carries an em dash, a hand-written source carries a comment it is not allowed, or a consumer document crosses the branch boundary. **Size**: 60,000 characters unless `SIZE_ALLOWANCE` raises it; `SIZE_EXEMPT` names what is exempt by charter and `PROSE_EXEMPT` the one tree exempt from the punctuation rule, each with its reason. **An allowance is not an exemption**: the document is still measured against the raised number, and one that falls back inside the shared limit, or has moved or gone, fails as a **stale allowance**. The cell cap exists because a document cap does not cap a row. The measure is characters, so read one by hand with `node -e "console.log(require('fs').readFileSync('X','utf8').length)"` and never `wc -m`. **Comments**: `scripts/` and tests may carry one header of at most 10 lines, everything else none. Every rule here reads by lexing, so a `//` inside a string is not a comment, a `@ts-` or `eslint-` directive is not the allowance, and an em dash in a fence or a code span is quoted code. **The branch boundary**, over every consumer document (a `.prompt.md`, an `INDEX.md` under `frameworks/`, an `ENTRY.md` under `plugin-style-store/catalogue/`): `CONTRIBUTOR_PATHS` fails a citation of a path under `scripts/`, an `AGENTS.md` under `contracts/` or `frameworks/`, or `frameworks/PACKAGING.md`, and `BRANCH_SWITCH` is the carve-out for the root `SKILL.md`; `RULE_OWNERS` runs the boundary on concepts, failing a registered rule's phrase on the branch that does not own it; `FOREIGN_CODE` fails a fenced example writing another design system's markup, while prose naming one passes. `COMPONENT_COUNT_CLAIM` holds the one count a consumer page carries, each `PACKAGE.md`'s, against `frameworks/Components.json`. | | `check-graph.ts` | the declared script graph disagrees with itself: a script under `build/`, `generate/` or `check/` that neither declares a node nor is named in one of the two lists that say it is out, an entry in either list that has since subscribed or gone, a node whose name is no npm script, a `feeds` naming no node or naming itself, a cycle, an artifact two nodes write, a node reading nothing that is there, or a spec whose directory holds no file at all. **The rule it exists for is the unsubscribed reader**: node B reads what node A writes and A does not list B in `feeds`, so a run would keep B's last answer over an artifact A has just rewritten. Both halves are held, so a `feeds` entry no artifact carries fails too, an edge nobody maintains being the same defect read backwards. A spec matching no file today whose directory IS there is reported and allowed: refusing it would mean a declaration can only describe the tree as it stands, and the first file of a new kind would be read by nobody. What it asserts lives in `scripts/graph/graph-problems.ts`, with the rest of the graph; this file is a print and an exit. | -| `check-portability.ts` | a script assumes one operating system. Every rule is a **ban with a named owner**, never a judgement about whether a construct is correct, and that is what keeps its false-positive rate zero by construction: it does not ask whether spawning a bare name is wise, it asks where that may live. `process.platform` belongs to `lib/arena/platform.ts`, an absolute path to the browser candidate table, a bare-name spawn to `host-binary.ts`, a `node_modules/.bin` path to `node-bin.ts`, a process-group signal to `chromium.ts`, a raw `symlinkSync` to `platform.ts`, and a separator spelling or a prefix containment to `utils/posix-path.ts`. Locale ordering is owned by **nowhere**, and that empty list is the claim. **A stale owner fails too**, so an exception cannot outlive the thing it was written for. The subject is scripts and **not suites**: a suite naming `win32` and a `C:` path is doing its job, and the win32 branches are covered from a Linux runner precisely because it does. Matches are filtered through the same lexer `check:docs` uses, so a construct named in a comment is described rather than performed; the two rules whose construct **is** a string say so in their own declaration, since skipping literals for those would skip the rule, and its suite plants one violation per rule because a gate only ever seen to pass is a gate seen to do nothing -- that fixture is what caught the `.bin` rule not firing at all. It also holds `pr.yml`'s matrix equal to `ci/arena/supported-os.ts` -- **names and blocking flags both**, so a leg cannot quietly stop counting nor a supported platform quietly stop gating -- and holds `pr-gate`'s `needs` equal to **every other job in that file**, stated as the complement rather than as a list somebody keeps in step: a job the one required check does not name is a job whose failure branch protection never hears about, which is exactly what a matrix living in a workflow of its own bought, since `needs` cannot cross workflows. It fails unless **exactly one** workflow names `CHROME_PATH`, exporting it everywhere being what made the candidate list unreachable. A separate set of rules is about the tracked **path** rather than the file, which is why no gate over content could see them: a **reserved device name** (`con`, `nul`, `com1`, whatever the extension or case), a character NTFS refuses, a segment ending in a dot or a space that Windows silently rewrites, a **case-only collision** that a case-insensitive filesystem resolves by overwriting one with the other, and a path past `MAX_PATH` once `CLONE_ROOT_BUDGET` is spent. None of those exist today, and the emptiness is the claim; the run states the longest tracked path against the budget so the margin is a number rather than a hope. | -| `check-workflow-scripts.ts` | a script a job runs cannot run in it. A job with no `bun install` still runs scripts, and Bun answers a bare specifier there by fetching it at whatever major the registry serves rather than the one `bun.lock` pins. That is not a hypothetical: the publish guard reached `typescript` through the assembler, resolved 7.x against a tree pinned to 6.x, and threw inside `< <(...)`, where `set -e` cannot see it -- so it read no paths, **git reads no path as every path**, and two packages were republished from a diff of the whole tree. Either half satisfies the rule: install first, or import nothing outside `node:` and this tree. It reads a **job** and not a workflow, since installing in one says nothing about another; it follows `bun run` through `package.json` so a name cannot hide a path, and stops rather than hangs on two names that run each other; and it takes specifiers off the **parse**, because `export const MANIFEST_IMPORT = /import manifest from '...'/ ` is a regular expression whose source is an import statement, and every scan short of a parser -- this repository's own and typescript's `preProcessFile` both -- reports its character class as a package. | +| `check-portability.ts` | a script assumes one operating system. Every rule is a **ban with a named owner**, never a judgement about whether a construct is correct, which keeps its false-positive rate zero by construction: `process.platform` belongs to `lib/arena/platform.ts`, an absolute path to the browser candidate table, a bare-name spawn to `host-binary.ts`, a `node_modules/.bin` path to `node-bin.ts`, a process-group signal to `chromium.ts`, a raw `symlinkSync` to `platform.ts`, and a separator spelling or a prefix containment to `utils/posix-path.ts`. Locale ordering is owned by **nowhere**, and that empty list is the claim; **a stale owner fails too**. The subject is scripts and **not suites**, since a suite naming `win32` and a `C:` path is how the win32 branches are covered from a Linux runner. Matches go through the lexer `check:docs` uses, so a construct named in a comment is described rather than performed, and the suite plants one violation per rule, because a gate only ever seen to pass is a gate seen to do nothing. It also holds `pr.yml`'s matrix equal to `ci/arena/supported-os.ts`, names and blocking flags both; holds `pr-gate`'s `needs` equal to every other job in that file, stated as the complement, since a job the one required check does not name is a failure branch protection never hears about; and fails unless exactly one workflow names `CHROME_PATH`. **The tracked path** has rules of its own: a reserved device name, a character NTFS refuses, a segment ending in a dot or a space, a case-only collision, and a path past `MAX_PATH` once `CLONE_ROOT_BUDGET` is spent. None exists, the emptiness is the claim, and the run prints the longest tracked path against the budget. | +| `check-workflow-scripts.ts` | a script a job runs cannot run in it. A job with no `bun install` still runs scripts, and Bun answers a bare specifier there by fetching whatever major the registry serves rather than the one `bun.lock` pins; inside `< <(...)`, where `set -e` cannot see it, the throw reads as a list of no paths, and **git reads no path as every path**. Either half satisfies the rule: install first, or import nothing outside `node:` and this tree. It reads a **job** and not a workflow, since installing in one says nothing about another; it follows `bun run` through `package.json` so a name cannot hide a path, and stops rather than hangs on two names that run each other; and it takes specifiers off the **parse**, because a regular expression whose source is an import statement reads as an import to every scan short of a parser. | | `check-generated.ts` | a file whose header says a script writes it does not say so in its name, or a `.generated.` file is neither tracked nor covered by a reason-carrying ignore entry. Both directions fail silently otherwise: an unnamed output is read as hand-written, and an ignored payload file ships a tag whose `intro/styles.css` `@import`s resolve to nothing. | -| `check-skill-spec.ts` | the consumer branch stops answering the Agent Skills specification: a second tracked file under the reserved name, a skill carrying no frontmatter, a `name` that is not lowercase alphanumeric separated by single hyphens or does not match the directory holding it, an empty or over-long `description`, a frontmatter key neither the standard nor `HARNESS_KEYS` declares, a value that does not parse as YAML because an unquoted scalar carries a colon and a space, or a body over the ceiling the standard recommends. **The count is the assertion rather than a tidiness rule**: a file carrying that name and no frontmatter is not a lesser skill but a broken one to anything that globs for the name, which is what the npm convention does, so seventeen index documents under it were seventeen skills that fail to parse. The subject is what git tracks, since the plugin is served from the tag where nothing runs a build and a file outside the index reaches no reader. **`HARNESS_KEYS` is empty, and the emptiness is the claim**: a key the standard refuses costs every client that validates the frontmatter, and the one this skill carried set `user-invocable` to the value it already defaults to. An entry the skill stops carrying fails as a stale departure. The YAML rule is the one the standard's own reference validator found and this gate did not: the description had read `not the skin: it ships`, which ends a plain scalar and turns the rest into a mapping, so the frontmatter had never parsed. A run finding nothing in the index fails rather than passing over a branch that reaches no clone. | +| `check-skill-spec.ts` | the consumer branch stops answering the Agent Skills specification: a second tracked file under the reserved name, a skill carrying no frontmatter, a `name` that is not lowercase alphanumeric separated by single hyphens or does not match the directory holding it, an empty or over-long `description`, a frontmatter key neither the standard nor `HARNESS_KEYS` declares, a value that does not parse as YAML because an unquoted scalar carries a colon and a space, or a body over the ceiling the standard recommends. **The count is the assertion rather than a tidiness rule**: a file carrying that name and no frontmatter is a broken skill to anything that globs for the name, which is how the npm convention finds one. The subject is what git tracks, since the plugin is served from the tag where nothing runs a build. **`HARNESS_KEYS` is empty, and the emptiness is the claim**: a key the standard refuses costs every client that validates the frontmatter, and an entry the skill stops carrying fails as a stale departure. `skills-ref validate` is the standard's own second opinion and worth running by hand. A run finding nothing in the index fails rather than passing over a branch that reaches no clone. | | `check-skills.ts` | `frameworks/INDEX.md` or a layer's own `INDEX.md` stops matching a fresh emit, or one falls out of the git index. The index tree is tracked rather than built, because the plugin is served from the git tag where nothing runs a build, so a stale copy is a wrong answer handed to every reader of that tag with every other gate green. Tracking is this gate's to assert because `check:generated` scans no `.md`. Fails on an empty declaration too, rather than comparing an index of nothing against an index of nothing. | -| `check-support.ts` | a consumer page stops agreeing with what the packages declare, or hands a reader one runner and no other. Three claims: the `@support` regions of the repertoire page match a fresh emit, so a range moves in `support-matrix.ts` and lands everywhere it is stated rather than in the one copy somebody remembered; no consumer page names a peer at a version the manifests do not declare, nor another node engine, which is how a hand-typed range goes stale with every other gate green; and no consumer page writes `bunx` without an alternative beside it. **The third is why this gate exists.** The shipped command is a Node program reading three `node:` modules, and `bunx` had been written into five consumer pages, so the branch documented a runtime dependency Arena does not have and nothing on the board could see it. The evidence level on every answer is asserted rather than assumed, because an answer allowed by a manifest and never exercised is worth having and is not worth the same as one a suite runs. | +| `check-support.ts` | a consumer page stops agreeing with what the packages declare, or hands a reader one runner and no other. Three claims: the `@support` regions of the repertoire page match a fresh emit, so a range moves in `support-matrix.ts` and lands everywhere it is stated; no consumer page names a peer at a version the manifests do not declare, nor another node engine; and no consumer page writes `bunx` without an alternative beside it, because the shipped command is a Node program reading `node:` modules, and a runner written alone reads as a runtime dependency Arena does not have. The evidence level on every answer is asserted rather than assumed, because an answer allowed by a manifest and never exercised is worth having and is not worth the same as one a suite runs. | | `check-prompts.ts` | a `.prompt.md` carries no `@api` region, or one that does not match a fresh emit from `contracts/api/components/.json`. A prompt is where a consumer stops reading, so the members have to be stated there; a hand-typed table would be a second copy of the contract that nothing holds, and the fix for a wrong cell is always the contract. Whether a component is contracted at all is `check:api`'s question, so an uncontracted one is counted rather than failed. | -| `check-rules.ts` | a surface that states the rules of the language stops matching a fresh emit, or a rule claims an enforcement the shipped audit does not carry. Four surfaces stated them and none was emitted from another, so the router could say no gate reads your application while `arena-to-prod --audit` already did, with every gate green. A rule names the audit tag that reports it, out of the `RULE_TAGS` the shipped module declares, or says why a source text cannot show it. | -| `check-register.ts` | a consumer document runs a sentence past 30 words, hangs more than one subordinate clause off it, or opens one on a pronoun that names nothing. The claims on that branch were true and one sentence carried four of them at once, which a reader holding the whole page resolves and a model holding a half written screen does not. `NOT_YET_REWRITTEN` is the map that empties: a pathspec awaiting the rewrite with its reason, and an entry whose every document already reads in this register fails as a stale allowance. | -| `check-api.ts` | a layer's implementation diverges from `contracts/api/components/.json`. **No map excuses a divergence**, deliberately: a contract forbids one, so it has nowhere for a second opinion to live. `DERIVED_DEFAULT` is not that and is the only map here: it names the members whose default is a token reached through a constant, which is a value a contract cannot state without copying the token and which the literal reader therefore cannot compare. It fails both ways, on an entry whose initialiser became a literal and on a non-literal initialiser nobody named, so the set of members compared against nothing is declared rather than silently skipped. Both layers run through one comparison: reading a default in one layer alone is what let `ArenaGrid.min` ship. | +| `check-rules.ts` | a surface that states the rules of the language stops matching a fresh emit, or a rule claims an enforcement the shipped audit does not carry. Every surface stating the rules is emitted from one declaration, so what a page says reads a consumer's application and what `arena-to-prod --audit` actually reads cannot disagree. A rule names the audit tag that reports it, out of the `RULE_TAGS` the shipped module declares, or says why a source text cannot show it. | +| `check-register.ts` | a consumer document runs a sentence past 30 words, hangs more than one subordinate clause off it, or opens one on a pronoun that names nothing. A reader holding the whole page resolves a sentence carrying four claims, and a model holding a half written screen does not. `NOT_YET_REWRITTEN` is the map that empties: a pathspec awaiting the rewrite with its reason, and an entry whose every document already reads in this register fails as a stale allowance. | +| `check-api.ts` | a layer's implementation diverges from `contracts/api/components/.json`. **No map excuses a divergence**, deliberately: a contract forbids one, so it has nowhere for a second opinion to live. `DERIVED_DEFAULT` is not that and is the only map here: it names the members whose default is a token reached through a constant, which is a value a contract cannot state without copying the token and which the literal reader therefore cannot compare. It fails both ways, on an entry whose initialiser became a literal and on a non-literal initialiser nobody named, so the set of members compared against nothing is declared rather than silently skipped. Both layers run through one comparison, since a default read in one layer alone is a divergence the other layer never reports. | | `check-playgrounds.ts` | a fixture under `frameworks/demos/` stops holding against the contract it seeds: a seed the contract does not declare, a value of the wrong type, an enum value outside its declared set, a required member or slot the fixture leaves empty, a bind naming something that is not an event or writing a payload into a member that cannot hold it, or a host that does not mark exactly one place for the component under test. Type-checking a seed is the whole point: a component drawn from a bad seed still draws, so the page renders and lies while every other gate stays green. It also holds the half of the citation rule only this gate can make: a cited `.demo.generated.html` is one a contract actually **emits**, and a bare page filename names something under `frameworks/`, which is the one shape a rename rewrites in every import and in no sentence. Whether a cited path merely exists is `check-citations.ts`'s, which reads every surface rather than a layer's. It opens none of them: every check here compares emitted SOURCE, so a page that compiles and then mounts nothing is invisible to it, which DOUBTS.md carries. | | `check-kitchen-sink.ts` | an arrangement under `frameworks/kitchen-sink/` stops pointing `check-pixel-parity.ts` at the whole library: a component the registry names that no arrangement places, an arrangement naming a component that does not exist, a component placed twice, a `frameworks/demos/` fixture that stops pinning a member its component would otherwise read off the clock or the host's own zone, or an emitted page that is not what a fresh run of the generator writes. Every one of those shrinks the surface the pixel comparison runs over while leaving it green, which is the failure that gate cannot see about itself: it reports zero differences over a page holding nothing exactly as it does over a page holding everything. | -| `check-citations.ts` | prose anywhere in the tree names a repository file that is not there, or cites `path:member()` naming a file that declares no such member. **Prose is three surfaces**: a line of a `.md`, a prose value in a `.json` read through `check-contracts-neutrality.ts:strands(rel, tree)`, which is where the prose keys are declared, and every comment in a file `check-docs.ts:allowsHeader(repoRelativePath)` admits, which is the one header a script or a suite may carry. The fourth candidate, a member doc under a component directory, is deliberately not read: `check:api` keeps it equal to the `description` this gate already reads at the contract, so reading the copy judges one fact twice. A value is addressed by its key path rather than by a line, for the reason a document cites a member rather than a line, and a `.json` that does not parse is reported, since a file the walk cannot open contributes no passage and reads like one with nothing to say. `BARE_EXEMPT` is the map the wider reach needed: a scale step is spelled the way a document extension is. Both defects it was written for had stood with every gate green: a document citing a spec the charter deletes once executed, and a build chain naming a step that had been renamed. A match is judged only when it names a **file**, because prose shortens a path freely and a shortened one is not a claim, and a leading slash is a URL from a site root rather than a repo path. The roots are read from the tree, so a new top-level directory is covered the day it lands, and a run finding none of them fails rather than reporting every absolute path missing, which is the vacuous pass in reverse. `EXEMPT` names what is absent on purpose, with a reason each, and an entry no document cites any more fails as a stale allowance. **A root git ignores leaves that list**, asked of git rather than guessed, because `docs/` exists on the machine a spec was written on and on no clone, so the gate would otherwise scan a different alternation depending on where it ran -- and a gate whose subject varies by environment is the defect this whole rule set is about. Closing that opens the other half, which it also holds: **a document citing a file under an ignored root fails**, since such a citation passed here for the wrong reason, the root not being in the alternation at all, while no clone could follow it. **The walk skips those roots too**, and that half was found by filing a real plan under `docs/` and watching the gate judge it: excluding a root from the alternation and still walking INTO it fixes which paths are recognised and leaves which documents are read varying by machine, which is the same defect with a different subject. `basenames()` skips them for the same reason, or a bare citation would resolve against a scratch file that exists on one machine. | -| `check-agents.ts` | a level on the contributor branch is reached by no chain of links from the router, or a `README.md` is back on this branch. The levels are what the walk finds rather than what a list declares: the list this gate used to carry had gone thirty-five short while reporting itself complete, which is the shape a hand-maintained subject always ends in. **A level nobody wrote is deliberately not a failure**, because the convention hands a reader the nearest level above it and that is a correct answer; a level nobody LINKS is, since proximity is automatic for an agent and unavailable to a reader, so the same tree answers the two of them differently. `emitted()` drops a copy a build left behind, and it is scoped so `scripts/build/` stays in the walk: widening it by one word took five real levels out of the subject with the gate still printing a pass. `SURVIVORS` names every `README.md` kept and why each is not a contributor document, and an entry whose file has gone fails as a stale allowance. | -| `check-exports.ts` | a symbol a barrel carries to the root of a package has no home on that package's npm page. The router already states the rule and nothing held it, so a helper written for one component reached a consumer beside the components while `PACKAGE.md` said a symbol it does not name carries no compatibility promise. That pair is worse than either half: the tested focus trap behind a dialog ships, reaches a consumer who has been told a lightbox is theirs to write, and is disclaimed by omission, so they write a second one. **The subject is what a consumer CALLS**: a type is reached through the value whose signature carries it, so naming the hook names its options and a page listing both says one thing twice. `INTERNAL` declares what reaches the root and is nobody's to lean on, with the reason, and fails both ways. Derived from the barrel rather than listed, because a list goes short the first time a module gains an export. | -| `check-classes.ts` | a class a consumer writes ships inside a stylesheet and has no home on that package's npm page. It is the other half of the rule above: a symbol reaches a consumer through the barrel, a class reaches them through the tarball, and the tarball announces nothing. The two density classes ship inside the spacing sheet and a consumer reading either npm page cannot learn that `.arena-compact` exists at all, which is the shape this gate is written against. **The subject is what a consumer puts on their own markup**, derived from `scripts/lib/arena/package-assembly.ts:CSS_CHAIN` and `scripts/lib/arena/package-assembly.ts:CONSUMER_SHEETS` rather than listed here, so a class added to a shipped sheet is caught on the day it ships. `NOT_WRITTEN` declares the classes that are Arena's own name for something rather than a consumer's to write, with the reason, and fails both ways. | +| `check-citations.ts` | prose anywhere in the tree names a repository file that is not there, or cites `path:member()` naming a file that declares no such member. **Prose is three surfaces**: a line of a `.md`, a prose value in a `.json` read through `check-contracts-neutrality.ts:strands(rel, tree)`, which is where the prose keys are declared, and every comment in a file `check-docs.ts:allowsHeader(repoRelativePath)` admits. A member doc under a component directory is not read, because `check:api` keeps it equal to the `description` this gate already reads at the contract. A value is addressed by its key path rather than by a line, and a `.json` that does not parse is reported, since a file the walk cannot open reads like one with nothing to say. A match is judged only when it names a **file**, because prose shortens a path freely, and a leading slash is a URL from a site root. The roots are read from the tree, so a new top-level directory is covered the day it lands, and a run finding none of them fails. `EXEMPT` names what is absent on purpose and `BARE_EXEMPT` a bare name that is not a document, each with a reason, and an entry nothing cites any more fails as stale. **A root git ignores leaves the alternation and the walk alike**, asked of git rather than guessed, so the gate reads the same subject on every machine, and a document citing a file under an ignored root fails, since no clone can follow it. | +| `check-agents.ts` | a level on the contributor branch is reached by no chain of links from the router, or a `README.md` is back on this branch. The levels are what the walk finds rather than what a list declares, because a hand-maintained subject goes short while reporting itself complete. **A level nobody wrote is deliberately not a failure**, because the convention hands a reader the nearest level above it; a level nobody LINKS is, since proximity is automatic for an agent and unavailable to a person. `emitted()` drops a copy a build left behind, and it is scoped so `scripts/build/` stays in the walk. `SURVIVORS` names every `README.md` kept and why each is not a contributor document, and an entry whose file has gone fails as a stale allowance. | +| `check-exports.ts` | a symbol a barrel carries to the root of a package has no home on that package's npm page. A symbol reaching a consumer undocumented is disclaimed by omission: a consumer told a lightbox is theirs to write writes a second focus trap beside the tested one the package already ships. **The subject is what a consumer CALLS**: a type is reached through the value whose signature carries it, so naming the hook names its options and a page listing both says one thing twice. `INTERNAL` declares what reaches the root and is nobody's to lean on, with the reason, and fails both ways. Derived from the barrel rather than listed, because a list goes short the first time a module gains an export. | +| `check-classes.ts` | a class a consumer writes ships inside a stylesheet and has no home on that package's npm page. It is the other half of the rule above: a symbol reaches a consumer through the barrel, a class reaches them through the tarball, and the tarball announces nothing. **The subject is what a consumer puts on their own markup**, derived from `scripts/lib/arena/package-assembly.ts:CSS_CHAIN` and `scripts/lib/arena/package-assembly.ts:CONSUMER_SHEETS` rather than listed here, so a class added to a shipped sheet is caught on the day it ships. `NOT_WRITTEN` declares the classes that are Arena's own name for something rather than a consumer's to write, with the reason, and fails both ways. | | `check-agents-spec.ts` | the contributor branch stops answering [the convention published for its filename](https://agents.md), which fixes a file at the root, that name exactly, plain Markdown with no required field, a level resolved by proximity, and a command an agent runs because a page listed it. The convention publishes no validator and no schema, so this gate is the whole of what holds Arena to it and `npx agents-lint` is a third party's second opinion rather than the standard's. **The commands are derived and never listed**: a script whose name carries no colon is an entry point and one with a colon narrows a phase, so the root page has to name each entry point and gains the obligation the day a new one lands. Whether a name a document spells is a script at all belongs to `check-vocabulary.ts:BUN_RUN` and is not asked twice. `DEPARTURES` carries what Arena does differently with the reason, and it fails both ways: a departure the tree makes and nobody declared, and a declared one the tree has stopped making. | | `check-duplication.ts` | two documents state more of the same sentences than their limit allows: three anywhere in the tree, or `FAMILY_LIMIT` between two components filed under one category. The unit is a normalized sentence of at least `MIN_WORDS` words, compared through an index over every document at once rather than pair by pair, and a **generated region is cut out before anything is compared**, because it is one source rendered into many files and leaving it in made every prompt overlap every other one through the same emitted line. Two shapes are legitimate: `sameSubject`, two documents with one file name, which is a document rendered per layer and a generator question rather than a duplication one; and `sameFamily`, two components stating the rule they both answer to, which is `FAMILY_DEBT`, a real duplication that is bounded rather than paid because a reader opens exactly one prompt and moving the rule up a level would charge every build for a family it is not using. | | `check-deadlines.ts` | a bare span sits in a wait position, or a suite's budget does not name every deadline its import closure declares. A literal in a case timeout, a `withTimeout` bound or a `sleep` is a number read off whichever machine wrote it, so it is right where it was written and silently wrong everywhere else: it expires early and reports its subject absent, sending a reader into the component that has no defect. A budget under one of the deadlines it can spend abandons the callback with its subject still running, and every file loaded afterwards then reports an error naming neither. The closure is read as **text** by `scripts/graph/script-closure.ts` and never imported, because importing a script to read it runs it. Both maps carry a reason per entry, and the paired suite asserts on them by name. | | `check-consumer.ts` | the `arena-to-prod` command a package ships stops doing what the npm page says it does, run against a real fixture project rather than reasoned about. It assembles each package, installs it into a temporary project, runs the CLI and reads what came out: the theme sheet's import chain, `"components": "auto"` resolving to the sheets a source actually needs, a component name the package does not ship being reported rather than silently dropped, and a source naming a symbol this package does not export resolving to nothing, since no alias exists to soften it. `DOCUMENTED_LIST` reads the sheet list back out of the shipped `README.md`, so the page and the command cannot drift apart. | | `check-architecture.ts` | a layer withdraws one of the architecture promises the consumer branch makes on its behalf. **Arena is adopted on a tree whose last nodes tell a project it may server-render, prerender or embed as a fragment, and which dependencies each answer costs**, and every one of those answers is a property of these sources rather than a policy anybody stated: a browser global read while a module evaluates throws during a server render at import time rather than at paint, `useLayoutEffect` warns there and is the classic tell, an import outside `ENVELOPES` is a framework decision the library made on a consumer's behalf and arrives as a dependency they did not choose, and `@angular/router` stops being optional the moment anything outside the metadata entry point imports it, which turns an install-time promise into a run-time break. Two escapes are accepted because the layers already use them and both are correct: a read inside a function, an effect or an after-render hook, and one behind a `typeof` guard. Reading goes through the same lexer `check-docs.ts` uses, twice over: the pass that reads an import specifier blanks comments and keeps strings, since a specifier IS a string, and every other pass blanks both, since a global named inside a string is prose. Suites, demos and build outputs are out of scope, because a suite spawns a browser on purpose. | -| `check-vocabulary.ts` | a document states a naming convention no file in the tree answers to, cites a sibling by a bare FILE name nothing carries, which `BARE_FILENAME` reads as a name carrying an extension so a bare symbol or component name is nobody's claim here, tells a reader to run an npm script `package.json` does not declare, names a `README` on the branch whose one filename is `AGENTS.md`, or hands over a `find` whose pattern matches nothing where it looks. Each of the five had a live defect the day it landed, and every one had stood green: the claim is prose and the subject is the tree, so nothing was comparing them. A convention is the expensive one, since a reader hunting for the file it names cannot tell a stale document from their own mistake. `NAMED_BUT_NOT_HERE` carries what is named and correctly absent, the consumer's own files and what the assembled package ships, and `README_MEANS` the documents that mean the page npm shows; a deliberate absence is read from `check-citations.ts`'s own map rather than declared twice. Reach is the whole tree for the same reason `check-citations.ts`'s is: a gate names the shape it reads, never the shape it sits beside. | -| `check-routes.ts` | a declared reading route costs more than its budget, names a stop that reaches no document, or has fallen so far under its budget that the number stopped applying pressure. **A route is what a reader pays PAST the page it opens with.** `ENTRIES` declares each branch's router once, with a budget of its own, because a router that seven routes carried as their first stop was charged seven times and argued seven times, and moving a paragraph out of one reported seven stale allowances: neither number was ever about the reader. `ROUTES` is the registry of what is paid after that, and a stop is a pathspec charged for the **largest** file it reaches, so a route is measured at the worst case a reader can meet rather than at an average nobody experiences. **A route whose reader answers a question and walks one way declares `branches` instead of `stops`, and is charged its worst branch**, since a node only some answers reach is not a page every reader opens; the report names the branch that spends the budget. A route with `stops` alone is one unnamed branch, so both shapes take one measurement path. It is the only gate that measures a document by what reading it costs somebody else, which is why every budget carries its reason as a string: a route grows by a paragraph that reads as an improvement at the place it is written. | -| `check-community.ts` | the surface a stranger meets before any code stops holding: a declared outward-facing file nobody wrote, the governance policy stated in a second document or in none, a contact link naming a path the default branch does not carry, or a `context7.json` that has drifted from the router. `OUTWARD` names each file with the reason it exists, because none of them is reachable from the router and nothing else would notice one going. The two failures it was written for are both silent: a repository that reads as maintained and answers 404 to the reporter who followed its own contact link, and an indexed document the consumer branch never meant to hand over. Context7 has no router, so what it indexes is what it hands back, and every contributor document reaching the index is a contributor answer to a consumer question; an exclusion covering nothing fails as a stale allowance, in both directions. **Which names it guards is derived and never listed**: `contributorBasenames()` takes every document the walk finds, drops what a build emitted, what the config already excludes by folder, what `check-docs.ts:isConsumerDocument(repoRelativePath)` calls the other branch's, what `OUTWARD` sends to a stranger, and the one README that belongs to neither. The list it replaced named four while the tree carried six, and the two it was short of were a chart family's own tour and a page written the same session, both of them indexed and neither of them for a consumer. Every string in `rules` is required to be a span of `SKILL.md`, compared with runs of whitespace flattened, so the copy cannot go quietly false and rewrapping a paragraph is not a failure.. **It also holds the file to the schema it declares**: that schema sets `additionalProperties` to false, so a key outside its set is not one field ignored but a document that fails to validate whole, and what gets indexed is whatever the service falls back to. The set is written here because the schema is published rather than vendored, and a key it gains is a key this list has to gain with it | +| `check-vocabulary.ts` | a document states a naming convention no file in the tree answers to, cites a sibling by a bare FILE name nothing carries, which `BARE_FILENAME` reads as a name carrying an extension so a bare symbol or component name is nobody's claim here, tells a reader to run an npm script `package.json` does not declare, names a `README` on the branch whose one filename is `AGENTS.md`, or hands over a `find` whose pattern matches nothing where it looks. The claim is prose and the subject is the tree, so nothing else compares them. A convention is the expensive one, since a reader hunting for the file it names cannot tell a stale document from their own mistake. `NAMED_BUT_NOT_HERE` carries what is named and correctly absent, the consumer's own files and what the assembled package ships, and `README_MEANS` the documents that mean the page npm shows; a deliberate absence is read from `check-citations.ts`'s own map rather than declared twice. Reach is the whole tree for the same reason `check-citations.ts`'s is: a gate names the shape it reads, never the shape it sits beside. | +| `check-routes.ts` | a declared reading route costs more than its budget, names a stop that reaches no document, has fallen so far under its budget that the number stopped applying pressure, or carries a history where its reason should be. **A route is what a reader pays PAST the page it opens with.** `ENTRIES` declares each branch's router once, with a budget of its own, so a router every route shares is argued for its reader and not once per route. `ROUTES` is the registry of what is paid after it, and a stop is a pathspec charged for the **largest** file it reaches, so a route is measured at the worst case a reader can meet. **A route whose reader answers a question and walks one way declares `branches` and is charged its worst branch.** Every budget carries its reason as a string, because a route grows by a paragraph that reads as an improvement where it is written, and `reasonProblems` holds that string to why the number is right today: no figure, no "raised", and no more than `REASON_MAX_CHARS`. | +| `check-community.ts` | the surface a stranger meets before any code stops holding: a declared outward-facing file nobody wrote, the governance policy stated in a second document or in none, a contact link naming a path the default branch does not carry, or a `context7.json` that has drifted from the router. `OUTWARD` names each file with the reason it exists, because none of them is reachable from the router and nothing else would notice one going. Context7 has no router, so what it indexes is what it hands back, and a contributor document in the index is a contributor answer to a consumer question; an exclusion covering nothing fails as a stale allowance, in both directions. **Which names it guards is derived and never listed**: `contributorBasenames()` takes every document the walk finds, drops what a build emitted, what the config excludes by folder, what `check-docs.ts:isConsumerDocument(repoRelativePath)` calls the other branch's, what `OUTWARD` sends to a stranger, and the one README that belongs to neither. Every string in `rules` is required to be a span of `SKILL.md`, compared with runs of whitespace flattened, so the copy cannot go quietly false and rewrapping a paragraph is not a failure. **It also holds the file to the schema it declares**: that schema sets `additionalProperties` to false, so a key outside its set invalidates the whole document, and the set is written here because the schema is published rather than vendored. | | `check-site.ts` | the published site stops holding to what it claims: a page pointing at an asset the output does not carry, a page the manifest declares and the build dropped, one published that nothing declares, a module one of its pages imports and the output does not carry, a sitemap missing an entry, or a CNAME for another domain. The first is load-bearing and `intro/AGENTS.md` says why nothing else catches it: a broken path fails silently in both directions, and an unstyled page that fits its box passes a look. The module half is that assertion one notation in: a `src` resolving says nothing about what the entry imports, and an import that 404s throws before the entry runs, so the page keeps every href and renders nothing. The shape a static host breaks and a dev server hides is a directory answered at request time with a listing. It SKIPS when `dist/site/` is absent, since a walk of nothing reports every link resolved; what makes a dropped playground visible is `site-pages.ts` asking the component map rather than the filesystem. | | `check-icons.ts` | a `ph-` class name in the tree is not a Phosphor glyph, or is a glyph the weight beside it does not carry. A contract takes an icon as a string, and a string nothing checks is a typo that renders an empty box, with no error and no failing gate. The truth is read out of the installed `@phosphor-icons/web` rather than typed here, because a list of 1500 names ages the first time the package does, and finding no stylesheet is an explicit failure rather than a pass over nothing. `SCANNED_ROOTS` is the scope and `EXEMPT` is empty on purpose: a name Phosphor does not have is a typo rather than a case. | | `check-behaviour.ts` | a component declares no `.behaviour.json`, names a pattern or requirement that does not exist, or the two layers disagree without saying why. A green run is a **coverage** claim, never an accessibility one. | @@ -42,9 +42,9 @@ The domain for a gate that reads two or more layers at once, or the repository r | `check-contracts-resolve.ts` | a design token in the carried set stops resolving inside that set alone. Not a second `check-dtcg.ts`: that one asks whether a value is well formed here, where the whole tree is present, and this asks whether a reference still points at something once the CSS chain and the generated emission are gone. A reference resolving in this tree only through a file the package does not carry is the defect that lands in somebody else's generator and that nobody here would ever see. A cycle is reported rather than walked, and a run finding no reference at all fails, since the rhythm steps and the chart paddings are authored as aliases on purpose. | | `check-structure.ts` | a component is declared in two categories, sits in a category `frameworks/Components.json` assigns elsewhere, is named by no declaration, or is present in no layer. It says nothing about whether the category is the **right** one. | | `check-script-tokens.ts` | a `Tokens.generated.*` module disagrees with the DTCG source or the CSS, a `script: true` flag is orphaned, or `ArenaCatSlot` stops being exactly `1..catSlots` in order. | -| `check-intro-generated.ts` | a page bundle under `intro/` does not match its source, or a page loads a module entry that is not a bundle. Those bundles are **tracked**, because the Claude Code plugin is served from the git tag and nothing there runs a build, which is the same reason `contracts/design-generated/` is tracked; a tracked build product is a promise, and this is what keeps it. The second half is the load-bearing one: a page whose `