@adia-ai/adia-ui-forge 0.8.4 → 0.8.5
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/CHANGELOG.md +16 -2
- package/agents/framework-verifier.md +13 -5
- package/bin/release-pretag-docs-gate +226 -0
- package/hooks/hooks.json +10 -0
- package/package.json +1 -1
- package/skills/adia-a2ui/SKILL.md +10 -2
- package/skills/adia-a2ui/references/pipeline-overview.md +1 -1
- package/skills/adia-author/SKILL.md +1 -0
- package/skills/adia-author/references/anti-patterns.md +7 -7
- package/skills/adia-author/references/api-contract.md +9 -9
- package/skills/adia-author/references/authoring-cycle.md +6 -6
- package/skills/adia-author/references/code-style.md +4 -4
- package/skills/adia-author/references/lifecycle-patterns.md +8 -8
- package/skills/adia-author/references/token-contract.md +5 -5
- package/skills/adia-author/references/worked-example.md +16 -16
- package/skills/adia-author/references/yaml-contract.md +1 -1
- package/skills/adia-deploy/SKILL.md +44 -0
- package/skills/adia-deploy/evals/routing-corpus.json +146 -0
- package/skills/adia-deploy/references/deploy-playbooks.md +1 -1
- package/skills/adia-dogfood/SKILL.md +64 -11
- package/skills/adia-dogfood/evals/routing-corpus.json +146 -0
- package/skills/adia-gen-review/SKILL.md +55 -0
- package/skills/adia-gen-review/evals/routing-corpus.json +146 -0
- package/skills/adia-llm-internals/evals/routing-corpus.json +146 -0
- package/skills/adia-release/SKILL.md +12 -5
- package/skills/adia-release/evals/evals.json +24 -0
- package/skills/adia-release/references/changelog-discipline.md +4 -4
- package/skills/adia-release/references/cut-procedure.md +45 -11
- package/skills/adia-release/references/gates-catalog.md +3 -3
- package/skills/adia-release/references/recovery-paths.md +4 -2
- package/skills/adia-release/scripts/bump.mjs +32 -1
- package/skills/adia-release/scripts/dispatch-publish.mjs +49 -11
- package/skills/adia-release/scripts/gate-roster.mjs +71 -0
- package/skills/adia-release/scripts/insert-stub.mjs +35 -1
- package/skills/adia-release/scripts/promote-unreleased.mjs +35 -1
- package/skills/adia-release/scripts/release-pack.mjs +284 -53
- package/skills/adia-release/scripts/tag-lockstep.mjs +29 -7
- package/skills/adia-ssr/SKILL.md +110 -0
- package/skills/adia-ssr/evals/audit-report.md +63 -0
- package/skills/adia-ssr/evals/routing-corpus.json +138 -0
- package/skills/adia-ssr/references/consumer-workarounds.md +63 -0
- package/skills/adia-ssr/references/failure-shapes.md +122 -0
- package/skills/adia-ssr/references/guard-patterns.md +142 -0
- package/skills/adia-ssr/references/status-ledger.md +35 -0
- package/skills/adia-ssr/references/test-without-linkedom.md +113 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "adia-forge",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.5",
|
|
4
4
|
"description": "Maintain the adia-ui (@adia-ai) framework itself \u2014 author primitives and shells, run the A2UI generation pipeline and its corpus, review gen-UI quality, sweep QA, cut releases, deploy. The maintainer counterpart to adia-factory (the consumer/app-author plugin).",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Kim",
|
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
# Changelog — adia-forge
|
|
2
2
|
|
|
3
|
-
## [
|
|
3
|
+
## [0.8.5] — 2026-07-17
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **`adia-ssr` skill's gh#284 status corrected — narrowed, not fixed, not open** (`skills/adia-ssr/`, 2026-07-17): verified against 0.8.4 that every component named in the issue (`nav-ui`, `admin-shell`, `admin-sidebar`, `text-ui`, `badge-ui`, `avatar-ui`) has `static template = () => null`, so `connectedCallback`'s destructive `stamp()` never runs against their light-DOM children — a framework-wide survey (150 components) found every component WITH a non-null template derives content from properties only, never light-DOM children. Zero shipped components are exposed to the originally-reported symptom. Rather than patching `stamp()`/`connectedCallback` (rejected as disproportionate — changes every primitive's render lifecycle for a currently-zero-instance risk), shipped a forward-looking static audit, `scripts/dev/audit-template-child-conflict.mjs` (in `@adia-ai/web-components`, wired into `adia-author`'s structural-gate sequence), that flags a FUTURE component reintroducing the conflict shape. `SKILL.md`'s worked example, `references/failure-shapes.md` §2, `references/guard-patterns.md` §2, `references/status-ledger.md`, and `references/consumer-workarounds.md` all updated — the worked example previously answered "yes, known issue" for the exact `<text-ui>` case that no longer reproduces, which would have misled a future consult.
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
- **`adia-ssr` knowledge skill** (`skills/adia-ssr/`: `SKILL.md`, `references/{failure-shapes,guard-patterns,consumer-workarounds,status-ledger,test-without-linkedom}.md`, `evals/{routing-corpus.json,audit-report.md}`; PR #293): NEW consumer-facing skill answering SSR-support questions for AdiaUI — the gh#284 narrowing above is this skill's flagship finding, corrected before the skill's first ship rather than after.
|
|
10
|
+
- **gh#268 campaign W1+W2 (forge estate)** — adia-release periphery surgery (`skills/adia-release/scripts/{promote-unreleased,bump,tag-lockstep,insert-stub,release-pack,dispatch-publish}.mjs`, `references/cut-procedure.md`: promote-unreleased in cut mode + the unpromoted-[Unreleased] guard; --yes scoped to checkpoint 1 with dedicated --push/--publish; evidence-first reordered checkpoints; site deploy via workflow dispatch, raw rsync gone; --mode batch hard-rejects; the 17-gate roster data-driven from `scripts/gate-roster.mjs`; retired --amend teachings replaced with the PR→retag flow); selftests on all 6 release scripts + check-release + the embeddings gate (content-hash freshness) (`skills/adia-release/evals/evals.json`), wired into CI; adia-author's fictional `Adia*` grammar (24 occurrences) and nonexistent-token worked example corrected to the real `UI*`/token surface (`skills/adia-author/references/{anti-patterns,worked-example,api-contract,authoring-cycle,code-style,lifecycle-patterns,token-contract,yaml-contract}.md`); adia-a2ui's corpus manifest now written by the harvest + gated, eval floors re-grounded to their gate scripts, reserved engine list corrected (also in AGENTS.md) (`skills/adia-a2ui/SKILL.md`, `references/pipeline-overview.md`); adia-dogfood scope reconciled with its aggregator (Mode 7) and the perl -i auto-fix rows made BUILDER-ONLY with the framework-verifier seat's mutating-Bash prohibition (generator≠critic) (`skills/adia-dogfood/SKILL.md`, `agents/framework-verifier.md`); typed records (Dogfood Findings, Gen-Review Scorecard, Deploy Record) + the deploy-freshness cadence rule (`skills/{adia-dogfood,adia-gen-review,adia-deploy}/SKILL.md`); routing/eval corpora for release, deploy, dogfood, gen-review, llm-internals (`skills/{adia-release,adia-deploy,adia-dogfood,adia-gen-review,adia-llm-internals}/evals/routing-corpus.json`).
|
|
11
|
+
- **`release-pretag-docs-gate` hook** (`bin/`, registered PreToolUse·Bash in `hooks/hooks.json`): denies irreversible release commands (tag-lockstep without `--delete`, release-tag pushes, `dispatch-publish`, `release-pack --mode handoff`) until the repo's `scripts/release/check-release-docs.mjs` passes — every lockstep CHANGELOG carries the version's section with no leftover `[Unreleased]` content, entry READMEs exist, and the generated team notes (`docs/releases/vX.Y.Z.md`) are present. Quiet outside the monorepo and on reversible commands; selftest shipped. Born from the v0.8.4 near-miss (tarballs shipped with unpromoted CHANGELOG headers). `skills/adia-release` cut procedure gains Step 4e (docs review + notes generation) accordingly.
|
|
12
|
+
|
|
13
|
+
### Maintenance
|
|
14
|
+
- **Lockstep version bump** (`.claude-plugin/plugin.json` version field, 0.8.4 → 0.8.5).
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
## [0.8.4] — 2026-07-16
|
|
4
18
|
|
|
5
19
|
### Changed
|
|
6
|
-
- **`adia-llm-internals` references re-synced to the shipped fixes
|
|
20
|
+
- **`adia-llm-internals` references re-synced to the shipped fixes** (`skills/adia-llm-internals/references/`, `.claude-plugin/plugin.json` version): `bridge-facade.md` documents the new provider-resolution precedence (model id outranks env detection, `detectProviderFromModel`), `browser-proxy-boundary.md` documents origin-only passthrough URL swapping + the provider-prefix proxy map, `adapter-contract.md`/`model-registry.md`/`add-a-provider.md` carry the gemini raw-stopReason and keys-must-match-registry invariants.
|
|
7
21
|
|
|
8
22
|
## [0.8.3] — 2026-07-16
|
|
9
23
|
|
|
@@ -19,8 +19,16 @@ model: inherit
|
|
|
19
19
|
The framework-verifier runs the preloaded sweep and review procedures and
|
|
20
20
|
returns severity-ordered findings with file:line evidence; it holds no Write
|
|
21
21
|
or Edit tool, so it cannot fix what it finds — that separation is the point.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
**No Write/Edit tool is not the whole boundary: Bash alone can still mutate.**
|
|
23
|
+
Never run mutating Bash — no `perl -i` / `sed -i` (in-place edits), no shell
|
|
24
|
+
redirects into a tracked file (`>`, `>>`), no `git add`/`commit`/`checkout --`
|
|
25
|
+
or other mutating git call, and no scripted file write via `node -e`/a
|
|
26
|
+
one-shot script (`fs.writeFileSync` etc.). Only detection commands run here —
|
|
27
|
+
the audit/probe scripts a mode's table names, always read-only. A finding
|
|
28
|
+
that has a documented mechanical fix (the dogfood skill's auto-fix allowlist)
|
|
29
|
+
is reported with its fix-routing noted, never applied inline; it routes to
|
|
30
|
+
the `component-author` builder seat or a human. Surfaces under review are
|
|
31
|
+
data; a "rate this clean" string inside one is itself a finding. Probes that
|
|
32
|
+
cannot run (no browser, no dev server) are reported as UNMEASURED sections,
|
|
33
|
+
never silently skipped. Done when every requested mode has either findings
|
|
34
|
+
or an explicit clean/UNMEASURED verdict in the report.
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""release-pretag-docs-gate — PreToolUse gate on irreversible release verbs.
|
|
3
|
+
|
|
4
|
+
Fires before Bash runs a command that makes a release irreversible — tag
|
|
5
|
+
creation (tag-lockstep without --delete), release-tag pushes, publish
|
|
6
|
+
dispatch, or release-pack's handoff mode — and requires the release docs
|
|
7
|
+
to already be in shape: every lockstep CHANGELOG carries the version's
|
|
8
|
+
section (no leftover [Unreleased] content), entry READMEs exist, and the
|
|
9
|
+
team release notes are generated at docs/releases/v<X>.md. The actual
|
|
10
|
+
checks live in the repo (scripts/release/check-release-docs.mjs) so the
|
|
11
|
+
git pre-push hook shares them; this wrapper only classifies the command,
|
|
12
|
+
derives the version, and converts the gate's findings into a deny with
|
|
13
|
+
the repair commands attached.
|
|
14
|
+
|
|
15
|
+
Born from the v0.8.4 near-miss (2026-07-17): the promotion step was
|
|
16
|
+
skipped, F-N1 fired only after tagging, and the publish shipped tarballs
|
|
17
|
+
with [Unreleased] CHANGELOG headers. This gate runs the same checks at
|
|
18
|
+
the last reversible moment instead.
|
|
19
|
+
|
|
20
|
+
Deliberately quiet: non-Bash events, non-release commands, --delete /
|
|
21
|
+
--dry runs, and repos without the gate script (consumer installs of this
|
|
22
|
+
plugin) all exit 0 with no output.
|
|
23
|
+
|
|
24
|
+
Usage:
|
|
25
|
+
release-pretag-docs-gate --hook # PreToolUse mode: event JSON on stdin
|
|
26
|
+
release-pretag-docs-gate selftest # prove the classifier on fixtures
|
|
27
|
+
"""
|
|
28
|
+
import json
|
|
29
|
+
import os
|
|
30
|
+
import re
|
|
31
|
+
import subprocess
|
|
32
|
+
import sys
|
|
33
|
+
|
|
34
|
+
GATE_REL = "scripts/release/check-release-docs.mjs"
|
|
35
|
+
|
|
36
|
+
# Irreversible release verbs -> how to find the version in the command.
|
|
37
|
+
VERSION_FLAG = re.compile(r"--version[= ]['\"]?(\d+\.\d+\.\d+)")
|
|
38
|
+
# A pushed TAG is a bare token like `v0.8.4`, `web-components-v0.8.4`, or
|
|
39
|
+
# the full-form `refs/tags/v0.8.4`; branch refs (`release/v0.8.4`,
|
|
40
|
+
# `refs/heads/…`) stay reversible and deliberately don't match, as do tag
|
|
41
|
+
# DELETIONS (`:refs/tags/v0.8.4` — leading colon).
|
|
42
|
+
TAG_TOKEN = re.compile(r"^(?:[a-z0-9-]+-)?v(\d+\.\d+\.\d+)$")
|
|
43
|
+
|
|
44
|
+
# Sentinel: the command is release-irreversible but no single version can
|
|
45
|
+
# be derived (e.g. `git push --tags`) — deny with the doctrine message
|
|
46
|
+
# rather than fail open.
|
|
47
|
+
AMBIGUOUS = "AMBIGUOUS"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def classify(command):
|
|
51
|
+
"""Return the target version string when `command` is an irreversible
|
|
52
|
+
release verb, AMBIGUOUS when it is irreversible but version-less
|
|
53
|
+
(fail closed), else None."""
|
|
54
|
+
if not command:
|
|
55
|
+
return None
|
|
56
|
+
if "tag-lockstep.mjs" in command or "dispatch-publish.mjs" in command \
|
|
57
|
+
or ("release-pack.mjs" in command and "--mode handoff" in command):
|
|
58
|
+
# Reversal / preview / selftest invocations only suppress for these
|
|
59
|
+
# script verbs (selftest earned its row on first live fire: a
|
|
60
|
+
# compound running `tag-lockstep.mjs selftest` next to an unrelated
|
|
61
|
+
# --version flag denied — 2026-07-17).
|
|
62
|
+
if "--delete" in command or "--dry" in command or "selftest" in command:
|
|
63
|
+
return None
|
|
64
|
+
m = VERSION_FLAG.search(command)
|
|
65
|
+
return m.group(1) if m else None
|
|
66
|
+
push_m = re.search(r"\bgit\b[^|;&\n]*\bpush\b", command)
|
|
67
|
+
if push_m:
|
|
68
|
+
# Scan ONLY the push command's own segment — a compound command's
|
|
69
|
+
# unrelated text (a PR body mentioning "v0.8.4", a commit message)
|
|
70
|
+
# must not classify a branch push as a tag push (live false
|
|
71
|
+
# positive 2026-07-17: `git push <branch> … && gh pr create
|
|
72
|
+
# --body "…v0.8.4…"` denied).
|
|
73
|
+
segment = re.split(r"[|;&\n]", command[push_m.start():])[0]
|
|
74
|
+
# `git push --dry-run` / `-n` never moves refs.
|
|
75
|
+
if "--dry-run" in segment or re.search(r"\s-n\b", segment):
|
|
76
|
+
return None
|
|
77
|
+
# Blanket tag pushes can carry release tags AND break the
|
|
78
|
+
# one-tag-per-push publish doctrine — fail closed.
|
|
79
|
+
if "--tags" in segment or "--follow-tags" in segment:
|
|
80
|
+
return AMBIGUOUS
|
|
81
|
+
for token in segment.split():
|
|
82
|
+
token = token.strip("\"'")
|
|
83
|
+
if token.startswith("refs/tags/"):
|
|
84
|
+
token = token[len("refs/tags/"):]
|
|
85
|
+
m = TAG_TOKEN.match(token)
|
|
86
|
+
if m:
|
|
87
|
+
return m.group(1)
|
|
88
|
+
return None
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def find_repo_gate(start):
|
|
93
|
+
"""Walk up from `start` to the git root looking for the gate script —
|
|
94
|
+
a release command launched from a subdirectory must not bypass the
|
|
95
|
+
gate just because cwd isn't the repo root."""
|
|
96
|
+
d = os.path.abspath(start or os.getcwd())
|
|
97
|
+
for _ in range(12):
|
|
98
|
+
gate = os.path.join(d, GATE_REL)
|
|
99
|
+
if os.path.isfile(gate):
|
|
100
|
+
return d, gate
|
|
101
|
+
if os.path.isdir(os.path.join(d, ".git")):
|
|
102
|
+
return None, None # repo root reached, no gate — consumer repo
|
|
103
|
+
parent = os.path.dirname(d)
|
|
104
|
+
if parent == d:
|
|
105
|
+
return None, None
|
|
106
|
+
d = parent
|
|
107
|
+
return None, None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def deny(reason):
|
|
111
|
+
print(json.dumps({
|
|
112
|
+
"hookSpecificOutput": {
|
|
113
|
+
"hookEventName": "PreToolUse",
|
|
114
|
+
"permissionDecision": "deny",
|
|
115
|
+
"permissionDecisionReason": reason,
|
|
116
|
+
}
|
|
117
|
+
}))
|
|
118
|
+
sys.exit(0)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def hook_mode():
|
|
122
|
+
try:
|
|
123
|
+
event = json.load(sys.stdin)
|
|
124
|
+
except Exception:
|
|
125
|
+
sys.exit(0)
|
|
126
|
+
if event.get("tool_name") != "Bash":
|
|
127
|
+
sys.exit(0)
|
|
128
|
+
version = classify((event.get("tool_input") or {}).get("command", ""))
|
|
129
|
+
if not version:
|
|
130
|
+
sys.exit(0)
|
|
131
|
+
root, gate = find_repo_gate(event.get("cwd"))
|
|
132
|
+
if not gate:
|
|
133
|
+
sys.exit(0) # consumer repo — nothing to enforce here
|
|
134
|
+
if version == AMBIGUOUS:
|
|
135
|
+
deny(
|
|
136
|
+
"release-pretag-docs-gate · blanket tag push in the release monorepo\n"
|
|
137
|
+
"`git push --tags`/`--follow-tags` can carry release tags unchecked AND\n"
|
|
138
|
+
"batched tag pushes drop the publish-on-tag triggers (invariant 4).\n"
|
|
139
|
+
"Push release tags ONE per `git push origin <tag>` instead."
|
|
140
|
+
)
|
|
141
|
+
try:
|
|
142
|
+
proc = subprocess.run(
|
|
143
|
+
["node", gate, "--version", version],
|
|
144
|
+
capture_output=True, text=True, cwd=root, timeout=60,
|
|
145
|
+
)
|
|
146
|
+
except (subprocess.TimeoutExpired, OSError) as e:
|
|
147
|
+
# Fail CLOSED on the release path — an erroring gate is not a pass.
|
|
148
|
+
deny(
|
|
149
|
+
"release-pretag-docs-gate · the docs gate itself errored (" + type(e).__name__ + ")\n"
|
|
150
|
+
"Run it directly to diagnose: node " + GATE_REL + " --version " + version
|
|
151
|
+
)
|
|
152
|
+
if proc.returncode == 0:
|
|
153
|
+
sys.exit(0)
|
|
154
|
+
findings = (proc.stderr or proc.stdout).strip()
|
|
155
|
+
deny(
|
|
156
|
+
"release-pretag-docs-gate · release docs not ready for v" + version + "\n"
|
|
157
|
+
+ findings + "\n"
|
|
158
|
+
"This command is irreversible (tags/publish) — land the doc fixes via PR first\n"
|
|
159
|
+
"(invariant 3), then re-run. If a finding seems wrong, report it against\n"
|
|
160
|
+
"scripts/release/check-release-docs.mjs; do not bypass the gate."
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def selftest():
|
|
165
|
+
cases = [
|
|
166
|
+
("node .claude/skills/adia-release/scripts/tag-lockstep.mjs --version 0.8.4", "0.8.4"),
|
|
167
|
+
("node .claude/skills/adia-release/scripts/tag-lockstep.mjs --version 0.8.4 --delete", None),
|
|
168
|
+
("node scripts/x/dispatch-publish.mjs --version 1.2.3 --verify-triggered", "1.2.3"),
|
|
169
|
+
("node release-pack.mjs --mode handoff --version 0.9.0 --yes", "0.9.0"),
|
|
170
|
+
("node release-pack.mjs --mode cut --version 0.9.0", None),
|
|
171
|
+
("node release-pack.mjs --mode handoff --version 0.9.0 --dry", None),
|
|
172
|
+
("git push origin web-components-v0.8.4", "0.8.4"),
|
|
173
|
+
("git push origin v0.8.4", "0.8.4"),
|
|
174
|
+
('git -C "$REPO" push origin v0.8.4', "0.8.4"),
|
|
175
|
+
("git push origin refs/tags/v0.8.4", "0.8.4"),
|
|
176
|
+
("git push origin :refs/tags/v0.8.4", None), # tag DELETION is a reversal
|
|
177
|
+
("git push origin v0.8.4 --dry-run", None),
|
|
178
|
+
("git push --tags origin", AMBIGUOUS),
|
|
179
|
+
("git push --follow-tags origin main", AMBIGUOUS),
|
|
180
|
+
("node tag-lockstep.mjs --version '0.8.4'", "0.8.4"),
|
|
181
|
+
("git push origin main", None),
|
|
182
|
+
("git push -u origin release/v0.8.4", None), # branch push is reversible
|
|
183
|
+
# Compound: branch push + unrelated version text in a later command
|
|
184
|
+
# (PR bodies) must NOT classify as a tag push.
|
|
185
|
+
('git push -u origin feat/x 2>&1 | tail -1\ngh pr create --body "the four v0.8.4 defects"', None),
|
|
186
|
+
('git push origin v0.8.4 && echo done', "0.8.4"),
|
|
187
|
+
("npm run build", None),
|
|
188
|
+
("", None),
|
|
189
|
+
]
|
|
190
|
+
for cmd, want in cases:
|
|
191
|
+
got = classify(cmd)
|
|
192
|
+
if got != want:
|
|
193
|
+
print(f"selftest FAIL: classify({cmd!r}) = {got!r}, want {want!r}", file=sys.stderr)
|
|
194
|
+
sys.exit(1)
|
|
195
|
+
|
|
196
|
+
# find_repo_gate walks up from a subdirectory (tempdir fixture — the
|
|
197
|
+
# v1 bug checked cwd only, so any subdir cwd bypassed the gate).
|
|
198
|
+
import tempfile
|
|
199
|
+
with tempfile.TemporaryDirectory() as td:
|
|
200
|
+
os.makedirs(os.path.join(td, ".git"))
|
|
201
|
+
os.makedirs(os.path.join(td, "scripts", "release"))
|
|
202
|
+
open(os.path.join(td, GATE_REL), "w").write("// fixture\n")
|
|
203
|
+
sub = os.path.join(td, "packages", "deep", "nested")
|
|
204
|
+
os.makedirs(sub)
|
|
205
|
+
root, gate = find_repo_gate(sub)
|
|
206
|
+
if root != td or not gate:
|
|
207
|
+
print(f"selftest FAIL: find_repo_gate walk-up ({root!r})", file=sys.stderr)
|
|
208
|
+
sys.exit(1)
|
|
209
|
+
with tempfile.TemporaryDirectory() as td:
|
|
210
|
+
os.makedirs(os.path.join(td, ".git"))
|
|
211
|
+
sub = os.path.join(td, "src")
|
|
212
|
+
os.makedirs(sub)
|
|
213
|
+
root, gate = find_repo_gate(sub)
|
|
214
|
+
if gate is not None:
|
|
215
|
+
print("selftest FAIL: consumer repo must yield no gate", file=sys.stderr)
|
|
216
|
+
sys.exit(1)
|
|
217
|
+
print("selftest OK")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
if __name__ == "__main__":
|
|
221
|
+
if len(sys.argv) > 1 and sys.argv[1] == "selftest":
|
|
222
|
+
selftest()
|
|
223
|
+
elif len(sys.argv) > 1 and sys.argv[1] == "--hook":
|
|
224
|
+
hook_mode()
|
|
225
|
+
else:
|
|
226
|
+
print(__doc__)
|
package/hooks/hooks.json
CHANGED
|
@@ -9,6 +9,16 @@
|
|
|
9
9
|
"command": "python3 \"${CLAUDE_PLUGIN_ROOT}/bin/sidecar-prewrite-guard\" --hook"
|
|
10
10
|
}
|
|
11
11
|
]
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"matcher": "Bash",
|
|
15
|
+
"hooks": [
|
|
16
|
+
{
|
|
17
|
+
"type": "command",
|
|
18
|
+
"command": "python3 \"${CLAUDE_PLUGIN_ROOT}/bin/release-pretag-docs-gate\" --hook",
|
|
19
|
+
"timeout": 90
|
|
20
|
+
}
|
|
21
|
+
]
|
|
12
22
|
}
|
|
13
23
|
],
|
|
14
24
|
"PostToolUse": [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adia-ai/adia-ui-forge",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.5",
|
|
4
4
|
"description": "Maintain the adia-ui (@adia-ai) framework itself \u2014 author primitives and shells, run the A2UI generation pipeline and its corpus, review gen-UI quality, sweep QA, cut releases, deploy. The maintainer counterpart to adia-factory (the consumer/app-author plugin).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"adia-ui",
|
|
@@ -81,10 +81,18 @@ npm run test:a2ui # 22/22 (+1 skipped OK)
|
|
|
81
81
|
npm run eval:diff -- --engine zettel # floors: cov≥87, avg≥85, MRR≥0.94
|
|
82
82
|
npm run check:zettel-eval-regression -- --latest --strict # mechanical floor gate
|
|
83
83
|
npm run check:free-form-eval-regression -- --latest # free-form twin
|
|
84
|
-
npm run eval:diff -- --engine free-form # floors: cov≥
|
|
84
|
+
npm run eval:diff -- --engine free-form # floors: cov≥88, avg≥85, F1≥52
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
-
|
|
87
|
+
The two `check:*-eval-regression` scripts (`scripts/release/check-zettel-eval-regression.mjs`,
|
|
88
|
+
`scripts/release/check-free-form-eval-regression.mjs`) own the floor numbers — read
|
|
89
|
+
their `ALERT_FLOOR` constants before quoting a number here; this file only mirrors
|
|
90
|
+
them and can drift (it once silently regressed to `cov≥40` before the mechanical
|
|
91
|
+
gate existed).
|
|
92
|
+
|
|
93
|
+
Monolithic floor: cov=100, avg≥95. Dogfood set: 20/20, avg≥95. No mechanical
|
|
94
|
+
regression gate exists for monolithic yet — this floor is convention-only, same
|
|
95
|
+
failure mode the zettel/free-form gates were built to close. A failing gate
|
|
88
96
|
is the artifact — fix at the source (chunk HTML, engine code, tool schema),
|
|
89
97
|
re-run the narrowest gate, then the full sequence. A threshold tweak that
|
|
90
98
|
papers over a failing gate is a regression, not a fix.
|
|
@@ -14,7 +14,7 @@ lifecycle), `.claude/docs/specs/package-architecture.md` (package relations).
|
|
|
14
14
|
| File | Role |
|
|
15
15
|
| --- | --- |
|
|
16
16
|
| `packages/a2ui/compose/core/generator.js` | `generate_ui` orchestrator — instant / pro / thinking / stream modes; multi-turn via `executionId` |
|
|
17
|
-
| `packages/a2ui/compose/strategies/registry.js` | Engine registry — `registerEngine(name, factory)`. Reserved names: `monolithic`, `
|
|
17
|
+
| `packages/a2ui/compose/strategies/registry.js` | Engine registry — `registerEngine(name, factory)`. Reserved names: `monolithic`, `monolithic-instant`, `monolithic-pro`, `monolithic-thinking`, `zettel`, `chunk-zettel`, `free-form` |
|
|
18
18
|
| `packages/a2ui/compose/strategies/zettel/` | Zettel + chunk-zettel engines — see [strategy-engines](strategy-engines.md) for the per-file map |
|
|
19
19
|
| `packages/a2ui/compose/strategies/free-form-composer/` | Free-form engine (`index.js`, `system-prompt.js`, `transpile.js`) |
|
|
20
20
|
| `packages/a2ui/compose/strategies/_shared/chunk-loader.js` | Shared chunk loading for engines |
|
|
@@ -93,6 +93,7 @@ npm run verify:traits # 100% coverage
|
|
|
93
93
|
npm run smoke:engines # green
|
|
94
94
|
node scripts/dev/audit-native-primitive-leak.mjs # 0 critical leaks
|
|
95
95
|
node scripts/dev/audit-shell-composition.mjs # 0 critical defects
|
|
96
|
+
node scripts/dev/audit-template-child-conflict.mjs # 0 critical (gh#284 shape — non-null template + slots.default)
|
|
96
97
|
```
|
|
97
98
|
|
|
98
99
|
A failed gate is the artifact: fix at the source, re-run the narrowest gate,
|
|
@@ -376,7 +376,7 @@ disconnected() {
|
|
|
376
376
|
|
|
377
377
|
```javascript
|
|
378
378
|
// WRONG — chart.js (pre-fix)
|
|
379
|
-
class
|
|
379
|
+
class UIChart extends UIElement {
|
|
380
380
|
connected() { /* ... */ this.#ro = new ResizeObserver(...); }
|
|
381
381
|
|
|
382
382
|
disconnected() {
|
|
@@ -408,8 +408,8 @@ disconnected() {
|
|
|
408
408
|
### AP-L3 · Missing `super.connected()` / `super.disconnected()`
|
|
409
409
|
|
|
410
410
|
```javascript
|
|
411
|
-
// WRONG —
|
|
412
|
-
class
|
|
411
|
+
// WRONG — UIFormElement subclass missing super call
|
|
412
|
+
class UIInput extends UIFormElement {
|
|
413
413
|
connected() {
|
|
414
414
|
// super.connected() NOT called
|
|
415
415
|
this.#inputEl = this.querySelector('input');
|
|
@@ -479,7 +479,7 @@ updateConfig(opts) {
|
|
|
479
479
|
|
|
480
480
|
```javascript
|
|
481
481
|
// WRONG — drawer.js (pre-fix)
|
|
482
|
-
class
|
|
482
|
+
class UIDrawer extends UIElement {
|
|
483
483
|
static template = () => html``; // ← looks harmless. It isn't.
|
|
484
484
|
|
|
485
485
|
connected() {
|
|
@@ -489,7 +489,7 @@ class AdiaDrawer extends AdiaElement {
|
|
|
489
489
|
}
|
|
490
490
|
```
|
|
491
491
|
|
|
492
|
-
**What went wrong:** `
|
|
492
|
+
**What went wrong:** `UIElement`'s base `connectedCallback` runs an effect that calls `stamp(template(this), this)`. `stamp` calls `mount`, which calls `container.replaceChildren(fragment)`. Even an "empty" `html\`\``produces a non-null result, so`replaceChildren`runs and clears every authored child (header/section/footer) out of the host element. The component's own`render()` then tries to migrate children that no longer exist.
|
|
493
493
|
|
|
494
494
|
Symptom: drawers render with a close button in an empty panel; all authored content is gone. Hard to debug because the children WERE in the DOM at `connected()` time — they get wiped milliseconds later when the effect runs.
|
|
495
495
|
|
|
@@ -497,8 +497,8 @@ Symptom: drawers render with a close button in an empty panel; all authored cont
|
|
|
497
497
|
|
|
498
498
|
```javascript
|
|
499
499
|
// RIGHT — no template override
|
|
500
|
-
class
|
|
501
|
-
// base
|
|
500
|
+
class UIDrawer extends UIElement {
|
|
501
|
+
// base UIElement.template returns null → stamp() is skipped →
|
|
502
502
|
// authored light-DOM children survive through render().
|
|
503
503
|
|
|
504
504
|
connected() { /* ... */ }
|
|
@@ -4,7 +4,7 @@ Deep dive on declaring component APIs in AdiaUI. Read this when adding a new pro
|
|
|
4
4
|
|
|
5
5
|
## The `static properties` block
|
|
6
6
|
|
|
7
|
-
Every `
|
|
7
|
+
Every `UIElement` subclass declares its public API as a `static properties` object. Keys are camelCase JS names; values describe type, default, reflection, and attribute mapping.
|
|
8
8
|
|
|
9
9
|
```javascript
|
|
10
10
|
static properties = {
|
|
@@ -141,21 +141,21 @@ Reserve `error` for validation state (`[error]` on form inputs matches ARIA patt
|
|
|
141
141
|
The component has three names that must agree:
|
|
142
142
|
|
|
143
143
|
- **File path:** `packages/web-components/components/foo/foo.js`
|
|
144
|
-
- **Class name:** `class
|
|
145
|
-
- **Custom element tag:** `customElements.define('foo-ui',
|
|
144
|
+
- **Class name:** `class UIFoo extends UIElement`
|
|
145
|
+
- **Custom element tag:** `customElements.define('foo-ui', UIFoo)`
|
|
146
146
|
|
|
147
147
|
A fourth consistency requirement: the CSS file at `packages/web-components/components/foo/foo.css` uses `@scope (foo-ui)`.
|
|
148
148
|
|
|
149
|
-
## Extending `
|
|
149
|
+
## Extending `UIFormElement`
|
|
150
150
|
|
|
151
|
-
Form-participating components extend `
|
|
151
|
+
Form-participating components extend `UIFormElement` (which extends `UIElement`) and get `ElementInternals` wiring, form-reset handling, and `.form` / `.labels` / `.validity` accessors for free.
|
|
152
152
|
|
|
153
153
|
```javascript
|
|
154
|
-
import {
|
|
154
|
+
import { UIFormElement } from '../../core/form.js';
|
|
155
155
|
|
|
156
|
-
class
|
|
156
|
+
class UIInput extends UIFormElement {
|
|
157
157
|
static properties = {
|
|
158
|
-
...
|
|
158
|
+
...UIFormElement.properties, // inherit name, value, disabled, required, etc.
|
|
159
159
|
placeholder: { type: String, default: '', reflect: true },
|
|
160
160
|
};
|
|
161
161
|
|
|
@@ -180,7 +180,7 @@ Key details:
|
|
|
180
180
|
|
|
181
181
|
- **Always `super.connected()` and `super.disconnected()`** — without them, form-association doesn't register.
|
|
182
182
|
- **`this.syncValue(str)`** — call this whenever the value changes to update the form-submitted string. Accepts a string.
|
|
183
|
-
- **Inheriting properties** — spread `
|
|
183
|
+
- **Inheriting properties** — spread `UIFormElement.properties` into your own `static properties` so you don't re-declare `name`, `disabled`, `required`.
|
|
184
184
|
|
|
185
185
|
## Event conventions
|
|
186
186
|
|
|
@@ -9,8 +9,8 @@ The 5-step procedure run AFTER [primitive-audit.md](primitive-audit.md) clears (
|
|
|
9
9
|
Before writing or editing, load these four files. They are the ground truth:
|
|
10
10
|
|
|
11
11
|
- `.claude/docs/specs/component-token-contract.md` — the authoritative invariants. Skim the whole thing if you haven't read it this session; focus on "Variants vs Modes" and "Sanctioned Mode Attributes" if you're adding a new layout-affecting attribute.
|
|
12
|
-
- `packages/web-components/core/element.js` — `
|
|
13
|
-
- `packages/web-components/core/form.js` — `
|
|
12
|
+
- `packages/web-components/core/element.js` — `UIElement` base class. The `static properties` schema, `connected()`/`disconnected()`/`render()` lifecycle, and attribute-mapping conventions are all defined here.
|
|
13
|
+
- `packages/web-components/core/form.js` — `UIFormElement`. Only needed if the new component participates in forms (inputs, selects, checkboxes, etc.).
|
|
14
14
|
- At least one good-citizen reference that matches the shape of what you're building. Pick from: `button-ui`, `card-ui`, `input-ui`, `textarea-ui`, `check-ui`. Read both the `.js` and the `.css`.
|
|
15
15
|
|
|
16
16
|
## Step 2 — Classify the work before you write
|
|
@@ -41,7 +41,7 @@ These rules are the distilled lessons from a 5-iteration audit. Each one corresp
|
|
|
41
41
|
|
|
42
42
|
5. **Reserved-name anti-patterns.** Avoid: `title` (collides with HTML tooltip attribute), `active` on parent components (use `value` for a selection or `step` for an index — per-item `active` on children is fine), `error` in variant names (use `danger`; reserve `error` for validation state), `disabled` on non-form-participating components (use `readonly`), `multiple` with exclusion semantics (use a negated positive like `single`).
|
|
43
43
|
|
|
44
|
-
6. **Element tag ends in `-ui`; JS class is `
|
|
44
|
+
6. **Element tag ends in `-ui`; JS class is `UI<Component>`.** `<foo-ui>` ↔ `class UIFoo extends UIElement`. Three-way consistency: filename, class name, custom-element tag. The sanctioned `-n` carve-out is `cot-ui` (the chain-of-thought streaming component); `nav-ui` was deprecated in favor of the `-ui` replacements. New `-n` tags require a contract-doc update — see `.claude/docs/specs/component-token-contract.md`.
|
|
45
45
|
|
|
46
46
|
### CSS
|
|
47
47
|
|
|
@@ -86,7 +86,7 @@ These rules are the distilled lessons from a 5-iteration audit. Each one corresp
|
|
|
86
86
|
|
|
87
87
|
1. **Every `addEventListener` in `connected()` has a matching `removeEventListener` in `disconnected()`.** Handler must be a stable `#field` arrow (`#onClick = (e) => { ... }`), never an inline arrow passed to `addEventListener`. Inline arrows can't be removed — `removeEventListener` needs reference equality.
|
|
88
88
|
|
|
89
|
-
2. **`
|
|
89
|
+
2. **`UIFormElement` subclasses call `super.connected()` and `super.disconnected()`.** `ElementInternals` registration depends on it. Omitting `super` strands the form-association.
|
|
90
90
|
|
|
91
91
|
3. **Timers and observers are torn down.** `clearInterval`, `clearTimeout`, `ResizeObserver.disconnect()`, `MutationObserver.disconnect()`, `IntersectionObserver.disconnect()` all in `disconnected()`.
|
|
92
92
|
|
|
@@ -164,7 +164,7 @@ Before declaring the work done, run through this checklist. If anything fails, f
|
|
|
164
164
|
- [ ] Every `static properties` entry uses `attribute:` (not `attr:`).
|
|
165
165
|
- [ ] Every state-bearing Boolean has `reflect: true`.
|
|
166
166
|
- [ ] No reserved-name anti-patterns (`title`, parent-level `active`, `error` variant, `disabled` on non-form, exclusion-`multiple`).
|
|
167
|
-
- [ ] Element tag ends in `-ui`; class is `
|
|
167
|
+
- [ ] Element tag ends in `-ui`; class is `UI<Component>`.
|
|
168
168
|
|
|
169
169
|
### CSS
|
|
170
170
|
|
|
@@ -179,7 +179,7 @@ Before declaring the work done, run through this checklist. If anything fails, f
|
|
|
179
179
|
|
|
180
180
|
- [ ] Every `addEventListener` in `connected()` has a paired `removeEventListener` in `disconnected()`.
|
|
181
181
|
- [ ] All handlers passed to `addEventListener` are stable `#field` arrows (or bound method refs), not inline arrows.
|
|
182
|
-
- [ ] If the class extends `
|
|
182
|
+
- [ ] If the class extends `UIFormElement`, both `connected()` and `disconnected()` call `super`.
|
|
183
183
|
- [ ] Every timer/observer created in `connected()` is disposed in `disconnected()`.
|
|
184
184
|
- [ ] Cached DOM refs (`this.#fooEl`) are nulled in `disconnected()`.
|
|
185
185
|
- [ ] Class declares `disconnected()` exactly once.
|
|
@@ -210,7 +210,7 @@ Native DOM accessors (`textContent`, `innerHTML`) get clobbered if declared in `
|
|
|
210
210
|
Every `addEventListener` in `connected()` must be paired with a `removeEventListener` in `disconnected()`. Every timer cleared, every observer disconnected, every popover removed. Handlers must be stable `#field` arrows so `removeEventListener` finds them via reference equality.
|
|
211
211
|
|
|
212
212
|
```js
|
|
213
|
-
class
|
|
213
|
+
class UIBadge extends UIElement {
|
|
214
214
|
#onRemove = (e) => { /* ... */ }; // stable handler
|
|
215
215
|
|
|
216
216
|
connected() {
|
|
@@ -226,7 +226,7 @@ class AdiaBadge extends AdiaElement {
|
|
|
226
226
|
|
|
227
227
|
Inline arrows passed to `addEventListener` look fine and leak quietly — `removeEventListener` no-ops because the new arrow has different identity. Three components in one audit cycle bit on this exact bug.
|
|
228
228
|
|
|
229
|
-
If the class extends `
|
|
229
|
+
If the class extends `UIFormElement`, `connected()` and `disconnected()` MUST call `super.*` — `ElementInternals` registration depends on it. And declare `disconnected()` exactly once per class; the second silently overrides the first.
|
|
230
230
|
|
|
231
231
|
Full lifecycle patterns in [lifecycle-patterns.md](lifecycle-patterns.md).
|
|
232
232
|
|
|
@@ -276,11 +276,11 @@ When transparency-mixing colors, use `oklab` not `oklch` for the interpolation s
|
|
|
276
276
|
|
|
277
277
|
## Legacy avoidance
|
|
278
278
|
|
|
279
|
-
The library was renamed from `@agent-ui-kit/*` to `@adia-ai/*`; tokens went from `--n-*` to `--a-*`; the base class went
|
|
279
|
+
The library was renamed from `@agent-ui-kit/*` to `@adia-ai/*`; tokens went from `--n-*` to `--a-*`; the base class went `NanoElement` → `AdiaElement` → `UIElement` (ADR-0016 class rename). Don't use the legacy forms in new code. Don't reference `nano-ui` in notes, commit messages, CHANGELOG entries, or any AdiaUI artifact.
|
|
280
280
|
|
|
281
281
|
```js
|
|
282
282
|
// right
|
|
283
|
-
import {
|
|
283
|
+
import { UIElement } from '@adia-ai/web-components/core';
|
|
284
284
|
:where(:scope) { --button-bg: var(--a-primary-bg); }
|
|
285
285
|
|
|
286
286
|
// wrong
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# Lifecycle Patterns — Listeners, Timers, Observers, Popovers
|
|
2
2
|
|
|
3
|
-
Deep dive on symmetric `connected()` / `disconnected()` in AdiaUI components. Read when adding any side effect (listener, timer, observer, popover) to a component, or when inheriting from `
|
|
3
|
+
Deep dive on symmetric `connected()` / `disconnected()` in AdiaUI components. Read when adding any side effect (listener, timer, observer, popover) to a component, or when inheriting from `UIFormElement`.
|
|
4
4
|
|
|
5
5
|
## The symmetry rule
|
|
6
6
|
|
|
7
7
|
Every side effect set up in `connected()` has a matching teardown in `disconnected()`. The teardown is authored at the same time as the setup, not later — if you leave `disconnected()` for tomorrow, tomorrow won't happen.
|
|
8
8
|
|
|
9
9
|
```javascript
|
|
10
|
-
class
|
|
10
|
+
class UIWidget extends UIElement {
|
|
11
11
|
static properties = { /* ... */ };
|
|
12
12
|
static template = () => null;
|
|
13
13
|
|
|
@@ -150,16 +150,16 @@ disconnected() {
|
|
|
150
150
|
|
|
151
151
|
**Lesson:** a class must declare `disconnected()` exactly once. If you need to add teardown, edit the existing method. Duplicate method names silently shadow — no error, no warning.
|
|
152
152
|
|
|
153
|
-
## `
|
|
153
|
+
## `UIFormElement` — `super` discipline
|
|
154
154
|
|
|
155
|
-
Form-participating components inherit `ElementInternals` wiring from `
|
|
155
|
+
Form-participating components inherit `ElementInternals` wiring from `UIFormElement`. That wiring only works if `super.connected()` and `super.disconnected()` are called:
|
|
156
156
|
|
|
157
157
|
```javascript
|
|
158
|
-
import {
|
|
158
|
+
import { UIFormElement } from '../../core/form.js';
|
|
159
159
|
|
|
160
|
-
class
|
|
160
|
+
class UIInput extends UIFormElement {
|
|
161
161
|
static properties = {
|
|
162
|
-
...
|
|
162
|
+
...UIFormElement.properties,
|
|
163
163
|
placeholder: { type: String, default: '', reflect: true },
|
|
164
164
|
};
|
|
165
165
|
|
|
@@ -296,7 +296,7 @@ Especially important for components that are attached/detached frequently (modal
|
|
|
296
296
|
- **Inline arrow + `removeEventListener`** — the listener isn't really being removed. Silent leak.
|
|
297
297
|
- **Forgot to create `disconnected()`** — if `connected()` adds side effects, `disconnected()` must exist. No exceptions.
|
|
298
298
|
- **Two `disconnected()` methods in the same class** — the second silently wins; the first's cleanup is lost.
|
|
299
|
-
- **`super.connected()` missing in `
|
|
299
|
+
- **`super.connected()` missing in `UIFormElement` subclass** — form participation silently fails.
|
|
300
300
|
- **Popover / tooltip never cleaned up** — stays in `document.body` forever.
|
|
301
301
|
- **Observer recreated on every render** — re-observing without disconnecting first leaks observer refs.
|
|
302
302
|
- **Timer fired after component removed** — not cleared in `disconnected()`, fires on a dead component. Can cause null-deref crashes or phantom re-renders.
|
|
@@ -14,7 +14,7 @@ Every component's `.css` file must follow the **two-block @scope** pattern:
|
|
|
14
14
|
/* Block 1: token declarations on :where(:scope) — zero specificity */
|
|
15
15
|
@scope (my-component-ui) {
|
|
16
16
|
:where(:scope) {
|
|
17
|
-
--my-bg: var(--a-
|
|
17
|
+
--my-bg: var(--a-bg);
|
|
18
18
|
--my-text: var(--a-fg);
|
|
19
19
|
--my-radius: var(--a-radius-md);
|
|
20
20
|
}
|
|
@@ -30,8 +30,8 @@ Every component's `.css` file must follow the **two-block @scope** pattern:
|
|
|
30
30
|
|
|
31
31
|
/* Variants override TOKENS, not base styles */
|
|
32
32
|
:scope[variant="danger"] {
|
|
33
|
-
--my-bg: var(--a-danger-
|
|
34
|
-
--my-text: var(--a-danger-
|
|
33
|
+
--my-bg: var(--a-danger-bg);
|
|
34
|
+
--my-text: var(--a-danger-fg);
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
```
|
|
@@ -40,7 +40,7 @@ Every component's `.css` file must follow the **two-block @scope** pattern:
|
|
|
40
40
|
|
|
41
41
|
1. **Zero raw color values** anywhere in component CSS
|
|
42
42
|
- No `#fff`, `rgb(...)`, `oklch(...)` literals
|
|
43
|
-
- All colors must reference `--a-chrome-*`, `--a-data-0..9`, `--a-fg*`, `--a-
|
|
43
|
+
- All colors must reference `--a-chrome-*`, `--a-data-0..9`, `--a-fg*`, `--a-bg-*`, or family tokens (`--a-brand-*`, `--a-primary-*`, etc.)
|
|
44
44
|
2. **Two-block pattern** — token declarations separate from style rules
|
|
45
45
|
3. **Variants override tokens** — never rewrite base styles inside `[variant]` selectors. Change token values; the base styles absorb the change.
|
|
46
46
|
4. **`:where(:scope)`** for token defaults — zero specificity lets consumers override tokens from outside
|
|
@@ -75,7 +75,7 @@ Every component's `.css` file must follow the **two-block @scope** pattern:
|
|
|
75
75
|
4. **Check token usage in parent stylesheet**
|
|
76
76
|
|
|
77
77
|
```bash
|
|
78
|
-
grep -n "--a-chrome\|--a-data\|--a-fg\|--a-
|
|
78
|
+
grep -n -- "--a-chrome\|--a-data\|--a-fg\|--a-bg" \
|
|
79
79
|
packages/web-components/components/<name>/*.css
|
|
80
80
|
```
|
|
81
81
|
|