@adia-ai/adia-ui-forge 0.8.43 → 0.8.45

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.
Files changed (46) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +21 -0
  3. package/README.md +5 -3
  4. package/agents/a2ui-maintenance-agent.corpus.json +1 -1
  5. package/agents/a2ui-maintenance-agent.md +8 -10
  6. package/agents/a2ui-planner-agent.md +9 -11
  7. package/agents/{framework-reviewer.corpus.json → demo-audit-agent.corpus.json} +12 -12
  8. package/agents/demo-audit-agent.md +51 -0
  9. package/agents/{framework-alignment-agent.md → framework-checker.md} +14 -15
  10. package/agents/gen-ui-review-agent.corpus.json +30 -0
  11. package/agents/gen-ui-review-agent.md +53 -0
  12. package/agents/package-release-agent.md +8 -11
  13. package/agents/primitive-authoring-agent.corpus.json +1 -1
  14. package/agents/primitive-authoring-agent.md +6 -8
  15. package/agents/routing-corpus.json +13 -13
  16. package/package.json +1 -1
  17. package/skills/a2ui-maintenance/SKILL.md +49 -100
  18. package/skills/a2ui-maintenance/references/eval-diagnostics.md +18 -0
  19. package/skills/a2ui-maintenance/references/pipeline-overview.md +46 -0
  20. package/skills/demo-audit/SKILL.md +55 -120
  21. package/skills/demo-audit/references/auto-fix-allowlist.md +26 -0
  22. package/skills/demo-audit/references/mode7-status-battery.md +22 -0
  23. package/skills/demo-audit/references/output-contract-worked-example.md +24 -0
  24. package/skills/demo-audit/references/probe-discipline-and-escalation.md +27 -0
  25. package/skills/gen-ui-review/SKILL.md +68 -136
  26. package/skills/gen-ui-review/references/exit-gate-mechanics.md +26 -0
  27. package/skills/gen-ui-review/references/lookup-maintenance.md +8 -0
  28. package/skills/gen-ui-review/references/loop-protocol.md +16 -0
  29. package/skills/gen-ui-review/references/scorecard-worked-examples.md +36 -0
  30. package/skills/package-release/SKILL.md +69 -56
  31. package/skills/package-release/references/authorization-model.md +34 -0
  32. package/skills/package-release/references/invariants-detail.md +61 -0
  33. package/skills/package-release/references/mechanization.md +54 -0
  34. package/skills/package-release/references/recovery-paths.md +10 -0
  35. package/skills/primitive-authoring/SKILL.md +13 -32
  36. package/skills/primitive-authoring/references/api-contract.md +60 -2
  37. package/skills/primitive-authoring/references/code-style.md +38 -0
  38. package/skills/primitive-authoring/references/token-contract.md +61 -1
  39. package/skills/site-deployment/SKILL.md +30 -88
  40. package/skills/site-deployment/references/deploy-playbooks.md +38 -0
  41. package/skills/site-docs-authoring/SKILL.md +1 -1
  42. package/skills/ssr-compatibility/SKILL.md +39 -74
  43. package/skills/ssr-compatibility/references/failure-shapes.md +172 -0
  44. package/skills/ssr-compatibility/references/guard-patterns.md +143 -0
  45. package/skills/ssr-compatibility/references/status-ledger.md +1 -0
  46. package/agents/framework-reviewer.md +0 -52
@@ -0,0 +1,34 @@
1
+ # Authorization model — history and mechanics
2
+
3
+ SKILL.md's own "Authorization" section states the rule; this is the history
4
+ and the mechanism behind it.
5
+
6
+ ## Why single-authorization replaced the checkpoint ceremony
7
+
8
+ This replaced the 4-checkpoint sign-off ceremony after the v0.8.5 cut
9
+ (2026-07-17): ~40 minutes of approval relays that produced zero safety —
10
+ every real protection had already fired deterministically (the
11
+ `release-pretag-docs-gate` hook, the pre-push F-N1 trip-wire, the
12
+ one-tag-per-push rule, the registry verify). Those deterministic gates are
13
+ untouched and remain non-skippable: **an F-N1 error hard-stops
14
+ unconditionally — no flag, no instruction wording, bypasses it.**
15
+
16
+ ## Why releases run inline, not dispatched
17
+
18
+ **Releases run INLINE in the operator's session by default.** Do not
19
+ dispatch a subagent to run an interactive release — a dispatched seat cannot
20
+ receive the operator's words directly, which the v0.8.5 cut proved
21
+ structurally deadlocks on any consent question (the relay-refusal loop). The
22
+ `package-release-agent` agent exists for UNATTENDED contexts only
23
+ (scheduled/batch/CI cuts), where the dispatch itself carries the operator's
24
+ authorization.
25
+
26
+ ## Mechanics
27
+
28
+ `release-pack.mjs --go` auto-confirms all checkpoints (it equals
29
+ `--yes --push --publish`); the evidence blocks still print as a log — the
30
+ audit trail survives, nothing waits on input. The granular flags remain for
31
+ cautious manual runs; interactive prompts fire for whatever flag is absent.
32
+ A cosmetic F-N1 warn still refuses auto-confirmation at the push boundary —
33
+ with Step 4f (mechanized pre-tag coverage) warns should be extinct, so one
34
+ appearing means something novel: treat it as a gate failure, not ceremony.
@@ -0,0 +1,61 @@
1
+ # Invariants — detail and history
2
+
3
+ SKILL.md's own "Invariants" section states each rule; this expands the
4
+ history and mechanics behind the denser ones.
5
+
6
+ ## 1. Lockstep coherence
7
+
8
+ Every roster package bumps together (`check:lockstep`). **The roster is
9
+ `scripts/package-paths.mjs`'s `PACKAGE_ROSTER` — read it, never a copy.**
10
+ This line used to enumerate the names inline and went stale the first time
11
+ the roster moved (ADR-0048 P4 added a package while the list still read the
12
+ pre-split six); a name list has no gate behind it, so it can only decay.
13
+ Each plugin's `.claude-plugin/plugin.json` version moves with its
14
+ package.json — `bump.mjs` handles both; `verify:plugins` gates the sync.
15
+
16
+ ## 2. PATCH-cut asymmetry
17
+
18
+ Internal `@adia-ai/*` ranges hold at `^X.Y.0` during PATCH cuts; only MINOR
19
+ bumps the floor. `^0.0.x` is forbidden (pre-1.0 caret locks exact — shipped
20
+ a real stale-sibling bug).
21
+
22
+ ## 3. Release commits land via PR, never a direct push to `main`
23
+
24
+ (Repo policy, operator ruling 2026-07-12.) Commit on `release/vX.Y.Z` → PR →
25
+ CI → merge — THEN tag at `main`'s post-merge HEAD, not at the bump commit;
26
+ post-bump fixes merge before tagging and belong in the tarball. Exception:
27
+ batch push tags each version at its own release-merge SHA.
28
+
29
+ ## 4. One umbrella + one per-package tag per cut
30
+
31
+ One per lockstep-roster package (`scripts/package-paths.mjs`); publish
32
+ workflows key off the per-package tags. Push tags **one per `git push`** — a
33
+ batched multi-tag push drops the trigger event.
34
+
35
+ ## 5. F-N1 per-package clean
36
+
37
+ `check:release --all-pending` — the umbrella-tag mismatch error is expected
38
+ noise. Coverage is mechanized at cut time: Step 4f
39
+ (`check-release.mjs --pending-version X.Y.Z --fix`) runs the SAME matcher
40
+ pre-PR and auto-appends verified Maintenance bullets, so a post-tag warn
41
+ means something novel — stop and investigate, don't loop enrichment PRs.
42
+
43
+ ## 6. `npm dist-tag latest` is set by publish order
44
+
45
+ Batch pushes publish oldest first and WAIT for settle.
46
+
47
+ ## 7. A breaking (MINOR) cut MUST ship its MIGRATION GUIDE section in the same cycle
48
+
49
+ MINOR is reserved for removed/renamed API symbols; everything else,
50
+ including visible behavior changes, stays PATCH.
51
+
52
+ ## 8. The adia-factory plugin's `.mcp.json` pins the generation MCP exactly
53
+
54
+ Bump the pin in `packages/plugins/adia-ui-factory/.mcp.json` in the same cut
55
+ (the consumer never floats; with the plugins in-lockstep this lands in the
56
+ release commit itself, not a follow-up). The pinned NAME is `@adia-ai/mcp`
57
+ with the `gen-ui` subcommand (`npx -y @adia-ai/mcp@<exact> gen-ui`) — the
58
+ single live name since the ADR-0048 P7 cut (v0.8.37) per the gh#1240
59
+ one-package amendment; `@adia-ai/a2ui-mcp` is retired and the P2-planned
60
+ `@adia-ai/gen-ui-mcp` never published. `check:lockstep`'s mcp-pin guard and
61
+ `bump.mjs` both enforce this pin.
@@ -0,0 +1,54 @@
1
+ # Mechanization — bundled `scripts/`, stdlib Node, all support `--dry`
2
+
3
+ `release-pack.mjs` walks the cycle in two phases per invariant 3 — `--mode
4
+ cut` / `--mode from-scratch` promote `[Unreleased]` (BOTH modes now — a
5
+ peer's hand-authored section must be promoted whichever mode cut it, and a
6
+ loud guard blocks the bump if any roster package still carries non-empty
7
+ `[Unreleased]` content after promotion), run Step 4f (pre-tag coverage
8
+ `--fix` with the authoritative F-N1 matcher), and stop at the release commit
9
+ (PR → merge); `--mode handoff` tags/publishes/deploys from post-merge main.
10
+ **`--go` is the standard invocation for an operator-initiated release** — the
11
+ single authorization covers the cycle; evidence blocks print as a log;
12
+ granular `--yes`/`--push`/`--publish` remain for cautious manual runs
13
+ (`--push` still refuses to auto-fire past any F-N1 finding). `--mode batch`
14
+ is not implemented — it hard-rejects with a pointer to
15
+ [recovery-paths.md](recovery-paths.md) §Scenario 2 rather than silently
16
+ misbehaving; a real batch tags each version at its own release-merge SHA,
17
+ which this single-version orchestrator has no model for. Pre-flight (Step 3)
18
+ runs the full 30-gate roster from the single shared list in
19
+ `scripts/gate-roster.mjs` — the same list [cut-procedure.md](cut-procedure.md)
20
+ §3.1 transcribes, so a subset run requires editing that one file, not two.
21
+
22
+ The pieces are callable alone: `bump.mjs` (version fields + the factory
23
+ `.mcp.json` `@adia-ai/mcp` pin, invariant 8), `promote-unreleased.mjs` /
24
+ `insert-stub.mjs` (CHANGELOG blocks), `tag-lockstep.mjs` (umbrella + one tag
25
+ per lockstep roster package — the set is derived from `PACKAGE_ROSTER`,
26
+ never a number written here: it went stale at 14 while the roster said 17,
27
+ 17 went stale in turn when gh#1240 folded two planned MCP rows into one
28
+ (now 16), and 16 goes stale again the day the ADR-0048 shims retire; run
29
+ `--dry` to see the live list, `--delete` to move), `dispatch-publish.mjs`
30
+ (publish workflows; `--verify-triggered` re-dispatches missing AND dead —
31
+ cancelled/failed/timed-out — runs, each gated behind an npm-registry check
32
+ so an already-published version is never re-dispatched (gh#763); `--after`
33
+ enforces batch ordering — dispatch output states what WAS done, never a
34
+ to-do phrased as if it hadn't fired), and `pr-bridge.mjs` (the two-phase
35
+ bridge: pushes the release branch, opens the PR, waits out CI, and merges
36
+ ONLY on all-non-fail-soft-checks-green + zero unresolved threads + no
37
+ changes-requested; any other state stops with evidence — §5.7 mechanized,
38
+ `--dry` to preview the decision). All fail fast outside a monorepo root
39
+ (`assert-monorepo-root.mjs`); fork-configurable via `--host` / `--scope`.
40
+ All 6 CLI scripts (plus `scripts/release/check-release.mjs`) have a
41
+ `selftest` mode wired into CI's Plugin bin selftests step; the 3 helper
42
+ modules (`assert-monorepo-root`, `gate-roster`, `package-paths`) are
43
+ exercised through the CLIs' selftests, not their own.
44
+
45
+ ## Site deploy
46
+
47
+ Site deploy (Step 10 of the handoff) dispatches `deploy-site.yml` (its own
48
+ pre-flight verify → snapshot → hardened rsync → post-deploy verify →
49
+ auto-rollback), never a raw local `rsync` — H1 (forge-campaign gh#268
50
+ audit): the script had drifted into bypassing that pipeline even though this
51
+ doc already taught the workflow-dispatch path. The dispatch rides the
52
+ cycle's single authorization (`--go`, or `--publish` on a manual run); the
53
+ workflow's own GitHub environment gate is the human stop for the deploy
54
+ itself.
@@ -9,6 +9,16 @@
9
9
 
10
10
  ---
11
11
 
12
+ ## §Scenario 0 — Recon: classifying an unclear starting state
13
+
14
+ `git branch --show-current` (must be `main`) · `git status --short` ·
15
+ `git log origin/main..HEAD --oneline` · `git tag --list 'vX.Y.*'` ·
16
+ CHANGELOG heads for `## [Unreleased]`. One unpushed `release(*):` commit →
17
+ deploy handoff · several → batch push (Scenario 2) · `[Unreleased]`, no
18
+ bump → author from scratch · removed/renamed API symbol → breaking cut,
19
+ guide owed · target under `packages/plugins/*` → independent release.
20
+ Ambiguous → surface it, don't guess.
21
+
12
22
  ## §Scenario 1 — Version-skip correction
13
23
 
14
24
  **Shape:** a peer's release commit mislabels the version — package.json bumped 0.6.X → 0.6.X+2, skipping X+1; the CHANGELOG body may narrate the work as two releases. No tags yet, npm latest still 0.6.X, commit unpushed. (Real case: a "v0.6.13" cut that was actually v0.6.12's work.)
@@ -3,16 +3,14 @@ name: primitive-authoring
3
3
  description: >-
4
4
  Author or modify AdiaUI framework source inside the monorepo — primitives
5
5
  (packages/web-components), shells/composites (packages/web-modules), yaml
6
- SoTs, demos. Use when asked to add a new primitive component, add or fix a prop/slot/
7
- attribute/CSS variant, update a component's yaml, build or fix a shell
8
- (chat-shell, admin-shell, editor-shell — sidebar/pane/bespoke-tier
9
- composition), promote or extract repeated inline content into a shared
10
- module, audit a component's four-axis contract/token usage/lifecycle
11
- (connected/disconnected symmetry) for drift, or author/write a demo or
12
- examples.html for a component or composite. NOT for composing app screens
13
- from primitives (screen-composition), A2UI pipeline internals (a2ui-maintenance),
14
- @adia-ai/llm adapter/streaming/bridge internals (llm-client-maintenance), or
15
- site/pages docs pages (site-docs-authoring).
6
+ SoTs, demos. Use to add a new primitive, fix a prop/slot/attribute/CSS
7
+ variant, update a yaml, build or fix a shell (chat-shell, admin-shell,
8
+ editor-shell — sidebar/pane/bespoke-tier composition), promote repeated
9
+ inline content into a shared module, audit a component's four-axis
10
+ contract/token usage/lifecycle for drift, or author a demo or
11
+ examples.html. NOT for app screens (screen-composition), A2UI internals
12
+ (a2ui-maintenance), @adia-ai/llm internals (llm-client-maintenance), or
13
+ site/pages docs (site-docs-authoring).
16
14
  disable-model-invocation: false
17
15
  user-invocable: true
18
16
  ---
@@ -58,28 +56,11 @@ citation mechanically on every web-modules demo write; the
58
56
 
59
57
  ## First principles
60
58
 
61
- 1. **Invariants are enforced by the next author, not the linter.** A component
62
- that violates the contract silently teaches the next agent the violation is
63
- acceptable. Write as if your component is the reference the next one is
64
- patterned after because it will be.
65
- 2. **Default behavior is the absent attribute.** `<component-ui>` with no
66
- attributes does the expected default thing; every Boolean prop defaults to
67
- `false`. If the expected default is "on," the prop name is wrong — flip it
68
- (`closable` → `permanent`, `animate` → `static`).
69
- 3. **Variants change tokens; modes change layout.** A variant body contains
70
- only `--component-*: var(...)` lines — no `padding`, `display`, `position`,
71
- `width`, `height`, `gap`, `flex`, `grid`, `overflow`, `border-radius`.
72
- Layout-changing attributes are modes and require a Sanctioned Mode
73
- Attributes entry in the contract doc.
74
- 4. **Symmetric lifecycle or it's a leak.** Every listener added in
75
- `connected()` is removed in `disconnected()`; every timer cleared, observer
76
- disconnected, cached ref nulled. Handlers are stable `#field` arrows so
77
- `removeEventListener` can match — inline arrows bit three components in one
78
- audit cycle.
79
- 5. **Component tokens consume L3, not L2.** Alias from the role×state matrix
80
- (`--a-primary-bg-hover`), never the family base (`--a-primary`) — bypassing
81
- L3 strands the component outside theme / dark-mode / contrast cascades
82
- silently.
59
+ Invariants are enforced by the next author, not the linter; default behavior
60
+ is the absent attribute; variants change tokens while modes change layout;
61
+ symmetric lifecycle or it's a leak; component tokens consume L3, not L2.
62
+ Each expanded, with examples, in
63
+ [code-style.md](references/code-style.md)'s First principles section.
83
64
 
84
65
  ## Verify targets (named before executing)
85
66
 
@@ -18,10 +18,30 @@ static properties = {
18
18
  **Field rules:**
19
19
 
20
20
  - `type` — one of `String`, `Number`, `Boolean`, `Object`, `Array`. The runtime uses this to coerce attribute strings into typed values.
21
- - `default` — the value the prop takes when no attribute is present AND no JS value has been assigned. For `Boolean` props, default is ALWAYS `false` (see rule 1). For numeric props, default is `0`, a real value, or `null` for indeterminate — NEVER a sentinel like `-1`.
21
+ - `default` — the value the prop takes when no attribute is present AND no JS value has been assigned. For `Boolean` props, default is `false` in the standard shape (see rule 1); `default: true` is a rare, ratified exception carried by connect-time attribute stamping (ADR-0075, gh#961) — never the default shape to reach for. For numeric props, default is `0`, a real value, or `null` for indeterminate — NEVER a sentinel like `-1`.
22
22
  - `reflect` — when `true`, JS property changes write back to the HTML attribute so CSS can match it. Required for every state-bearing Boolean. Usually safe to omit for large value props (long strings, big objects).
23
23
  - `attribute` — explicit kebab-case mapping when the JS name doesn't auto-convert cleanly. `camelCase` → `camel-case` automatic; override via `attribute: 'max-length'` when you want non-default behavior.
24
24
 
25
+ [verified 2026-08-19] **Literal-string `"false"` parity (ADR-0075).** For a
26
+ `Boolean`-typed prop, `parseAttr` (`core/element.js:93-94`) special-cases the
27
+ literal attribute string `"false"` to parse as JS `false` — a deliberate
28
+ deviation from strict HTML boolean-attribute semantics (where any presence,
29
+ including `attr="false"`, means `true`). This matches the A2UI transpiler's
30
+ own prior `// (defensive)` special-case
31
+ (`compose/transpiler/transpiler-maps.js:206-207`), so identical markup now
32
+ parses the same in the live DOM and in transpiled/generated output. Quoting
33
+ the Decision: "for `t === Boolean`, the literal string `\"false\"` parses to
34
+ JS `false`; any other present value (including empty string) parses to
35
+ `true`; absence stays `false`." A component author or generation pipeline
36
+ that writes `interactive="false"` on a `default: true` Boolean prop gets the
37
+ intuitive result — do not assume strict HTML semantics here. (`default:
38
+ true` itself remains the rare, ratified exception this ADR's blast radius
39
+ runs against — ADR-0063's stamped-attribute mechanism, gh#961 — never the
40
+ default shape "Boolean prop naming — the flip rule" above documents.) Named
41
+ blast radius already shipped and relying on this: `password-strength-ui
42
+ show-label="false"`, `nav-group-ui collapsible="false"`
43
+ (`.examples.html` demos for both).
44
+
25
45
  ## The `attr:` silent-typo trap
26
46
 
27
47
  A real bug caught in iteration 4:
@@ -40,7 +60,7 @@ Use `attribute:` verbatim. If you think you're writing `attr:`, stop and correct
40
60
 
41
61
  ## Boolean prop naming — the flip rule
42
62
 
43
- AdiaUI conventions require `default: false` on all Booleans. The naming follows:
63
+ AdiaUI conventions require `default: false` on Boolean props in the standard shape, unless a ratified exception applies (ADR-0075's connect-time attribute stamping, gh#961 — a rare exception, not a route open to new work). The naming follows:
44
64
 
45
65
  | Intended default behavior | Wrong name (default:true) | Right name (default:false) |
46
66
  | --- | --- | --- |
@@ -61,6 +81,44 @@ AdiaUI conventions require `default: false` on all Booleans. The naming follows:
61
81
  - `enabled` (invert to `disabled`), `visible` (invert to `hidden`) — these clash with standard HTML attribute vocabulary.
62
82
  - Double-negatives like `unhide` or `dontSkip`.
63
83
 
84
+ ## Enum attribute + container-query auto-snap default — no interpolation
85
+
86
+ [verified 2026-08-19] ADR-0074 establishes the cross-component pattern for a
87
+ value-enum attribute whose sensible default is "pick automatically from live
88
+ layout, but let a consumer pin it explicitly": `chart-ui` /
89
+ `chart-legend-ui` / `chart-in-card`'s `ratio` attribute, three allowed
90
+ values (`3:2`, `1:1`, `2:3`), unset by default. Quoting the Decision:
91
+ "Unset (the default, empty string) means auto-snap: a CSS container query on
92
+ the element's own box compares its live aspect ratio against two midpoint
93
+ boundaries … and renders the nearest named ratio's studied layout — never a
94
+ scaled/interpolated blend. Setting `ratio` explicitly pins that ratio's
95
+ rendering regardless of the container's actual aspect, overriding the
96
+ container query."
97
+
98
+ Shape to follow for a new attribute of this kind:
99
+
100
+ - A plain value-enum attribute, not a boolean — ADR-0063's `no-*`
101
+ negation-prefix grammar does not apply.
102
+ - Unset/default means container-query-driven auto-snap against studied
103
+ breakpoints — discrete snapping between named values, never continuous
104
+ interpolation (an in-between state was never individually studied, so it
105
+ can't carry a "deliberately designed" claim).
106
+ - An explicit attribute value always pins and overrides the auto-snap,
107
+ never blends with it.
108
+ - The resolved value reflects back for CSS/consumer introspection —
109
+ `data-ratio-resolved` on `chart-ui` names which of the enum's snapped or
110
+ pinned values is currently in effect, distinct from the (possibly unset)
111
+ `ratio` attribute itself.
112
+ - Don't fold the new axis into an existing enum attribute that covers a
113
+ different concern (`size` stayed `sm|md|lg`-only; `ratio` didn't grow a
114
+ compound value like `lg-2:3`) — orthogonal axes get their own attribute.
115
+
116
+ `table-toolbar-ui[stage]` (ADR-0076) is the second precedent for this same
117
+ shape — see the ADR-0063 conventions list in
118
+ [token-contract.md](token-contract.md).
119
+
120
+ Source: [ADR-0074](../../../../../../docs/ops/adr/adr-0074-chart-ratio-attribute-grammar.md).
121
+
64
122
  ## Numeric props — `null` over sentinels
65
123
 
66
124
  Indeterminate, unknown, or "not yet set" numeric state uses `null`, not `-1` or `Infinity`:
@@ -1,5 +1,30 @@
1
1
  # AdiaUI code style — best practices
2
2
 
3
+ ## First principles (SKILL.md's own summary, expanded)
4
+
5
+ 1. **Invariants are enforced by the next author, not the linter.** A component
6
+ that violates the contract silently teaches the next agent the violation is
7
+ acceptable. Write as if your component is the reference the next one is
8
+ patterned after — because it will be.
9
+ 2. **Default behavior is the absent attribute.** `<component-ui>` with no
10
+ attributes does the expected default thing; every Boolean prop defaults to
11
+ `false`. If the expected default is "on," the prop name is wrong — flip it
12
+ (`closable` → `permanent`, `animate` → `static`).
13
+ 3. **Variants change tokens; modes change layout.** A variant body contains
14
+ only `--component-*: var(...)` lines — no `padding`, `display`, `position`,
15
+ `width`, `height`, `gap`, `flex`, `grid`, `overflow`, `border-radius`.
16
+ Layout-changing attributes are modes and require a Sanctioned Mode
17
+ Attributes entry in the contract doc.
18
+ 4. **Symmetric lifecycle or it's a leak.** Every listener added in
19
+ `connected()` is removed in `disconnected()`; every timer cleared, observer
20
+ disconnected, cached ref nulled. Handlers are stable `#field` arrows so
21
+ `removeEventListener` can match — inline arrows bit three components in one
22
+ audit cycle.
23
+ 5. **Component tokens consume L3, not L2.** Alias from the role×state matrix
24
+ (`--a-primary-bg-hover`), never the family base (`--a-primary`) — bypassing
25
+ L3 strands the component outside theme / dark-mode / contrast cascades
26
+ silently.
27
+
3
28
  Modern AdiaUI is small, declarative, and token-driven. Most of the "bugs" agents write are bugs _against the conventions_ — bare `<div>`s where `col-ui` belongs, raw `<input>` where `input-ui` belongs, hex colors where tokens belong. The conventions are not stylistic preferences; each one corresponds to a working feature (theme switching, density modes, form association, focus rings) that breaks silently when the convention is violated.
4
29
 
5
30
  When in doubt: look up the catalog (the `lookup_component` MCP tool, or the adia-factory plugin's composition skill), pick the existing primitive, wire through tokens.
@@ -221,6 +246,19 @@ Default behavior is the absent attribute. Attributes exist to opt OUT or carry a
221
246
 
222
247
  State-bearing Booleans must `reflect: true` so CSS can match `:scope[disabled]`, `:scope[selected]`, etc. Without reflection, hover / active / selected styles break silently.
223
248
 
249
+ [verified 2026-08-19] **The literal string `"false"` is not "presence = true"
250
+ here.** ADR-0075 special-cases `parseAttr` for `Boolean`-typed props so the
251
+ literal attribute string `"false"` parses to JS `false`, not `true` —
252
+ deviating from strict HTML semantics on purpose, to match this repo's own
253
+ A2UI transpiler (which already special-cased it defensively) and the
254
+ authoring intuition both human and generated markup default to. Writing
255
+ `show-label="false"` on a `default: true` Boolean prop does what it looks
256
+ like it does. (`default: true` is itself the rare, ratified exception to
257
+ this section's "every Boolean defaults to `false`" rule — ADR-0063's
258
+ stamped-attribute mechanism, gh#961 — not a license to skip the flip rule
259
+ above for a new prop.) See [api-contract.md](api-contract.md)'s field-rules
260
+ section for the full rule and shipped blast radius (`core/element.js:93-94`).
261
+
224
262
  Native DOM accessors (`textContent`, `innerHTML`) get clobbered if declared in `static properties` — `installProps` overrides the native setter and `el.textContent = ''` becomes a signal write, not a child-wipe. Don't declare those names in `static properties`.
225
263
 
226
264
  ## Symmetric lifecycle
@@ -142,7 +142,7 @@ mechanics; never work from this summary alone):
142
142
  granted since: `qr-code-ui[color]` / `icon-ui[weight]`, ADR-0070).
143
143
 
144
144
  Beyond the global grammar, ADR-0063
145
- (`docs/ops/adr/adr-0063-attribute-grammar-addendum.md`) ratifies six
145
+ (`docs/ops/adr/adr-0063-attribute-grammar-addendum.md`) ratifies seven
146
146
  CROSS-SIBLING conventions for component-local attribute naming — the axis
147
147
  ADR-0053/0054 don't cover. Any new attribute follows these:
148
148
 
@@ -179,12 +179,72 @@ ADR-0053/0054 don't cover. Any new attribute follows these:
179
179
  - **`-picker` is reserved for the outer trigger+popover form-associated
180
180
  composite** — never the inline substrate it composes. `color-picker-ui`
181
181
  (the inline substrate) renames to `color-area-ui`.
182
+ - **[verified 2026-08-19] Preset-boolean-vs-alias-retirement boundary**
183
+ (ADR-0076): a boolean attribute that is additive sugar over several
184
+ existing granular `no-*` opt-outs — `table-toolbar-ui[chrome-only]`
185
+ equivalent to setting all four of `no-filter`, `no-sort`, `no-columns`,
186
+ `no-search` — is NOT an ADR-0063-style alias-retirement case (the
187
+ granular attributes aren't duplicate spellings of one concept the way
188
+ `alert[dismissible]`/`[closable]` were; a consumer may legitimately want
189
+ a subset off, a combination the preset alone can't express), so the
190
+ granular attributes stay shipped, independently-addressable API with no
191
+ deprecation. The preset's precedence is **pure, absolute OR — never a
192
+ tri-state**: while set, all covered controls are off, full stop, with no
193
+ partial re-enable via clearing an individual `no-*` while the preset
194
+ remains set (ADR-0076: "`no-*` attributes are presence-based booleans …
195
+ meaning 'presence forces off' is expressible but 'absence means defer to
196
+ [the preset]'s own OR' is not without a tri-state amendment"). A second
197
+ auto-snap-enum precedent alongside ADR-0074's chart `ratio` lands here
198
+ too: `table-toolbar-ui[stage]` (`full | search-tight | icon-only |
199
+ overflow`) — unset auto-snaps via `@container` queries against studied
200
+ breakpoints, an explicit value pins and overrides the query, no
201
+ interpolation between stages.
182
202
 
183
203
  The renames above have LANDED: dual-read compat shims shipped via gh#1563,
184
204
  and the breaking cut removed the old names in 0.8.43 (gh#1617). The new
185
205
  spellings are the sole ones — a yaml or demo still showing an old name is
186
206
  stale and should be fixed.
187
207
 
208
+ ## Disabled-state tokens
209
+
210
+ [verified 2026-08-19] ADR-0073 ratifies the standing convention for any
211
+ control supporting the `disabled` boolean attribute/state:
212
+
213
+ - **Shared bg role, container-low tier.** `--a-ui-bg-disabled`
214
+ (`styles/colors/semantics/features.css`) resolves to
215
+ `var(--md-sys-color-neutral-container-low)` — the same 10%-tint role
216
+ `--a-bg-hover` / `--a-bg-muted` ride for REST-state de-emphasis. A
217
+ component's own `--<component>-bg-disabled` indirection aliases this
218
+ shared role — never a raw color, never a per-family `*-container-low`
219
+ variant (none exists in the disabled path).
220
+ - **`[state][disabled]` specificity override for checked/selected fills.**
221
+ A `[checked]`/`[selected]` selector outranks a plain `[disabled]` rule on
222
+ CSS specificity (2 attribute selectors beat 1), so a checked+disabled
223
+ control silently keeps its active-state fill unless the component adds an
224
+ explicit higher-specificity override — `[checked][disabled]` /
225
+ `[selected][disabled]` — routed through its own
226
+ `--<component>-...-checked-disabled` (or `-selected-disabled`) custom
227
+ prop, itself aliasing `--a-ui-bg-disabled`.
228
+ - **Reduced-contrast disabled border — the `--input-border-disabled`
229
+ pattern.** Any control that renders a border when disabled adds
230
+ `--<component>-border-disabled: var(--a-ui-border-disabled)`, applied as
231
+ `border-color` under `:scope[disabled]`.
232
+
233
+ Quoting ADR-0073's Decision 5: "a control supporting `disabled` uses the
234
+ shared, single neutral `--md-sys-color-neutral-container-low` role for
235
+ background … always through a `--<component>-bg-disabled` … indirection
236
+ aliasing `--a-ui-bg-disabled` … uses the reduced-contrast
237
+ `--input-border-disabled` pattern … for any border it renders when disabled
238
+ … overrides any checked/selected state fill at `[state][disabled]`
239
+ specificity rather than relying on `[disabled]` alone."
240
+
241
+ The re-runnable check is `scripts/audit/audit-disabled-fill-tokens.mjs`
242
+ (`check:disabled-fill-audit`, advisory) — it fails any `[disabled]`-scoped
243
+ `background`/`background-color` that doesn't resolve through a
244
+ disabled-aware indirection.
245
+
246
+ Source: [ADR-0073](../../../../../../docs/ops/adr/adr-0073-disabled-state-container-low-token-convention.md).
247
+
188
248
  ## When to update this reference
189
249
 
190
250
  If you add a new token category (like `--a-chrome-*` was added), update both this file and `.claude/docs/specs/component-token-contract.md`. The spec doc is the live source of truth; this file is the practitioner's checklist.
@@ -28,13 +28,11 @@ directives are findings.
28
28
 
29
29
  ## Platform contract (the non-obvious bits)
30
30
 
31
- - The app binds **`:8000` plain HTTP**; the exe.dev edge terminates TLS on
32
- `<host>.exe.xyz` and forwards to VM `:8000`. Nothing listening ⇒ exe.dev
33
- serves its "**Port 8000 unbound.**" error page (its nginx hint is just an
34
- example anything binding :8000 works). Don't bind :443 on the VM.
35
- - Default user **`exedev`** (uid 1000, in `sudo` + `docker`); service
36
- processes run as it. Preinstalled: `git`, `rsync`, `docker`. NOT
37
- preinstalled: `caddy`, `node`, `nginx` (apt + NodeSource).
31
+ - The app binds **`:8000` plain HTTP**; the exe.dev edge terminates TLS and
32
+ forwards to VM `:8000`. Nothing listening ⇒ "**Port 8000 unbound.**"
33
+ Don't bind :443 on the VM.
34
+ - Default user **`exedev`** (uid 1000, `sudo` + `docker`); service runs as
35
+ it. Preinstalled: `git`, `rsync`, `docker`. NOT: `caddy`, `node`, `nginx`.
38
36
  - **`127.0.0.1:9999` runs `shelley`** — exe.dev's internal agent,
39
37
  localhost-only. Leave it running; don't bind 9999.
40
38
  - Disk: 25 GB on `/`. New VMs ship RSA-2048-only host keys — verify the
@@ -47,18 +45,15 @@ Standard layout: `/srv/<app>/dist/` webroot (exedev-owned) ·
47
45
 
48
46
  ## Deploy-freshness cadence — a lockstep cut is not a site deploy
49
47
 
50
- `package-release` cutting and publishing the lockstep version (the roster in
51
- `scripts/package-paths.mjs` is the live count) does
52
- **not** itself update `ui-kit.exe.xyz` that only happens on a `site-v*` tag
53
- push (see below). Any lockstep cut that changes a package the site actually
54
- serves `web-components`, `web-modules`, `llm`, or `a2ui/*` **owes a site
55
- deploy in the same release cycle**, or an explicit operator decision to skip
56
- it, recorded in the release notes (who decided, why). "The release finished"
57
- is not evidence the site is current: the v0.8.x window is the proof case a
58
- lockstep cut touching served packages landed with no matching `site-v*` tag,
59
- and the deployed site sat a week behind npm before anyone noticed. When
60
- handing off from a release, check whether the cut touched a served package
61
- and close the loop before calling the cycle done.
48
+ `package-release` cutting and publishing does **not** itself update
49
+ `ui-kit.exe.xyz` only a `site-v*` tag push does. Any lockstep cut that
50
+ changes a package the site actually serves (`web-components`,
51
+ `web-modules`, `llm`, `a2ui/*`) **owes a site deploy in the same release
52
+ cycle**, or an explicit, recorded operator decision to skip it. "The
53
+ release finished" is not evidence the site is current (the v0.8.x window:
54
+ a served-package cut landed with no matching tag, site sat a week stale).
55
+ When handing off from a release, check whether the cut touched a served
56
+ package before calling the cycle done.
62
57
 
63
58
  ## Current deployments
64
59
 
@@ -72,60 +67,22 @@ VM artifacts (Caddyfile, unit, env example) live in repo `deploy/`.
72
67
 
73
68
  **Never run `npm run deploy:site` from a local shell.** Push a tag matching
74
69
  `site-v*` (or run the workflow via `workflow_dispatch`) — `deploy-site.yml`
75
- builds, dry-runs, and (once the `production-site` GitHub Environment has
76
- required reviewers configured a one-time repo-settings step, Settings
77
- Environments) waits for a human to read the dry-run's delete summary in the
78
- job before the destructive `deploy` job runs. The hardened sequence below
79
- is what the workflow automates; it's kept here as the reference for what
80
- the workflow does and as a manual fallback if CI itself is unavailable.
81
-
82
- `npm run deploy:site` still exists locally (`build:site` + `rsync -az
83
- --delete dist/ /srv/adia-ui/dist/`) for that fallback case only. It is
84
- destructive (a 2026-06-08 manual run deleted 3,572 files) — every step
85
- below is incident-earned; the reference carries the full commands and the
86
- delete-adjudication classes.
87
-
88
- 1. Build from clean, fully-merged `main`. In a fresh worktree,
89
- `npm run build -w @adia-ai/llm` runs **before** `build:site` — llm compiles
90
- at publish time and its outputs are gitignored, so without it
91
- `/packages/llm/core/index.js` 404s and component registration breaks site-wide
92
- (found live 2026-06-09).
93
- 2. Dry-run `rsync -azni --delete --exclude='packages/gen-ui/a2ui/corpus/feedback/'`
94
- and bucket **every** `*deleting` line into a known-safe class; any
95
- unexplained served-content delete aborts the deploy. The exclude protects
96
- prod-only runtime-written files (feedback logs wiped 2026-06-10).
97
- 3. Snapshot prod: `cp -al /srv/<app>/dist /srv/<app>/dist.bak-<date>`
98
- (hardlink farm — instant, and the only rollback).
99
- 4. Real rsync — the dry-run minus `-n`, SAME excludes, and the SAME BYTES:
100
- both jobs rsync the artifact-round-tripped tree, and the deploy job aborts
101
- if its tree's sha256 fingerprint differs from the one whose delete summary
102
- was approved (gh#425 — the dry-run used to read the build job's own
103
- `dist/`, so the v0.8.14 summary listed 3,128 deletions the real send never
104
- performed; an over-reporting summary trains reviewers to approve past scary
105
- numbers, and the same gap can under-report).
106
- 5. Verify a fixture **file** that only exists in the new build, then
107
- render-check a `/site/components/*` page headlessly (SPA returns 200 shell
108
- for any route; file presence alone misses the llm-404 class).
109
- 6. On verify failure: `rm -rf dist && mv dist.bak-<date> dist`. Keep the
110
- snapshot until the deploy is confirmed good.
70
+ builds, dry-runs, and waits for a human to read the delete summary before
71
+ the destructive `deploy` job runs. `npm run deploy:site` still exists
72
+ locally for CI-unavailable fallback only, and is destructive (a 2026-06-08
73
+ manual run deleted 3,572 files). The full step-by-step hardened sequence
74
+ (llm-build-first gotcha, delete-adjudication classes, snapshot, verified
75
+ rsync, fixture+render verify, rollback) is in
76
+ [deploy-playbooks.md](references/deploy-playbooks.md)'s "Hardened
77
+ `--delete` deploy sequence" every step there is incident-earned; read it
78
+ before running the fallback by hand.
111
79
 
112
80
  If `server.js` changed: rsync it, then `sudo systemctl restart <app>`.
113
81
 
114
- ### One-time CI setup for `ui-kit.exe.xyz`
115
-
116
- - **Repo secret `SITE_DEPLOY_SSH_KEY`** — done. An ed25519 keypair generated
117
- *by a human*, never by the agent (Hard gate 1). Public half goes in the
118
- VM's `~exedev/.ssh/authorized_keys`; private half goes in
119
- Settings → Secrets and variables → Actions, pasted directly — it should
120
- never appear in an agent's Bash context or a commit.
121
- - **Environment `production-site` reviewer gate — CONFIGURED** (verified
122
- live 2026-08-11, `gh api repos/<org>/<repo>/environments`:
123
- `protection_rules` carries `required_reviewers`). The `deploy` job in
124
- `deploy-site.yml` therefore blocks on a human approval after its dry-run
125
- job — the delete-adjudication gate this skill's hardened-deploy design
126
- assumes. Changing the reviewer set is operator-only (repo Settings →
127
- Environments → `production-site`) — no agent can configure it. Re-check
128
- the API output before trusting this line; it drifts with repo settings.
82
+ One-time CI setup for `ui-kit.exe.xyz` (the deploy SSH key, the
83
+ `production-site` reviewer gate) is in
84
+ [deploy-playbooks.md](references/deploy-playbooks.md)'s own section by
85
+ that name.
129
86
 
130
87
  ## Other playbooks (reference §-anchors)
131
88
 
@@ -161,24 +118,9 @@ rollback state: not-needed | rolled-back — <if rolled back, what triggered
161
118
  verdict: shipped | held — <one line>
162
119
  ```
163
120
 
164
- Filled example (a real cut, `gh run view 29586391343`):
165
-
166
- ```text
167
- Deploy Record
168
- tag / run id: site-v4 (workflow run 29586391343, 2026-07-17T14:03:15Z)
169
- dry-run deletes: see the run's dry-run job log for the class breakdown
170
- fixture verified: pass — CI's post-deploy verify step, run marked success
171
- render verified: pass — CI's post-deploy verify step, run marked success
172
- snapshot: CI pre-deploy hardlink step (deploy-site.yml)
173
- rollback state: not-needed
174
- verdict: shipped
175
- ```
176
-
177
- This example cites the run URL rather than restating its log inline — the
178
- record's job is to point at the evidence, not transcribe it; re-derive the
179
- dry-run/fixture/render lines from `gh run view <id> --log` if the detail is
180
- ever needed, don't assume this filled example's prose stays current with a
181
- run that already happened.
121
+ A filled worked example (a real cut) is in
122
+ [deploy-playbooks.md](references/deploy-playbooks.md)'s own "Deploy Record"
123
+ section.
182
124
 
183
125
  ## Hard gates
184
126