@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.
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +21 -0
- package/README.md +5 -3
- package/agents/a2ui-maintenance-agent.corpus.json +1 -1
- package/agents/a2ui-maintenance-agent.md +8 -10
- package/agents/a2ui-planner-agent.md +9 -11
- package/agents/{framework-reviewer.corpus.json → demo-audit-agent.corpus.json} +12 -12
- package/agents/demo-audit-agent.md +51 -0
- package/agents/{framework-alignment-agent.md → framework-checker.md} +14 -15
- package/agents/gen-ui-review-agent.corpus.json +30 -0
- package/agents/gen-ui-review-agent.md +53 -0
- package/agents/package-release-agent.md +8 -11
- package/agents/primitive-authoring-agent.corpus.json +1 -1
- package/agents/primitive-authoring-agent.md +6 -8
- package/agents/routing-corpus.json +13 -13
- package/package.json +1 -1
- package/skills/a2ui-maintenance/SKILL.md +49 -100
- package/skills/a2ui-maintenance/references/eval-diagnostics.md +18 -0
- package/skills/a2ui-maintenance/references/pipeline-overview.md +46 -0
- package/skills/demo-audit/SKILL.md +55 -120
- package/skills/demo-audit/references/auto-fix-allowlist.md +26 -0
- package/skills/demo-audit/references/mode7-status-battery.md +22 -0
- package/skills/demo-audit/references/output-contract-worked-example.md +24 -0
- package/skills/demo-audit/references/probe-discipline-and-escalation.md +27 -0
- package/skills/gen-ui-review/SKILL.md +68 -136
- package/skills/gen-ui-review/references/exit-gate-mechanics.md +26 -0
- package/skills/gen-ui-review/references/lookup-maintenance.md +8 -0
- package/skills/gen-ui-review/references/loop-protocol.md +16 -0
- package/skills/gen-ui-review/references/scorecard-worked-examples.md +36 -0
- package/skills/package-release/SKILL.md +69 -56
- package/skills/package-release/references/authorization-model.md +34 -0
- package/skills/package-release/references/invariants-detail.md +61 -0
- package/skills/package-release/references/mechanization.md +54 -0
- package/skills/package-release/references/recovery-paths.md +10 -0
- package/skills/primitive-authoring/SKILL.md +13 -32
- package/skills/primitive-authoring/references/api-contract.md +60 -2
- package/skills/primitive-authoring/references/code-style.md +38 -0
- package/skills/primitive-authoring/references/token-contract.md +61 -1
- package/skills/site-deployment/SKILL.md +30 -88
- package/skills/site-deployment/references/deploy-playbooks.md +38 -0
- package/skills/site-docs-authoring/SKILL.md +1 -1
- package/skills/ssr-compatibility/SKILL.md +39 -74
- package/skills/ssr-compatibility/references/failure-shapes.md +172 -0
- package/skills/ssr-compatibility/references/guard-patterns.md +143 -0
- package/skills/ssr-compatibility/references/status-ledger.md +1 -0
- 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
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
51
|
-
`
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
`
|
|
83
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
-
|
|
117
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
|