@adia-ai/adia-ui-forge 0.8.50 → 0.8.52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +30 -0
- package/README.md +23 -1
- package/__init__.py +63 -0
- package/agents/a2ui-maintenance-agent.md +6 -4
- package/agents/a2ui-planner-agent.md +5 -5
- package/agents/demo-audit-agent.md +4 -4
- package/agents/framework-checker.md +5 -5
- package/agents/framework-planner-agent.md +7 -6
- package/agents/gen-ui-review-agent.md +4 -4
- package/agents/package-release-agent.md +4 -2
- package/agents/primitive-authoring-agent.md +1 -0
- package/package.json +10 -2
- package/plugin.yaml +4 -0
- package/prompts/demo-audit.md +10 -0
- package/prompts/gen-ui-review.md +9 -0
- package/prompts/package-release.md +12 -0
- package/prompts/site-deployment.md +9 -0
- package/skills/cross-harness-authoring-standards/SKILL.md +120 -0
- package/skills/cross-harness-authoring-standards/agents/openai.yaml +3 -0
- package/skills/cross-harness-compatibility-standards/SKILL.md +219 -0
- package/skills/cross-harness-compatibility-standards/agents/openai.yaml +3 -0
- package/skills/package-release/references/cut-procedure.md +7 -0
- package/skills/package-release/scripts/gate-roster.mjs +5 -0
- package/skills/package-release/scripts/release-pack.mjs +244 -42
- package/skills/primitive-authoring/references/INDEX.md +1 -1
- package/skills/primitive-authoring/references/api-contract.md +35 -0
- package/skills/primitive-authoring/references/css-patterns.md +79 -0
- package/skills/primitive-authoring/references/lifecycle-patterns.md +17 -0
- package/skills/primitive-authoring/references/module-promotion.md +2 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cross-harness-compatibility-standards
|
|
3
|
+
description: >-
|
|
4
|
+
The engineering/architecture standard for actually BUILDING a Claude
|
|
5
|
+
agent-family (or any multi-runtime capability) so it genuinely works
|
|
6
|
+
across Claude Code, Codex, Pi, and Hermes — not the consumer-facing
|
|
7
|
+
install docs once it already works (that's
|
|
8
|
+
cross-harness-authoring-standards). Use when deciding whether a
|
|
9
|
+
capability earns a new user-facing skill or should stay a thin
|
|
10
|
+
runtime-specific agent/seat/extension, before writing a Pi extension or
|
|
11
|
+
Hermes command and unsure whether the target API actually exists,
|
|
12
|
+
defining a structured handoff record two or more runtimes must exchange,
|
|
13
|
+
or naming a new plugin distribution/marketplace channel alongside an
|
|
14
|
+
existing one. NOT for writing the resulting per-harness install commands
|
|
15
|
+
(cross-harness-authoring-standards owns that, once the capability
|
|
16
|
+
already works).
|
|
17
|
+
disable-model-invocation: false
|
|
18
|
+
user-invocable: true
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
# cross-harness-compatibility-standards — build it portable, don't just document it
|
|
22
|
+
|
|
23
|
+
`cross-harness-authoring-standards` covers the downstream concern: writing the
|
|
24
|
+
consumer-facing install instructions once a capability already works across
|
|
25
|
+
harnesses. This skill covers the upstream concern — the engineering standard
|
|
26
|
+
for actually **building** that capability so it works on Claude Code, Codex,
|
|
27
|
+
Pi, and Hermes in the first place. If you're drafting a "how to install"
|
|
28
|
+
block, you want the other skill; if you're deciding whether something needs
|
|
29
|
+
a new skill, writing a Pi/Hermes adapter, or designing a structured record
|
|
30
|
+
two runtimes must exchange, you're in the right place.
|
|
31
|
+
|
|
32
|
+
This standard is not invented doctrine — it is extracted from
|
|
33
|
+
[ADR-0091](../../../../../docs/ops/adr/adr-0091-ui-architect-cross-harness-portability.md)
|
|
34
|
+
and its companion
|
|
35
|
+
[PRD](../../../../../docs/ops/prd/prd-ui-architect-cross-harness-portability.md)/
|
|
36
|
+
[SPEC](../../../../../docs/ops/spec/spec-ui-architect-cross-harness-portability.md)/
|
|
37
|
+
[LLD](../../../../../docs/ops/lld/lld-ui-architect-cross-harness-portability.md),
|
|
38
|
+
ratified and built across four merged PRs (#1997 docs, #1998 skill + contracts
|
|
39
|
+
+ Codex marketplace, #1999 agent pinning, #2000 Pi extension + Hermes
|
|
40
|
+
command) that made `ui-architect` portable. Every rule below cites the
|
|
41
|
+
decision or requirement it comes from — re-read the source before extending
|
|
42
|
+
this standard, don't paraphrase from this skill alone.
|
|
43
|
+
|
|
44
|
+
## Rule 1 — skill vs. thin adapter: earn the promotion `[gate]`
|
|
45
|
+
|
|
46
|
+
Substantive reusable behavior belongs in a portable skill body; a
|
|
47
|
+
runtime-specific agent/seat/extension stays **only if** it provides one of:
|
|
48
|
+
restricted tools, isolated context, parallelism, multi-skill preload, or a
|
|
49
|
+
distinct model/effort configuration (ADR-0091 D1). Delete an agent, or fold
|
|
50
|
+
it into its skill, if it provides none of those structural properties. Never
|
|
51
|
+
convert every internal seat into a user-facing skill merely because
|
|
52
|
+
portability is required — a capability is user-invocable only when a user
|
|
53
|
+
has a sensible reason to request it directly, not because promoting it was
|
|
54
|
+
convenient.
|
|
55
|
+
|
|
56
|
+
Apply the rule by checking each existing seat against it, on the record, not
|
|
57
|
+
by assertion:
|
|
58
|
+
|
|
59
|
+
- `ui-architect` was the one seat of four in `adia-ui-factory` missing its
|
|
60
|
+
skill twin — it earned `skills/ui-architect/SKILL.md` because the
|
|
61
|
+
Gear-selection/wave/generator-≠-reviewer/four-lap-cap/failure-routing
|
|
62
|
+
procedure is exactly the "substantive reusable behavior" class the rule
|
|
63
|
+
routes to a skill, and Codex/Pi/Hermes users are the concrete "sensible
|
|
64
|
+
reason to request it directly." The agent seat was **retained, not
|
|
65
|
+
replaced**: no Write/Edit (restricted tools), dispatches into isolated
|
|
66
|
+
per-screen contexts (isolated context + parallelism), pinned `fable`/
|
|
67
|
+
`high` never-`inherit` (distinct model/effort) — independently sufficient
|
|
68
|
+
to keep it.
|
|
69
|
+
- `app-planning-agent`, `screen-composition-agent`, `surface-qa-agent`
|
|
70
|
+
already each preloaded a same-named user-invocable skill before this
|
|
71
|
+
change touched anything — the rule was already satisfied; nothing about
|
|
72
|
+
their shape changed beyond the same Claude-adapter slimming (Rule 3)
|
|
73
|
+
applied to all four seats equally.
|
|
74
|
+
- `wave-coordination` stayed `user-invocable: false`, Claude-only,
|
|
75
|
+
unpromoted — team-pattern *mechanics* for one runtime's dispatch model,
|
|
76
|
+
not a capability a Codex/Pi/Hermes user has a sensible reason to request
|
|
77
|
+
directly. Failing the rule's own admission test on purpose is a legitimate
|
|
78
|
+
outcome, not an oversight to fix later.
|
|
79
|
+
|
|
80
|
+
## Rule 2 — verify before coding, per harness, every time `[gate]`
|
|
81
|
+
|
|
82
|
+
Never assume a target harness's API, extension mechanism, or delegation
|
|
83
|
+
surface from documentation alone. Check the actually-installed version
|
|
84
|
+
live — read its real source, run its real CLI — before writing one line of
|
|
85
|
+
adapter code against it. This is the single most load-bearing lesson from
|
|
86
|
+
how `ui-architect`'s Pi and Hermes adapters were actually built (PR #2000):
|
|
87
|
+
|
|
88
|
+
- **Pi** was verified present by reading the installed
|
|
89
|
+
`@earendil-works/pi-coding-agent` package directly: its own shipped
|
|
90
|
+
`examples/extensions/subagent/` proved a real subprocess-based
|
|
91
|
+
subagent-dispatch pattern (`registerCommand` + `child_process` spawn of a
|
|
92
|
+
nested `pi --mode json -p --no-session`), and `package.json`'s
|
|
93
|
+
`pi.extensions` field was confirmed real and documented — not inferred by
|
|
94
|
+
analogy to Claude's Agent tool. Once verified, the extension was
|
|
95
|
+
**live-tested end to end**: a real `pi` session ran `/ui-architect`
|
|
96
|
+
against a live model, dispatched all three isolated passes, wrote a real
|
|
97
|
+
file, took a real screenshot, and returned a clean `VerifyProof`.
|
|
98
|
+
- **Hermes** was verified sufficient by reading the installed Hermes
|
|
99
|
+
Agent's own source — `PluginContext.register_command` + `dispatch_tool`,
|
|
100
|
+
and `tools/delegate_tool.py`'s `delegate_task(goal, context, role, ...)` —
|
|
101
|
+
confirmed as real, current APIs before the generator surgery (Rule 3)
|
|
102
|
+
was written against them. The generated command was built behind its own
|
|
103
|
+
`--selftest` fixture (a real `py_compile` check plus a fake `delegate_task`
|
|
104
|
+
double driving the actual generated logic against the real linters)
|
|
105
|
+
*before* wiring it into the real plugin tree, then verified live via
|
|
106
|
+
`hermes plugins doctor --ci`. A full interactive dispatch was blocked by
|
|
107
|
+
the build sandbox refusing to mutate Hermes's global plugin-enable
|
|
108
|
+
state — that limitation was **noted honestly in the README rather than
|
|
109
|
+
worked around or silently dropped**.
|
|
110
|
+
|
|
111
|
+
If a required capability is genuinely absent, or materially different from
|
|
112
|
+
what the docs claimed (SPEC REQ-011/012's capability-boundary discipline):
|
|
113
|
+
stop that adapter at an explicit, clearly-labeled capability boundary and
|
|
114
|
+
record four things — the API checked, the observed mismatch, the smallest
|
|
115
|
+
viable fallback, and the remaining work. Never simulate success with an
|
|
116
|
+
unverified manifest field or a dead adapter file that looks like it works
|
|
117
|
+
but was never actually exercised. One of the two outcomes (working adapter
|
|
118
|
+
+ passing native smoke test, or a capability-boundary record with all four
|
|
119
|
+
fields) must exist — never neither, never both.
|
|
120
|
+
|
|
121
|
+
## Rule 3 — one authored copy, many thin consumers
|
|
122
|
+
|
|
123
|
+
The portable behavior lives in exactly **one** skill body. Every runtime's
|
|
124
|
+
own adapter mechanism *consumes* that one body — it never gets a second,
|
|
125
|
+
hand-copied prose duplicate:
|
|
126
|
+
|
|
127
|
+
- **Claude** preloads it (`skills:` frontmatter on the owning agent file).
|
|
128
|
+
- **Codex** auto-derives its manifest sibling
|
|
129
|
+
(`skills/<name>/agents/openai.yaml`) from the same skill frontmatter via
|
|
130
|
+
this repo's own generator (`scripts/build/codex-manifests.mjs`) — no
|
|
131
|
+
manual authoring, ever.
|
|
132
|
+
- **Pi**'s extension reads the skill file *at runtime* rather than copying
|
|
133
|
+
its prose into the extension source (`ui-architect`'s
|
|
134
|
+
`pi/ui-architect.extension.ts` loads agent descriptions from the
|
|
135
|
+
plugin-local skill directly).
|
|
136
|
+
- **Hermes**'s command derives from the same source
|
|
137
|
+
(`scripts/build/harness-manifests.mjs`'s generated `__init__.py`
|
|
138
|
+
registers the skill; the generated `/ui-architect` command runs the same
|
|
139
|
+
planning/composition/QA loop through `delegate_task`, gated by the same
|
|
140
|
+
contracts).
|
|
141
|
+
|
|
142
|
+
A change to the workflow means editing one file, not four. If you find
|
|
143
|
+
yourself pasting a skill's prose into an agent file, an extension, or a
|
|
144
|
+
generator template instead of citing or reading it, stop — that's the
|
|
145
|
+
duplication this rule exists to prevent.
|
|
146
|
+
|
|
147
|
+
## Rule 4 — one contract grammar, not a schema per runtime
|
|
148
|
+
|
|
149
|
+
When two or more runtimes need to exchange a structured handoff record,
|
|
150
|
+
define it once as a plain strict-prose-record shape validated by a small
|
|
151
|
+
stdlib CLI linter — never introduce JSON Schema or another
|
|
152
|
+
schema-validation library into every runtime's own toolchain to do the same
|
|
153
|
+
job (ADR-0091 D3). This repo's own `record-lint` family is the reusable
|
|
154
|
+
grammar: `<script> <file>|-|selftest`, exit 0 clean / 1 on findings, with
|
|
155
|
+
`UNMEASURED — <reason>` as a legal per-slot value (silent omission is not
|
|
156
|
+
legal). `OrientationRecord` (`scripts/record-lint`), `VerifyProof`
|
|
157
|
+
(`scripts/verify-proof-lint`), and `BuildResult`
|
|
158
|
+
(`scripts/build-result-lint`) — all three in `adia-ui-factory`'s
|
|
159
|
+
`scripts/` — are structurally identical siblings, not three different
|
|
160
|
+
formats doing the same job. Every runtime adapter that produces or consumes
|
|
161
|
+
one of these contracts calls the same linter script; a second,
|
|
162
|
+
runtime-specific parsing or validation implementation for the same contract
|
|
163
|
+
is a defect, not a stylistic choice.
|
|
164
|
+
|
|
165
|
+
Before minting a new contract shape, check whether an existing one already
|
|
166
|
+
covers it under a different name — reuse beats a fourth near-duplicate
|
|
167
|
+
grammar.
|
|
168
|
+
|
|
169
|
+
## Rule 5 — name distribution channels distinctly
|
|
170
|
+
|
|
171
|
+
When the same plugin ships **both** an in-repo/access-gated channel and a
|
|
172
|
+
public/npm-sourced channel for the same runtime, name them distinctly. A
|
|
173
|
+
collision — both channels sharing one marketplace name — makes
|
|
174
|
+
`<plugin>@<marketplace-name>` resolution ambiguous for a consumer trying to
|
|
175
|
+
install from a specific channel.
|
|
176
|
+
|
|
177
|
+
The confirmed, working precedent is Claude's own two-channel split: the
|
|
178
|
+
repo-root, access-gated `.claude-plugin/marketplace.json` is named
|
|
179
|
+
`adia-ui-kit`; the public, npm-sourced
|
|
180
|
+
`packages/plugins/adia-plugins/marketplace.json` is named `adia-plugins`
|
|
181
|
+
(ADR-0091 D2's own table) — two distinct names, two distinct reachability
|
|
182
|
+
tiers, no ambiguity for a consumer running `/plugin install
|
|
183
|
+
<plugin>@<marketplace-name>`.
|
|
184
|
+
|
|
185
|
+
ADR-0091 D2 designed the Codex side to mirror that same split (a
|
|
186
|
+
repo-root/local-source channel alongside the existing
|
|
187
|
+
`packages/plugins/adia-plugins/.agents/plugins/marketplace.json` npm
|
|
188
|
+
sibling) — but the LLD (§C7) flagged the new manifest's `name` field as an
|
|
189
|
+
explicit, unverified, build-time question: does Codex key off `name` for
|
|
190
|
+
`<plugin>@<marketplace-name>` resolution the way Claude does? **Verify this
|
|
191
|
+
against the actually-shipped manifests before citing the Codex side as a
|
|
192
|
+
settled precedent** — Rule 2's verify-before-coding discipline applies to
|
|
193
|
+
this rule's own worked example, not only to Pi/Hermes adapters. Diff the
|
|
194
|
+
`name` field of both Codex marketplace manifests against each other before
|
|
195
|
+
assuming they follow the Claude-side pattern.
|
|
196
|
+
|
|
197
|
+
## Failure branches
|
|
198
|
+
|
|
199
|
+
A capability promoted to a user-facing skill with none of Rule 1's five
|
|
200
|
+
qualifying properties on its remaining agent seat → fold the seat into the
|
|
201
|
+
skill or delete it; don't keep a Write/Edit-free, non-isolated, no-model-pin
|
|
202
|
+
shell around "for consistency." An adapter written against a harness's
|
|
203
|
+
documented API with no live verification → stop, verify against the
|
|
204
|
+
installed runtime first (Rule 2); a passing test against a mocked or
|
|
205
|
+
assumed API is not verification. A second hand-copied prose block for a
|
|
206
|
+
skill's workflow anywhere outside its one `SKILL.md` → delete the copy,
|
|
207
|
+
point the consumer at the source (Rule 3). A new structured record
|
|
208
|
+
introduced as JSON Schema, or as a shape that already has a linter under a
|
|
209
|
+
different name → stop, extend `record-lint`'s family instead or reuse the
|
|
210
|
+
existing contract (Rule 4). A new distribution manifest reusing an existing
|
|
211
|
+
sibling's marketplace name → rename before shipping (Rule 5).
|
|
212
|
+
|
|
213
|
+
Done when: every new or promoted seat cites the specific Rule 1 property
|
|
214
|
+
that keeps it (or is deleted/folded); every harness adapter names the API
|
|
215
|
+
it verified live and how (Rule 2); the skill body has exactly one authored
|
|
216
|
+
copy with every consumer citing or reading it, never duplicating it (Rule
|
|
217
|
+
3); every cross-runtime contract has exactly one shape and one stdlib
|
|
218
|
+
linter (Rule 4); and every distribution channel's marketplace name is
|
|
219
|
+
diffed against its siblings and confirmed distinct (Rule 5).
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
interface:
|
|
2
|
+
display_name: "Cross Harness Compatibility Standards"
|
|
3
|
+
short_description: "The engineering/architecture standard for actually BUILDING a Claude agent-family (or any multi-runtime capability) so it genuinely works across Claude Code, Codex, Pi, and Hermes — not the consumer-facing install docs once it already works (that's cross-harness-authoring-standards)."
|
|
@@ -89,6 +89,8 @@ When only source *hashes* move and chunk content does not, `check:embeddings-fre
|
|
|
89
89
|
|
|
90
90
|
### 3.1 The full roster — every gate runs; a subset = pre-flight failure
|
|
91
91
|
|
|
92
|
+
**Execution model (gh#2006): three phases, not one serial walk.** `step3PreFlight()` runs gate 4 solo first (see its own note below), then gates 16 → 27 → 28 strictly in order (the eval-health write-then-read dependency — gate 28 reads whichever `evals/mcp/runs/` directory sorts lexically LAST, so nothing else may write there between 27 and 28), concurrently with a bounded pool running every other gate at once (`PREFLIGHT_CONCURRENCY`, default 4 — override for a dedicated/idle host). Every gate still resolves the same command, still fails the whole pre-flight on a red result, and still reports its own number — only the WALL-CLOCK schedule changed, never the roster below or its numbering. `--dry` previews stay the original flat serial walk unchanged.
|
|
93
|
+
|
|
92
94
|
```bash
|
|
93
95
|
node scripts/build/components.mjs --verify # 1 yaml ↔ sidecar ↔ .d.ts
|
|
94
96
|
npm run verify:traits # 2 trait coverage
|
|
@@ -120,6 +122,8 @@ npm run eval:diff -- --engine free-form # 27 fresh free-form
|
|
|
120
122
|
node scripts/release/write-eval-health.mjs --version <cut> # 28 evals/health/<version>.json committed, AC-01/AC-02 run for real (gh#1135, WS-4 SPEC REQ-06)
|
|
121
123
|
node scripts/release/check-estate-split-latch.mjs # 29 no lockstep cut mid estate-split (ADR-0048 / gh#1192) — see the note below
|
|
122
124
|
npm run check:catalog-tiers # 30 tier-index.json vs committed catalog (gh#1494 — ADR-0069 moved its PR-blocking half to derived-resync; the pre-cut roster re-asserts Class-R freshness before a tag. Gate 13 can't catch this: the harvester hashes tier-index.json as a SOURCE)
|
|
125
|
+
npm run check:codex-manifests-fresh # 31 Codex plugin.json + openai.yaml vs .claude-plugin/plugin.json SoT (gh#1888)
|
|
126
|
+
npm run check:harness-manifests-fresh # 32 Hermes/Pi plugin.yaml + __init__.py + prompts vs .claude-plugin/plugin.json + commands SoT (gh#1954)
|
|
123
127
|
```
|
|
124
128
|
|
|
125
129
|
**Gate 29 was the ADR-0048 latch; since P5 it is a permanent invariant.** Between P1 and P5 the repo was correct in-repo but deliberately **not publishable** (old-name stubs marked `private: true` that the roster still mapped, plus dependency edges onto workspace packages no cut published), and a cut in that window would have shipped broken packages that npm cannot unpublish. **P5 cleared it by landing the real thing** — the six stubs became publishable shims and `PACKAGE_ROSTER` gained the three remaining new names, at which point all 9 offending edges resolved and the gate went green on its own. No gate logic was changed.
|
|
@@ -153,6 +157,8 @@ Any red → route via [`gates-catalog.md`](gates-catalog.md); fix at the source,
|
|
|
153
157
|
|
|
154
158
|
**4d.6. Regenerate the Codex plugin manifests** (gh#1888, gh#1899): `node scripts/build/codex-manifests.mjs`, then `npm run check:codex-manifests-fresh` to confirm. Both plugins' `.codex-plugin/plugin.json` embed `version` from `.claude-plugin/plugin.json`, which the 4b bump just moved — same "derived artifact carries the lockstep version" class as 4d.5's genui catalog. Gate 31 runs pre-bump in Step 3 and only proves freshness against the PREVIOUS version; nothing re-ran the generator post-bump before this line existed — the v0.8.48 release PR (#1897) shipped stale manifests as a result, caught by `check:codex-manifests-fresh` in CI and fixed by hand on the release branch. `packages/plugins/*/.codex-plugin/plugin.json` and `packages/plugins/*/skills/*/agents/openai.yaml` ride the release commit (Step 5 stages them).
|
|
155
159
|
|
|
160
|
+
**4d.7. Regenerate the Hermes/Pi plugin manifests** (gh#1954): `node scripts/build/harness-manifests.mjs`, then `npm run check:harness-manifests-fresh` to confirm. Same hazard as 4d.6, same fix — both plugins' `plugin.yaml` embed `version` from `.claude-plugin/plugin.json`. Gate 32 runs pre-bump in Step 3 and only proves freshness against the PREVIOUS version; run this post-bump every cut, not just when a skill/command changed. `packages/plugins/*/plugin.yaml`, `packages/plugins/*/__init__.py`, `packages/plugins/*/hermes-mcp.yaml` (factory only), and `packages/plugins/*/prompts/*.md` ride the release commit (Step 5 stages them).
|
|
161
|
+
|
|
156
162
|
**4e. Release docs + team notes** (gated, not optional): review the entry
|
|
157
163
|
files the release touches (root README/CHANGELOG, per-package READMEs —
|
|
158
164
|
content currency is YOUR judgment; the gate only proves presence), then
|
|
@@ -215,6 +221,7 @@ npm install --package-lock-only --no-audit --no-fund
|
|
|
215
221
|
npm run check:lockstep # 4d
|
|
216
222
|
node scripts/build/derive-genui-catalog.mjs # 4d.5 — catalogId carries the bumped version (gh#617)
|
|
217
223
|
node scripts/build/codex-manifests.mjs # 4d.6 — Codex manifest version carries the bumped version (gh#1899)
|
|
224
|
+
node scripts/build/harness-manifests.mjs # 4d.7 — Hermes/Pi manifest version carries the bumped version (gh#1954)
|
|
218
225
|
node scripts/release/check-release.mjs --pending-version X.Y.Z --fix # 4f — AFTER the stubs exist
|
|
219
226
|
node scripts/release/generate-release-notes.mjs --version X.Y.Z --write # 4e
|
|
220
227
|
node scripts/release/check-release-docs.mjs --version X.Y.Z # 4e gate
|
|
@@ -137,6 +137,11 @@ export const GATE_ROSTER = [
|
|
|
137
137
|
// hazard the same way a stale dist bundle is a real Claude-install
|
|
138
138
|
// hazard. Appended, not inserted — same numbering discipline as gate 30.
|
|
139
139
|
{ n: 31, cmd: 'npm run check:codex-manifests-fresh', what: 'Codex plugin.json + openai.yaml vs .claude-plugin/plugin.json SoT' },
|
|
140
|
+
// gh#1954: the Hermes (plugin.yaml + __init__.py + hermes-mcp.yaml) and Pi
|
|
141
|
+
// (prompts/*.md) external-distribution overlays are derived the same way
|
|
142
|
+
// gate 31's Codex tree is — a stale copy is the same real install hazard.
|
|
143
|
+
// Appended, not inserted — same numbering discipline as gate 30/31.
|
|
144
|
+
{ n: 32, cmd: 'npm run check:harness-manifests-fresh', what: 'Hermes/Pi plugin.yaml + __init__.py + prompts vs .claude-plugin/plugin.json + commands SoT' },
|
|
140
145
|
];
|
|
141
146
|
|
|
142
147
|
// -- CLI ------------------------------------------------------------------
|
|
@@ -60,7 +60,8 @@
|
|
|
60
60
|
// block BEFORE the prompt (H5) — never a bare y/N — whether it waits for input
|
|
61
61
|
// (manual flags) or auto-confirms (--go).
|
|
62
62
|
|
|
63
|
-
import { execSync } from 'node:child_process';
|
|
63
|
+
import { execSync, exec as execCb } from 'node:child_process';
|
|
64
|
+
import { promisify } from 'node:util';
|
|
64
65
|
import { createInterface } from 'node:readline/promises';
|
|
65
66
|
import { stdin, stdout } from 'node:process';
|
|
66
67
|
import fs from 'node:fs';
|
|
@@ -347,6 +348,15 @@ function shCapture(cmd, args) {
|
|
|
347
348
|
}
|
|
348
349
|
}
|
|
349
350
|
|
|
351
|
+
// Async, CAPTURING exec — the concurrent pre-flight phases (gh#2006) can't
|
|
352
|
+
// use sh()'s stdio:'inherit' (N processes' live output interleaved is
|
|
353
|
+
// unreadable), so every gate's stdout+stderr is captured and printed as one
|
|
354
|
+
// block the moment that gate finishes instead. maxBuffer generous (64 MB) —
|
|
355
|
+
// a truncated capture on a real failure (tsc, vitest) would read as a false
|
|
356
|
+
// pass/silent-gap the same class of defect shFailure's own doc-comment warns
|
|
357
|
+
// against.
|
|
358
|
+
const execAsync = promisify(execCb);
|
|
359
|
+
|
|
350
360
|
// ── Half-cut detection (gh#765) ──────────────────────────────────
|
|
351
361
|
// An aborted Step-4 run (the v0.8.29 cut aborted three times at Step 4e)
|
|
352
362
|
// leaves a half-cut tree: package versions bumped to the cut version +
|
|
@@ -428,53 +438,245 @@ function step1ReBaseline(args) {
|
|
|
428
438
|
console.log(' (empty — origin not ahead)');
|
|
429
439
|
}
|
|
430
440
|
|
|
431
|
-
// ── Step 3 — Pre-flight gates
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
441
|
+
// ── Step 3 — Pre-flight gates (gh#2006) ───────────────────────────
|
|
442
|
+
//
|
|
443
|
+
// gh#1987's audit found the real "feels slow" lever isn't roster size (all
|
|
444
|
+
// 32 gates are justified defense-in-depth — `main`'s branch protection only
|
|
445
|
+
// hard-requires the browser-scheme probe, so the `npm run check` name-
|
|
446
|
+
// duplicates in this roster are real re-checks, not dead weight) — it's
|
|
447
|
+
// that step3PreFlight ran every gate one at a time. Most of the 32 are
|
|
448
|
+
// independent, read-only checks with no shared mutable state; this splits
|
|
449
|
+
// them into three phases instead of one flat serial for-loop. Two real
|
|
450
|
+
// sequencing requirements survived the audit — everything else runs
|
|
451
|
+
// concurrently:
|
|
452
|
+
//
|
|
453
|
+
// Phase 1 (solo) — gate 4 (test:unit:serial) alone, no sibling gate
|
|
454
|
+
// process running at the same time. Not a proven requirement — the
|
|
455
|
+
// roster's own "serial on purpose" note is about vitest's OWN
|
|
456
|
+
// --no-file-parallelism, not about co-running with sibling PROCESSES —
|
|
457
|
+
// but a deliberately conservative one: gate 4 is the roster's single
|
|
458
|
+
// heaviest gate, and this repo's own host-audit F3 remedy
|
|
459
|
+
// (scripts/dev/worker-cap.mjs, gh#1792) exists precisely because
|
|
460
|
+
// unbounded concurrent lanes on a shared multi-session host turn a gate
|
|
461
|
+
// that's clean in isolation red under contention. Revisit with real
|
|
462
|
+
// measurement before loosening this.
|
|
463
|
+
// Phase 2 (concurrent) — two workstreams launched together and awaited
|
|
464
|
+
// together:
|
|
465
|
+
// (a) a bounded-concurrency pool of every remaining gate EXCEPT the
|
|
466
|
+
// eval-health chain below — each independently read-only
|
|
467
|
+
// (--verify/--fix-gated/dry-diff, checked directly against every
|
|
468
|
+
// script in the roster) or, for gate 17 (dogfood:status), writing
|
|
469
|
+
// only to a gate-private path (qa/findings/dogfood-tracker.md)
|
|
470
|
+
// no other gate reads or writes;
|
|
471
|
+
// (b) the eval-health chain — gates 16 → 27 → 28 — run strictly
|
|
472
|
+
// serially relative to EACH OTHER (still concurrent with (a),
|
|
473
|
+
// since neither touches (a)'s files). This is the roster's one
|
|
474
|
+
// CSS-before-JS-shaped file-write ordering dependency: gate 27
|
|
475
|
+
// (`eval:diff --engine free-form`) writes a fresh
|
|
476
|
+
// evals/mcp/runs/<ISO>/ directory; gate 28
|
|
477
|
+
// (write-eval-health.mjs) reads whichever directory under
|
|
478
|
+
// evals/mcp/runs/ SORTS LEXICALLY LAST — not specifically the
|
|
479
|
+
// latest free-form run — and expects free-form.json there. Gate
|
|
480
|
+
// 16 (`eval:diff --engine zettel`) writes its OWN fresh directory
|
|
481
|
+
// under that same root; if 16 and 27 ran concurrently and 16
|
|
482
|
+
// happened to finish after 27, gate 28 would silently pick 16's
|
|
483
|
+
// directory (no free-form.json) and commit
|
|
484
|
+
// evals/health/<version>.json with `engines.free-form: null` — a
|
|
485
|
+
// real, silent coverage loss the roster's fixed serial order
|
|
486
|
+
// prevents today only by construction. Keeping 16 before 27
|
|
487
|
+
// before 28, uninterrupted by any other evals/mcp/runs/ writer,
|
|
488
|
+
// preserves that guarantee.
|
|
489
|
+
//
|
|
490
|
+
// Fail-fast BETWEEN phases (a Phase 1 failure skips Phase 2 outright — no
|
|
491
|
+
// point spending Phase 2's wall-clock once the roster's known-flakiest gate
|
|
492
|
+
// is already red); fail-TOGETHER within Phase 2 (every Phase 2 gate runs to
|
|
493
|
+
// completion regardless of a sibling's failure, so one red pre-flight run
|
|
494
|
+
// reports every failing gate at once instead of the old one-at-a-time
|
|
495
|
+
// discovery) — the explicit fail-fast-vs-fail-together decision gh#2006's
|
|
496
|
+
// acceptance criteria calls for. Within the eval-health chain specifically,
|
|
497
|
+
// a failure DOES stop the chain (16 failing skips 27 and 28; 27 failing
|
|
498
|
+
// skips 28) — that trio has a real dependency relationship the rest of
|
|
499
|
+
// Phase 2 doesn't, so it keeps today's strict fail-fast semantics.
|
|
500
|
+
//
|
|
501
|
+
// versionArg/targetVersionArg/resume-mode branching is unchanged — every
|
|
502
|
+
// gate resolves its actual command through the same resolveGateCommand()
|
|
503
|
+
// logic step3PreFlight always applied, now factored out so all three
|
|
504
|
+
// phases share it verbatim.
|
|
505
|
+
|
|
506
|
+
const SOLO_GATE_NUMBER = 4; // test:unit:serial — see the phase-model note above.
|
|
507
|
+
const EVAL_HEALTH_CHAIN_NUMBERS = [16, 27, 28]; // eval:diff zettel → eval:diff free-form → write-eval-health.mjs
|
|
508
|
+
|
|
509
|
+
// Concurrency cap for Phase 2's pool — a fixed, deliberately conservative
|
|
510
|
+
// constant rather than the host's full core count. Most Phase-2 gates are
|
|
511
|
+
// lightweight verify/diff scripts, but a few (typecheck, the bundle
|
|
512
|
+
// verifiers, corpus verify) are real tsc/esbuild/Node processes, and
|
|
513
|
+
// worker-cap.mjs's own host-audit F3 finding is that unbounded concurrent
|
|
514
|
+
// lanes on a shared multi-session host reintroduce exactly the contention
|
|
515
|
+
// class this ticket must not reintroduce. Override via PREFLIGHT_CONCURRENCY
|
|
516
|
+
// for a dedicated/idle host.
|
|
517
|
+
const PREFLIGHT_CONCURRENCY = Number(process.env.PREFLIGHT_CONCURRENCY) || 4;
|
|
518
|
+
|
|
519
|
+
// Resolves one gate's actual command string — the same versionArg/
|
|
520
|
+
// targetVersionArg/resume branching step3PreFlight has always applied.
|
|
521
|
+
// Returns null for a versionArg gate deferred by an in-flight resume (Step
|
|
522
|
+
// 4g re-runs it once the notes it needs actually exist).
|
|
523
|
+
function resolveGateCommand(g, args) {
|
|
524
|
+
// versionArg gates take a MODE-DEPENDENT version (both directions bit the
|
|
525
|
+
// first 0.8.8 run): cut modes run pre-flight PRE-BUMP, where the README
|
|
526
|
+
// "Current version" claim still equals the PREVIOUS version (the bump
|
|
527
|
+
// moves it in Step 4; Step 4g then re-proves at the cut version) — while
|
|
528
|
+
// handoff runs POST-MERGE, where the claim already equals the CUT version
|
|
529
|
+
// and validating at previous fails by construction. A RESUMED half-cut run
|
|
530
|
+
// (gh#765) is post-bump too: the earlier aborted run already moved every
|
|
531
|
+
// reference to the cut version, so validating at previous false-fails
|
|
532
|
+
// gate 24 exactly like handoff would.
|
|
533
|
+
const hygieneVersion = (args.mode === 'handoff' || args.resume) ? args.version : args.previous;
|
|
534
|
+
// A RESUMED run's versionArg gates (cut-hygiene) demand artifacts Step 4
|
|
535
|
+
// has not recreated yet — docs/ops/releases/vX.Y.Z.md exists only after 4e,
|
|
536
|
+
// which runs AFTER this pre-flight. Step 4g re-runs these same gates at
|
|
537
|
+
// the cut version once the notes exist, so skipping them here loses no
|
|
538
|
+
// coverage — running them here false-fails by construction (the v0.8.32
|
|
539
|
+
// cut hit exactly this: 23/24 green, gate 24 dead on the missing notes).
|
|
540
|
+
if (g.versionArg && args.resume) return null;
|
|
541
|
+
if (g.versionArg) return `${g.cmd} --version ${hygieneVersion}`;
|
|
542
|
+
// targetVersionArg (gh#1135, REQ-06) is a distinct flag from versionArg:
|
|
543
|
+
// write-eval-health.mjs NAMES a new file after the cut's target version
|
|
544
|
+
// from the very start — there is no pre/post-bump claim to reconcile the
|
|
545
|
+
// way gate 24's README-currency check has, so it always takes
|
|
546
|
+
// `args.version` verbatim regardless of mode or resume.
|
|
547
|
+
if (g.targetVersionArg) return `${g.cmd} --version ${args.version}`;
|
|
548
|
+
return g.cmd;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Runs ONE gate to completion, capturing rather than streaming its output —
|
|
552
|
+
// see the file-header note above for why. Never throws: a failed gate is a
|
|
553
|
+
// normal, expected outcome the caller decides how to act on, exactly like
|
|
554
|
+
// shOk()'s own contract.
|
|
555
|
+
async function runGateCaptured(g, args) {
|
|
556
|
+
const cmd = resolveGateCommand(g, args);
|
|
557
|
+
if (cmd === null) {
|
|
558
|
+
return { n: g.n, cmd: g.cmd, what: g.what, ok: true, skipped: true,
|
|
559
|
+
note: '[resume] deferred to Step 4g (notes not regenerated yet)' };
|
|
560
|
+
}
|
|
561
|
+
const start = Date.now();
|
|
562
|
+
try {
|
|
563
|
+
const { stdout, stderr } = await execAsync(cmd, { cwd: REPO, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
|
|
564
|
+
return { n: g.n, cmd, what: g.what, ok: true, stdout, stderr, ms: Date.now() - start };
|
|
565
|
+
} catch (e) {
|
|
566
|
+
return {
|
|
567
|
+
n: g.n, cmd, what: g.what, ok: false, ms: Date.now() - start,
|
|
568
|
+
status: e.code ?? (e.signal ? `signal ${e.signal}` : '?'),
|
|
569
|
+
stdout: e.stdout, stderr: e.stderr,
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Prints one captured gate's result in the same shape a serial gate would
|
|
575
|
+
// have streamed live, so a scan-back-through-the-log habit built on the old
|
|
576
|
+
// stdio:'inherit' output still works.
|
|
577
|
+
function printGateResult(r) {
|
|
578
|
+
if (r.skipped) {
|
|
579
|
+
console.log(`\n ${r.n}/${GATE_ROSTER.length}. ${r.cmd} # ${r.what} — ${r.note}`);
|
|
435
580
|
return;
|
|
436
581
|
}
|
|
437
|
-
console.log(`\n
|
|
582
|
+
console.log(`\n ${r.n}/${GATE_ROSTER.length}. ${r.cmd} # ${r.what} (${(r.ms / 1000).toFixed(1)}s)`);
|
|
583
|
+
const out = [r.stdout, r.stderr].map((s) => (s || '').toString().trim()).filter(Boolean).join('\n');
|
|
584
|
+
if (out) console.log(out.split('\n').map((l) => ` ${l}`).join('\n'));
|
|
585
|
+
if (!r.ok) {
|
|
586
|
+
console.error(` ✗ FAILED (${r.n}/${GATE_ROSTER.length}): ${r.cmd} (exit ${r.status})`);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Runs a fixed array of gates strictly one after another, in array order —
|
|
591
|
+
// the eval-health chain (16 → 27 → 28), where the ordering IS the
|
|
592
|
+
// correctness requirement (see the phase-model note above), never a
|
|
593
|
+
// scheduling nicety. A failure stops the chain — the gates after it have a
|
|
594
|
+
// real dependency on what it would have produced, unlike the pool below.
|
|
595
|
+
async function runGateChain(gates, args) {
|
|
596
|
+
const results = [];
|
|
597
|
+
for (const g of gates) {
|
|
598
|
+
const r = await runGateCaptured(g, args);
|
|
599
|
+
printGateResult(r);
|
|
600
|
+
results.push(r);
|
|
601
|
+
if (!r.ok) break;
|
|
602
|
+
}
|
|
603
|
+
return results;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// Runs a list of gates concurrently, capped at `limit` in flight at once —
|
|
607
|
+
// a minimal pool, not a dependency graph: every gate here is fully
|
|
608
|
+
// independent, so order WITHIN the pool carries no meaning, only the cap
|
|
609
|
+
// does. Every gate runs to completion regardless of a sibling's failure
|
|
610
|
+
// (fail-together within the pool — see the file-header note above).
|
|
611
|
+
async function runGatePool(gates, args, limit) {
|
|
612
|
+
const queue = [...gates];
|
|
613
|
+
const results = [];
|
|
614
|
+
async function worker() {
|
|
615
|
+
while (queue.length) {
|
|
616
|
+
const g = queue.shift();
|
|
617
|
+
const r = await runGateCaptured(g, args);
|
|
618
|
+
printGateResult(r);
|
|
619
|
+
results.push(r);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
await Promise.all(Array.from({ length: Math.min(limit, gates.length) }, worker));
|
|
623
|
+
return results;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// DRY preview stays the original flat serial walk, byte-for-byte — no real
|
|
627
|
+
// process ever spawns in dry mode, so there is no concurrency win to chase,
|
|
628
|
+
// and selftest()'s exact-output assertions (below) only ever run `--dry`.
|
|
629
|
+
function step3PreFlightDry(args) {
|
|
438
630
|
for (const g of GATE_ROSTER) {
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
// README "Current version" claim still equals the PREVIOUS version (the
|
|
442
|
-
// bump moves it in Step 4; Step 4g then re-proves at the cut version) —
|
|
443
|
-
// while handoff runs POST-MERGE, where the claim already equals the CUT
|
|
444
|
-
// version and validating at previous fails by construction. A RESUMED
|
|
445
|
-
// half-cut run (gh#765) is post-bump too: the earlier aborted run already
|
|
446
|
-
// moved every reference to the cut version, so validating at previous
|
|
447
|
-
// false-fails gate 24 exactly like handoff would.
|
|
448
|
-
const hygieneVersion = (args.mode === 'handoff' || args.resume) ? args.version : args.previous;
|
|
449
|
-
// A RESUMED run's versionArg gates (cut-hygiene) demand artifacts Step 4
|
|
450
|
-
// has not recreated yet — docs/ops/releases/vX.Y.Z.md exists only after 4e,
|
|
451
|
-
// which runs AFTER this pre-flight. Step 4g re-runs these same gates at
|
|
452
|
-
// the cut version once the notes exist, so skipping them here loses no
|
|
453
|
-
// coverage — running them here false-fails by construction (the v0.8.32
|
|
454
|
-
// cut hit exactly this: 23/24 green, gate 24 dead on the missing notes).
|
|
455
|
-
if (g.versionArg && args.resume) {
|
|
631
|
+
const cmd = resolveGateCommand(g, args);
|
|
632
|
+
if (cmd === null) {
|
|
456
633
|
console.log(`\n ${g.n}/${GATE_ROSTER.length}. ${g.cmd} # ${g.what} — [resume] deferred to Step 4g (notes not regenerated yet)`);
|
|
457
634
|
continue;
|
|
458
635
|
}
|
|
459
|
-
// targetVersionArg (gh#1135, REQ-06) is a distinct flag from versionArg:
|
|
460
|
-
// write-eval-health.mjs NAMES a new file after the cut's target version
|
|
461
|
-
// from the very start — there is no pre/post-bump claim to reconcile
|
|
462
|
-
// the way gate 24's README-currency check has, so it always takes
|
|
463
|
-
// `args.version` verbatim regardless of mode or resume.
|
|
464
|
-
const cmd = g.versionArg
|
|
465
|
-
? `${g.cmd} --version ${hygieneVersion}`
|
|
466
|
-
: g.targetVersionArg
|
|
467
|
-
? `${g.cmd} --version ${args.version}`
|
|
468
|
-
: g.cmd;
|
|
469
636
|
console.log(`\n ${g.n}/${GATE_ROSTER.length}. ${cmd} # ${g.what}`);
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
637
|
+
sh(cmd, args, { stdio: 'inherit' }); // dry: logs `[dry] cmd`, never executes
|
|
638
|
+
}
|
|
639
|
+
console.log(`\n ✓ all ${GATE_ROSTER.length}/${GATE_ROSTER.length} gates passed`);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
async function step3PreFlight(args) {
|
|
643
|
+
if (args.skipGates) {
|
|
644
|
+
console.log('\n=== Step 3 — Pre-flight (SKIPPED via --skip-gates; danger!) ===');
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
console.log(`\n=== Step 3 — Pre-flight gates (${GATE_ROSTER.length}-gate roster — gate-roster.mjs) ===`);
|
|
648
|
+
if (args.dry) {
|
|
649
|
+
step3PreFlightDry(args);
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const soloGate = GATE_ROSTER.find((g) => g.n === SOLO_GATE_NUMBER);
|
|
654
|
+
const chainGates = EVAL_HEALTH_CHAIN_NUMBERS.map((n) => GATE_ROSTER.find((g) => g.n === n));
|
|
655
|
+
const chainNumbers = new Set(EVAL_HEALTH_CHAIN_NUMBERS);
|
|
656
|
+
const poolGates = GATE_ROSTER.filter((g) => g.n !== SOLO_GATE_NUMBER && !chainNumbers.has(g.n));
|
|
657
|
+
|
|
658
|
+
console.log(`\n-- Phase 1/2: gate ${SOLO_GATE_NUMBER} solo (test:unit:serial) --`);
|
|
659
|
+
const soloResult = await runGateCaptured(soloGate, args);
|
|
660
|
+
printGateResult(soloResult);
|
|
661
|
+
if (!soloResult.ok) {
|
|
662
|
+
console.error('\n Pre-flight aborted at Phase 1 — fix the gate failure before proceeding.');
|
|
663
|
+
console.error(' See references/gates-catalog.md for failure-mode → recovery.');
|
|
664
|
+
process.exit(1);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
console.log(`\n-- Phase 2/2: ${poolGates.length} gates (concurrency ${PREFLIGHT_CONCURRENCY}) + eval-health chain (gates ${EVAL_HEALTH_CHAIN_NUMBERS.join(' → ')}) --`);
|
|
668
|
+
const [poolResults, chainResults] = await Promise.all([
|
|
669
|
+
runGatePool(poolGates, args, PREFLIGHT_CONCURRENCY),
|
|
670
|
+
runGateChain(chainGates, args),
|
|
671
|
+
]);
|
|
672
|
+
|
|
673
|
+
const allResults = [soloResult, ...poolResults, ...chainResults].sort((a, b) => a.n - b.n);
|
|
674
|
+
const failed = allResults.filter((r) => !r.ok);
|
|
675
|
+
if (failed.length > 0) {
|
|
676
|
+
console.error(`\n ✗ ${failed.length}/${GATE_ROSTER.length} gate(s) FAILED: ${failed.map((r) => r.n).join(', ')}`);
|
|
677
|
+
console.error(' Pre-flight aborted. Fix the gate failure(s) before proceeding.');
|
|
678
|
+
console.error(' See references/gates-catalog.md for failure-mode → recovery.');
|
|
679
|
+
process.exit(1);
|
|
478
680
|
}
|
|
479
681
|
console.log(`\n ✓ all ${GATE_ROSTER.length}/${GATE_ROSTER.length} gates passed`);
|
|
480
682
|
}
|
|
@@ -1129,7 +1331,7 @@ async function main() {
|
|
|
1129
1331
|
process.exit(2);
|
|
1130
1332
|
}
|
|
1131
1333
|
step1ReBaseline(args);
|
|
1132
|
-
step3PreFlight(args);
|
|
1334
|
+
await step3PreFlight(args);
|
|
1133
1335
|
step4PromoteAndBump(args);
|
|
1134
1336
|
step5Commit(args);
|
|
1135
1337
|
// Invariant 3 (operator ruling 2026-07-12): release commits land via PR,
|
|
@@ -16,7 +16,7 @@ file, including the depth references those entries cross-link.
|
|
|
16
16
|
- [api-contract.md](api-contract.md) — how do I declare `static properties` (type, default, reflect, attribute), avoid the `attr:` silent-typo trap, and expose a selection-item's `selected` state (declared reflected prop, never a `data-*` stamp — ADR-0056)?
|
|
17
17
|
- [yaml-contract.md](yaml-contract.md) — what fields does the `<name>.yaml` SoT take, and how does it feed the generated `.a2ui.json` sidecar?
|
|
18
18
|
- [token-contract.md](token-contract.md) — is this component's CSS honoring the two-block `@scope` token contract, and why does it misrender under a theme?
|
|
19
|
-
- [css-patterns.md](css-patterns.md) — how do I structure a component stylesheet: two-block `@scope`, variants vs modes, token layering?
|
|
19
|
+
- [css-patterns.md](css-patterns.md) — how do I structure a component stylesheet: two-block `@scope`, variants vs modes, token layering, deriving an inset item's border-radius from its rounded container (concentric-corner radius)?
|
|
20
20
|
- [lifecycle-patterns.md](lifecycle-patterns.md) — how do I keep `connected()`/`disconnected()` symmetric for listeners, timers, observers, and popovers?
|
|
21
21
|
- [svg-authoring.md](svg-authoring.md) — viewBox/coordinate scaling, stroke-width, text-anchor/dominant-baseline, card-bleed clipping, currentColor vs `var()` inside SVG markup, hit-testing — for `chart-ui`/`qr-code-ui`/`icon-ui`/any new `<svg>`-emitting primitive.
|
|
22
22
|
- [form-control-sizing.md](form-control-sizing.md) — does a form-control host fill its container or hug its content, and where does the legibility floor live (ADR-0077)?
|