@adia-ai/adia-ui-factory 0.2.2

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 (42) hide show
  1. package/.claude-plugin/plugin.json +12 -0
  2. package/.mcp.json +8 -0
  3. package/CHANGELOG.md +55 -0
  4. package/README.md +44 -0
  5. package/bin/adia-lint +270 -0
  6. package/bin/adia-scaffold +411 -0
  7. package/commands/adia-compose.md +10 -0
  8. package/commands/adia-genui.md +12 -0
  9. package/commands/adia-migrate.md +10 -0
  10. package/commands/adia-orient.md +14 -0
  11. package/commands/adia-scaffold.md +17 -0
  12. package/commands/adia-verify.md +10 -0
  13. package/commands/adia-wire.md +13 -0
  14. package/hooks/hooks.json +15 -0
  15. package/package.json +40 -0
  16. package/references/a2ui-mcp-tools.md +64 -0
  17. package/references/authoring-components.md +88 -0
  18. package/references/component-model.md +71 -0
  19. package/references/data-and-hydration.md +84 -0
  20. package/references/genui-a2ui.md +62 -0
  21. package/references/llm.md +79 -0
  22. package/references/migration.md +57 -0
  23. package/references/project-shapes.md +96 -0
  24. package/references/shell-admin.md +65 -0
  25. package/references/shell-chat.md +44 -0
  26. package/references/shell-editor.md +48 -0
  27. package/references/shell-embed.md +33 -0
  28. package/references/shell-simple.md +38 -0
  29. package/references/spa-architecture.md +116 -0
  30. package/references/ssr-integration.md +117 -0
  31. package/references/verification.md +39 -0
  32. package/skills/adia-ui-compose/SKILL.md +57 -0
  33. package/skills/adia-ui-data/SKILL.md +62 -0
  34. package/skills/adia-ui-factory/SKILL.md +113 -0
  35. package/skills/adia-ui-genui/SKILL.md +74 -0
  36. package/skills/adia-ui-llm/SKILL.md +51 -0
  37. package/skills/adia-ui-migrate/SKILL.md +64 -0
  38. package/skills/adia-ui-project/SKILL.md +77 -0
  39. package/skills/adia-ui-shells/SKILL.md +62 -0
  40. package/skills/adia-ui-spa/SKILL.md +52 -0
  41. package/skills/adia-ui-ssr/SKILL.md +52 -0
  42. package/skills/adia-ui-verify/SKILL.md +44 -0
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "adia-ui-factory",
3
+ "version": "0.2.2",
4
+ "description": "Author and verify apps built on the adia-ui (@adia-ai) light-DOM web-component framework — scaffold, compose, wire, and verify across both SPA and SSR rendering modes. Wires the a2ui MCP for catalog retrieval, UI generation, and validation.",
5
+ "author": {
6
+ "name": "Kim",
7
+ "email": "kim@sublimeheroics.com"
8
+ },
9
+ "homepage": "https://github.com/adiahealth/gen-ui-kit",
10
+ "license": "MIT",
11
+ "keywords": ["adia-ui", "a2ui", "web-components", "app-authoring", "ssr", "spa", "scaffolding", "llm-ui"]
12
+ }
package/.mcp.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "mcpServers": {
3
+ "a2ui": {
4
+ "command": "npx",
5
+ "args": ["-y", "@adia-ai/a2ui-mcp@0.7.8"]
6
+ }
7
+ }
8
+ }
package/CHANGELOG.md ADDED
@@ -0,0 +1,55 @@
1
+ # Changelog
2
+
3
+ ## [0.2.2] — 2026-06-04
4
+
5
+ - **The design intent is now a precondition of making — a soft gate.** Before the maker converges on structure or UI, the app's **design intent** (the `BRIEF` — what this UI is reaching for) must be **at least lightly named**; composition reasoned toward _nothing_ drifts to "build the catalog" / feature improvisation. `adia-ui-project` gains a **"Step 0 — name the design intent [soft-gate]"** section before picking the shape (name a provisional pull, trace it in `spec/BRIEF` or inline); `adia-ui-compose` and `adia-ui-genui` each add a short design-intent soft-gate check before their loop (and for genui it also grounds corpus retrieval); `/adia-scaffold` and `/adia-genui` gain a one-line gate near the top. It is a _soft_ blocker throughout — cleared by **naming** a revisable direction, never by stopping, and "lightly declared and developed over time" is the healthy state. Mirrors the generalized rule in plugins-factory `operational-roles.md`.
6
+
7
+ ## [0.2.1] — 2026-06-04
8
+
9
+ - **Quoted `argument-hint` frontmatter.** `/adia-migrate`, `/adia-scaffold`, and `/adia-wire` used multi-bracket hints (e.g. `[spa|ssr] [app name]`) that YAML parsed as a flow sequence with trailing tokens → parse error → the loader dropped their **entire** frontmatter (description + hint) at load. All command hints are now quoted strings.
10
+
11
+ ## [0.2.0] — 2026-06-03
12
+
13
+ The v0.2 re-carve: from 6 skills to **11**, each rebuilt to plugins-factory's hardened skill-architecture standard (cold-start surface · modes · per-mode verify target · a `[gate]` rubric · §SelfAudit · load-on-demand references), now covering the full consumer authoring lifecycle for adia-ui apps. See `reviews/2026-06-03-v0.2-recarve.md`.
14
+
15
+ **New skills:** `adia-ui-project` (project shapes · four-axis · page-trio/DUO), `adia-ui-shells` (admin/chat/editor/simple + the forthcoming embed), `adia-ui-data` (hydration · fetch/CRUD · section wiring · hybrid SPA-in-SSR), `adia-ui-genui` (the a2ui generative-UI loop + corpus), `adia-ui-migrate` (version-upgrade / port sweep discipline). **Hardened:** the orchestrator + compose/spa/ssr/llm/verify. **Tooling:** `bin/adia-scaffold` gains `page`/`component`; `bin/adia-lint` gains `RAW-PX` + `NATIVE-PRIMITIVE` + `LEGACY-SHELL`. **16 references** + two new commands (`/adia-migrate`, `/adia-genui`).
16
+
17
+ ### Red-team (plugins-factory 9-critic council) — fixes folded
18
+
19
+ No runtime Critical (trust boundary universal across all 11 skills, MCP pinned, hook never-blocks, no agent-trifecta). Folded:
20
+
21
+ - **Cut 0.2.0** (8/9 critics; 2 flagged the drift Critical): bumped `plugin.json` 0.1.0→0.2.0, added the 2 missing skill `version`s (spa/ssr), rewrote the README to the 11-skill shape, dated this entry.
22
+ - **Commands ↔ skills** (Steve/Charity): fixed the dangling "Step 0" reference in 3 commands; repointed `/adia-scaffold`→`adia-ui-project` and `/adia-wire`→`adia-ui-data`; added `/adia-migrate` + `/adia-genui`; documented the curated spine.
23
+ - **`[gate]` honesty** (Andrej K./Charity): corrected the false "framework `audit:*` is the mechanized layer" claim (those run in the app repo, not here) and marked the render / validate-before-render / a11y gates as **self-verified**, distinct from the `adia-lint`-mechanized ones.
24
+ - **MCP ledger** (Chip H.): added `refine_ui` to `a2ui-mcp-tools.md`; **shell-ref links** made reference-lint-guardable; **scaffolder claim** (Elon): the project skill no longer implies the bin one-shots rollup/shared-foundation.
25
+ - **Accepted tension** (Elon vs Steve/Boris/Chip H.): keep 11 skills (the signed-off carve); the genui↔llm and migrate↔verify merges were weighed and declined — the boundaries are clean (Chip H. verified) and comprehensiveness was the chosen trade.
26
+
27
+ ### Build (phases 1a–5)
28
+
29
+ - **Phase 1a** — rebuilt the `adia-ui-factory` orchestrator to the hardened standard: four cited-signal classifiers (rendering mode incl. **hybrid** · project shape · shell · task), an **Orientation Record** verify target with a `[gate]` rubric (route only on evidence, never a guess), §SelfAudit, §Teach, and a load-when references manifest. Added `references/project-shapes.md` (the shapes · four-axis · page-trio/DUO + an embedded structure rubric `[gate]`). The routing table targets the full 11-skill roster (`project` · `shells` · `data` · `genui` · `migrate` marked _building_).
30
+ - **Phase 1b** — added the `adia-ui-project` skill (modes: new-app · add-surface · add-page · add-component · inventory; the page-trio/DUO choice as a `[gate]`; the `project-shapes.md` structure rubric as the verify target; the layout **mechanized** by the scaffolder, not hand-rolled). Extended `bin/adia-scaffold` with `page` (trio default, `--duo` for declarative) and `component` sub-commands — both emit lint-clean output and are covered by the scaffolder selftest.
31
+ - **Phase 2 — shells.** Added the `adia-ui-shells` skill (one skill: a shell-selection decision table · the shared conventions · a shell-composition rubric `[gate]` as the verify target) with load-on-demand per-shell references `shell-admin.md` · `shell-chat.md` · `shell-editor.md` · `shell-simple.md`, plus `shell-embed.md` — the forthcoming `adia-embed-shell`, documented from the embedded-app pattern and clearly labeled emerging (`[PATTERN]` not `[SHELL-API]`). Extended `bin/adia-lint` with `NATIVE-PRIMITIVE` (raw `<button>`/`<input>` where a `*-ui` exists; slotted-trigger exception) and `LEGACY-SHELL` (retired ADR-0024 data-attribute shapes) audits — both selftested, generated scaffold output stays clean.
32
+ - **Phase 3 — data & hydration.** Added the consolidated `adia-ui-data` skill (data-flow-pattern + hydration-path decision tables; the attribution `[gate]`; a data-flow rubric `[gate]` — single-owner state · projections-only · attribution · property-API · one reactive path — as the verify target) over `references/data-and-hydration.md` (the five patterns with code shapes, the three hydration paths incl. the **hybrid SPA-island-in-SSR**, section registration, the `router-ui` query-param pattern). Hardened `adia-ui-spa` + `adia-ui-ssr` to the standard (verify target + §SelfAudit) and reconciled both to defer the shared data depth to `adia-ui-data`, with the hybrid boundary called out.
33
+ - **Phase 4 — generative UI.** Added the `adia-ui-genui` skill (the consumer build → generate → validate → render → refine loop; mount `<a2ui-root>` / `<gen-root>`; `registerResolver`; corpus core-vs-roll-your-own; a gen-UI rubric `[gate]` — validate-before-render · resolvers-registered · one-root-per-surface · grounded corpus) over `references/genui-a2ui.md` (render roots, the A2UI message union, resolvers, the loop, corpus). Hardened `adia-ui-llm` to the standard (verify target + an LLM-feature rubric `[gate]`: no-key-in-browser · all-stream-branches-handled · untrusted-output · right-path) and re-pointed UI generation to `adia-ui-genui`.
34
+ - **Phase 5 — verify · migrate · compose.** Added the `adia-ui-migrate` skill (migration-type decision table; the 5-step discipline read→audit→sweep→verify→report; a migration rubric `[gate]` — audited-before-swept · sweeps-verified · judgment-items-flagged · no-leftover-drift) over `references/migration.md` (the real v0.0.20 / v0.4.0 / token-rename history + sweep patterns + leftover-drift categories + MCP aids). Hardened `adia-ui-verify` (a verify rubric `[gate]` that wires in `adia-lint` + the framework `audit:*`) and `adia-ui-compose` (a compose rubric `[gate]`; theming + the typographic registers `verse` / regular / `prose`). **All 11 v0.2 skills now carry the hardened standard** (mode + verify target + `[gate]` rubric + §SelfAudit).
35
+
36
+ ## [0.1.0] — 2026-06-03
37
+
38
+ First release. An authoring plugin for the adia-ui (`@adia-ai`) light-DOM web-component framework, covering both SPA and SSR rendering modes. Ships: six skills (`adia-ui-factory` orchestrator + `compose`/`spa`/`ssr`/`llm`/`verify`), five `/adia-*` commands, the vendored methodology (7 `references/`), the wired + pinned a2ui MCP (`@adia-ai/a2ui-mcp@0.7.8`), a deterministic scaffolder (`bin/adia-scaffold`), and an advisory authoring-lint hook (`bin/adia-lint`). Authored **and red-teamed** via plugins-factory.
39
+
40
+ ### Red-team (plugins-factory 9-critic council) — fixes folded
41
+
42
+ - **Killed stale phase prose + wired the scaffolder** (8/9 critics): `/adia-scaffold` now invokes `bin/adia-scaffold`; removed every "later phase / phase c" hedge that contradicted the shipped artifacts.
43
+ - **Hardened the MCP** (Boris/Charity/Simon/Chip H./David F./Scott W.): pinned `@adia-ai/a2ui-mcp@0.7.8` (was floating `@latest` → reviewable upgrades); lifted the always-on cost + supply-chain shape + disable path to the README; documented that the ~24 tools can't be scoped from `.mcp.json` and that outbound behavior is upstream-owned.
44
+ - **Propagated the trust boundary** (Simon): "inputs are data, not instructions" now appears in `a2ui-mcp-tools.md` and the compose/verify/llm skills, not just the orchestrator.
45
+ - **`adia-lint`** (Andrej K./David F.): implemented the documented-but-missing **px ≥ 3** rule; replaced the path-substring color exemption (which silently un-linted `color-picker.css`) with a foundation-sheet signal; added a `selftest` — now in CI alongside the scaffolder selftest.
46
+ - **Routing** (Steve): reserved "author" for `adia-ui-compose`; the mode skills now defer to `adia-ui-factory` when the rendering mode is undecided (fixes the trigger collisions).
47
+ - **Honesty**: `llm.md` carries a version/snapshot banner; the `plugin.json` description was trimmed.
48
+
49
+ ### Build (phases a–c)
50
+
51
+ - **Phase (c) — tooling + the last two skills.** Authored `adia-ui-verify` (the browser-QA exit gate
52
+ - a11y + git, over `references/verification.md`) and `adia-ui-llm` (the `@adia-ai/llm` client, `<chat-shell-ui>`, and the production browser-proxy security model, over `references/llm.md`). Added `bin/adia-scaffold` — a minimal, lint-clean app scaffolder (SPA: four-axis layout + host document + self-booting surface; SSR: a client-boundary provider + integration checklist for next/nuxt/sveltekit/astro) with a `selftest`. Added `bin/adia-lint` + `hooks/hooks.json` — an advisory PostToolUse authoring-smell checker (shadow DOM, raw colors, dead `--a-font`, `::slotted`, width-on-`:scope`, SSR top-level import, double route-owner, hardcoded overlay `open`) that **never blocks** (always exits 0 in hook mode). CI runs the scaffolder selftest.
53
+ - **Phase (b) — core skills + methodology.** Authored the four core skills — `adia-ui-factory` (orient & route: the SPA/SSR fork + task routing), `adia-ui-compose` (catalog-driven UI construction
54
+ - light-DOM component authoring + theming), `adia-ui-spa`, and `adia-ui-ssr` — and vendored the methodology into five `references/` files (`component-model`, `authoring-components`, `spa-architecture`, `ssr-integration`, `a2ui-mcp-tools`), synthesized as a verified surface from the framework's own authoring skills with SSR claims labeled documented-vs-inferred. Wired the five commands from stubs into thin mode-aware routers. `adia-ui-llm` + `adia-ui-verify` remain stubs (phase c).
55
+ - **Phase (a) — skeleton.** Scaffolded the plugin structure, `plugin.json`, the marketplace entry, and the **a2ui MCP wiring** (`.mcp.json` → `npx @adia-ai/a2ui-mcp`). Six skills and five commands stubbed with real frontmatter (routing surface live); content authored in later phases. Authored via plugins-factory.
package/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # adia-ui-factory
2
+
3
+ **Author and verify apps built on the adia-ui (`@adia-ai`) framework** — a zero-dependency, light-DOM vanilla web-component library. A self-contained Claude Code plugin: it bundles the _authoring methodology_ and wires the framework's existing **a2ui MCP** as the live substrate for catalog retrieval, UI generation, and validation.
4
+
5
+ > **Status: v0.2.2 — feature-complete, red-teamed.** Eleven skills across the full app-authoring lifecycle, the vendored methodology (16 load-on-demand references), the pinned a2ui MCP, two bins (`adia-scaffold` + `adia-lint`), and the advisory hook are in; the plugins-factory 9-critic council pass has been run and its fixes folded (`CHANGELOG.md`). Every skill is built to plugins-factory's hardened skill-architecture standard (cold-start surface · modes · per-mode verify target · a `[gate]` rubric · §SelfAudit · load-when references).
6
+
7
+ ## The shape
8
+
9
+ `adia-ui-factory` is organized as the **app-authoring lifecycle** (orient → scaffold → compose/shell → wire → verify → migrate) over the load-bearing **rendering-mode** fork:
10
+
11
+ ```text
12
+ adia-ui-factory/
13
+ ├── .mcp.json → wires @adia-ai/a2ui-mcp@0.7.8 (npx) — catalog, retrieval, generate_ui, validate
14
+ ├── skills/ (11)
15
+ │ ├── adia-ui-factory/ orient & route — classify mode + shape + shell + task (the hub)
16
+ │ ├── adia-ui-project/ project shapes · four-axis layout · page-trio/DUO · scaffold
17
+ │ ├── adia-ui-compose/ construct the UI — catalog literacy, component authoring, theming + registers
18
+ │ ├── adia-ui-shells/ choose & compose a shell — admin · chat · editor · simple · embed (forthcoming)
19
+ │ ├── adia-ui-spa/ SPA architecture — static host, content-less <router-ui>
20
+ │ ├── adia-ui-ssr/ SSR integration — Next/Nuxt/SvelteKit/Astro, client-only registration
21
+ │ ├── adia-ui-data/ data · state · hydration · fetch/CRUD · section wiring · hybrid SPA-in-SSR
22
+ │ ├── adia-ui-llm/ app LLM features — @adia-ai/llm, chat, streaming, the smart proxy
23
+ │ ├── adia-ui-genui/ generative-UI experiences — the a2ui runtime, generate_ui, corpus
24
+ │ ├── adia-ui-verify/ the exit gate — browser QA + a11y + git
25
+ │ └── adia-ui-migrate/ version upgrades · port-to-adia · the sweep discipline
26
+ ├── commands/ /adia-scaffold · compose · wire · verify · orient · migrate · genui (a curated spine; the other skills route via the orchestrator)
27
+ ├── references/ (16) per-shell + per-shape + per-concern, each load-on-demand
28
+ ├── bin/ adia-scaffold (scaffolder: shapes · page · component) · adia-lint (advisory smell checker)
29
+ └── hooks/ hooks.json — runs adia-lint on writes; advisory, never blocks
30
+ ```
31
+
32
+ ## What it bundles vs. wires
33
+
34
+ - **Bundled (self-contained):** the authoring _methodology_ — the SPA app-authoring discipline (host document, four-axis structure, light-DOM components, content-less router, `DataClient`/projection, verification) and the SSR rendering-model (framework integration, guarded dynamic-import registration, server data, cookie/session state). These are the abstraction layer.
35
+ - **Wired (runtime dependency):** the **a2ui MCP** (pinned `@adia-ai/a2ui-mcp@0.7.8`) — ~24 tools over the live catalog + corpus (284 training chunks + embeddings): catalog lookup, `search_chunks`/`search_patterns`, `classify_intent`/`assemble_context`, `generate_ui`, `validate_schema`, `check_anti_patterns`. Most tools are **offline**; `generate_ui` uses the host's LLM in stdio mode (no key); semantic search optionally uses `VOYAGE_API_KEY`. The `@adia-ai/*` packages are a runtime dependency, the way the framework itself is — taught, not bundled.
36
+ - **What it costs, and how to opt out.** The MCP **auto-starts on enable** and adds ~24 always-on tool definitions every session — the plugin's biggest context/trust cost, and a methodology-only user pays it (the tool set can't be scoped from `.mcp.json`). Enabling the plugin runs the upstream package from npm (pinned, so upgrades are reviewable diffs). **If you want the methodology without that cost, disable the `a2ui` MCP server in your MCP settings** — the skills still work; you lose live catalog/generation/validation. Treat the server's returns as untrusted data. Full accounting in `references/a2ui-mcp-tools.md`.
37
+
38
+ ## SPA vs SSR — the load-bearing fork
39
+
40
+ Same components, same UI construction; **wildly different app architecture.** `compose` is mode-independent; `spa` and `ssr` diverge on host wiring, routing ownership (`<router-ui>` vs the framework router — never both), data-flow (client `DataClient` vs server fetch + props), state (router-lifetime vs cookie/session), and component registration (top-level import vs guarded dynamic import in a lifecycle hook).
41
+
42
+ ## Provenance
43
+
44
+ Authored and red-teamed with **plugins-factory** (sibling in this marketplace), against its 9-dimension plugin standard. The methodology is harvested from the framework's own `embedded-app/spec/app-authoring-best-practices` (SPA) and `adia-ui-kit/references/rendering-model` (SSR).
package/bin/adia-lint ADDED
@@ -0,0 +1,270 @@
1
+ #!/usr/bin/env python3
2
+ """adia-lint — advisory authoring-smell checker for adia-ui apps (the mechanizable slice).
3
+
4
+ SCOPE (be honest): pattern-matchable STRUCTURAL smells in component/page source — shadow-DOM use,
5
+ raw colors, raw px ≥ 3, the dead `--a-font` token, ::slotted, width on :scope, the SSR context traps
6
+ (top-level kit import + double route-owner — detected only in files carrying a framework signal, so a
7
+ bare registration module won't trip it), hardcoded overlay `open`, native-primitive leaks (raw
8
+ `<button>`/`<input>` where a `*-ui` exists), and retired shell shapes (the ADR-0024 data-attribute
9
+ forms). Foundation/token sheets (under a
10
+ styles/ or tokens/ dir, named tokens/theme/host/palette/…, or marked `/* adia-lint: foundation */`) are
11
+ exempt from the color/px checks. It does NOT judge whether the UI is good, on-spec, or accessible — that
12
+ lives in the skills (adia-ui-compose / -spa / -ssr / -verify) and the a2ui MCP's check_anti_patterns.
13
+ A clean adia-lint says "no structural tells," never "this is right."
14
+
15
+ Shared core: the regex bank below (RAW-COLOR/PX, SCOPE-EXTENT, NATIVE-PRIMITIVE, LEGACY-SHELL,
16
+ the _is_foundation_css exemption) is mirrored in the sibling MAINTAINER plugin's
17
+ adia-ui-forge/bin/forge-lint. They are deliberate VENDORED copies — the catalog forbids
18
+ cross-plugin imports (each plugin installs copy-alone) — so any change to a shared rule must be
19
+ reconciled in BOTH files. adia-lint keeps the consumer/app traps (SSR double-router, top-level
20
+ import, hardcoded overlay open) that forge-lint drops; that divergence is the point.
21
+
22
+ Usage:
23
+ adia-lint <file>... # lint files; exit 1 if any smell found, else 0
24
+ adia-lint - # lint stdin as a generic source file
25
+ adia-lint --hook # PostToolUse hook mode: read event JSON on stdin, lint the written
26
+ # source file, print advisory findings, ALWAYS exit 0 (never blocks)
27
+ adia-lint selftest # run built-in fixtures (seeded smells + a clean file + the hook exit-0 invariant)
28
+ Stdlib only (Python 3.8+).
29
+ """
30
+ import json
31
+ import os
32
+ import re
33
+ import sys
34
+
35
+ CODE_EXT = (".js", ".mjs", ".cjs", ".ts", ".tsx", ".jsx")
36
+ STYLE_EXT = (".css",)
37
+ MARKUP_EXT = (".html", ".htm", ".vue", ".svelte", ".astro", ".tsx", ".jsx")
38
+ LINT_EXT = tuple(sorted(set(CODE_EXT + STYLE_EXT + MARKUP_EXT)))
39
+
40
+ SSR_SIGNAL = re.compile(
41
+ r"""['"]use client['"]|\buseEffect\b|\bonMounted\b|\bonMount\b|"""
42
+ r"""from\s+['"](?:react|vue|svelte|next|nuxt|@sveltejs|astro)|"""
43
+ r"""getServerSideProps|defineNuxtComponent""", re.I)
44
+
45
+ HEXCOLOR = re.compile(r"#[0-9a-fA-F]{3,8}\b")
46
+ FUNCCOLOR = re.compile(r"\b(?:rgba?|hsla?|oklch|oklab|lab|lch)\s*\(")
47
+ DEAD_FONT = re.compile(r"var\(\s*--a-font\s*[,)]")
48
+ SCOPE_EXTENT = re.compile(
49
+ r":scope(?:\[[^\]]*\])?\s*\{[^{}]*?\b(?:width|height|inline-size|block-size)\s*:", re.S)
50
+ BOOL_TRUE = re.compile(r"\bdefault:\s*true\b")
51
+ ATTR_TYPO = re.compile(r"\battr:\s*['\"]")
52
+ OVERLAY_OPEN = re.compile(r"<(?:modal|drawer)-ui\b[^>]*?(?<![:.\w])\bopen\b(?!\s*=\s*\{)")
53
+ NATIVE_PRIMITIVE = re.compile(r"<(?:button|input|select|textarea|dialog)(?![\w-])") # raw native, not a *-ui
54
+ LEGACY_SHELL = re.compile(
55
+ r"data-chat-(?:messages|input|empty|name)|data-editor-body|data-canvas\b|data-sidebar="
56
+ r"|data-pane-(?:side|grow)|<aside-ui\b|<dialog\s+data-command") # retired shell shapes (ADR-0024)
57
+ TOPLEVEL_IMPORT = re.compile(r"""^\s*import\s+['"]@adia-ai/web-components['"]\s*;?\s*$""")
58
+ PX_GE = re.compile(r"(?<![\w.-])(\d+)px\b") # integer px values; fractional (1.5px) deliberately not matched
59
+ FOUNDATION_OPT_IN = re.compile(r"adia-lint:\s*foundation", re.I)
60
+ # Genuine token/foundation SHEETS are exempt from color/px — matched by exact stem or a styles/ dir,
61
+ # NOT a path substring (so a `color-picker` / `theme-toggle` component is still linted).
62
+ FOUNDATION_STEMS = {"tokens", "token", "theme", "themes", "foundation", "foundations",
63
+ "palette", "palettes", "host", "reset", "resets", "scheme", "schemes",
64
+ "color", "colors"}
65
+
66
+
67
+ def _ext(path):
68
+ return os.path.splitext(path or "")[1].lower()
69
+
70
+
71
+ def _is_foundation_css(path, text):
72
+ """A genuine token/foundation sheet — exempt from color/px (component sheets are not)."""
73
+ segs = (path or "").replace("\\", "/").split("/")
74
+ if "styles" in segs or "tokens" in segs:
75
+ return True
76
+ stem = os.path.splitext(segs[-1])[0].lower() if segs else ""
77
+ if stem in FOUNDATION_STEMS:
78
+ return True
79
+ return bool(FOUNDATION_OPT_IN.search(text[:1000]))
80
+
81
+
82
+ def lint_text(text, path=""):
83
+ """Return a list of (NAME, line, snippet, why) advisory findings."""
84
+ ext = _ext(path)
85
+ findings = []
86
+ is_ssr = bool(SSR_SIGNAL.search(text)) or ext in (".vue", ".svelte", ".astro")
87
+ is_tokenish = _is_foundation_css(path, text)
88
+
89
+ for i, line in enumerate(text.splitlines(), 1):
90
+ s = line.strip()[:90]
91
+ if "attachShadow" in line:
92
+ findings.append(("SHADOW-DOM", i, s,
93
+ "adia-ui is light-DOM — never attachShadow; it breaks the token cascade + @scope"))
94
+ if "::slotted(" in line:
95
+ findings.append(("SLOTTED", i, s,
96
+ "light DOM has no ::slotted — style projected content via :scope > [slot=\"x\"]"))
97
+ if ext in STYLE_EXT:
98
+ if DEAD_FONT.search(line):
99
+ findings.append(("DEAD-FONT-TOKEN", i, s,
100
+ "--a-font is not a real token (resolves to UA serif) — floor to var(--a-font-family-ui)"))
101
+ if not is_tokenish:
102
+ for decl in line.split(";"): # per-declaration: a literal on a line that also has a var() still counts
103
+ d = decl.strip()
104
+ if not d or d.startswith(("//", "/*", "*")) or "var(" in decl or "light-dark(" in decl:
105
+ continue
106
+ if HEXCOLOR.search(decl) or FUNCCOLOR.search(decl):
107
+ findings.append(("RAW-COLOR", i, s,
108
+ "component CSS is token-only — replace the literal with var(--a-*) (foundation/token files excepted)"))
109
+ break
110
+ if not is_tokenish and not line.lstrip().startswith("@"): # skip @media/@container/@scope at-rules
111
+ for decl in line.split(";"):
112
+ if "/*" in decl: # author-annotated carve-out
113
+ continue
114
+ if any(int(v) >= 3 for v in PX_GE.findall(decl)):
115
+ findings.append(("RAW-PX", i, s,
116
+ "no raw px ≥ 3 in component CSS — use var(--a-space-*); 1–2px hairlines exempt, annotate a deliberate exception with a comment"))
117
+ break
118
+ if ext in CODE_EXT:
119
+ if BOOL_TRUE.search(line):
120
+ findings.append(("BOOL-DEFAULT-TRUE", i, s,
121
+ "a boolean prop defaulting true can't be turned off by absence — flip the name so absent = false"))
122
+ if ATTR_TYPO.search(line):
123
+ findings.append(("ATTR-TYPO", i, s,
124
+ "did you mean `attribute:`? `attr:` is silently ignored in a property definition"))
125
+ if is_ssr:
126
+ if "<router-ui" in line:
127
+ findings.append(("SSR-DOUBLE-ROUTER", i, s,
128
+ "in SSR the framework router owns routing — don't also mount <router-ui> (two route owners)"))
129
+ if TOPLEVEL_IMPORT.search(line):
130
+ findings.append(("SSR-TOPLEVEL-IMPORT", i, s,
131
+ "a top-level kit import throws `HTMLElement is not defined` on the server — defer it into a client hook"))
132
+ if ext in MARKUP_EXT and OVERLAY_OPEN.search(line):
133
+ findings.append(("HARDCODED-OPEN", i, s,
134
+ "drive overlays via the .open property, not a hardcoded `open` attribute (it bricks the page)"))
135
+ if (ext in MARKUP_EXT and "slot=" not in line and not s.startswith(("<!--", "//", "*", "/*"))
136
+ and NATIVE_PRIMITIVE.search(line)):
137
+ findings.append(("NATIVE-PRIMITIVE", i, s,
138
+ "use the *-ui primitive (button-ui / input-ui / select-ui / textarea-ui / modal-ui) — raw natives skip focus rings, theming, and form association; a deliberate slotted trigger (with slot=) is the exception"))
139
+ if LEGACY_SHELL.search(line):
140
+ findings.append(("LEGACY-SHELL", i, s,
141
+ "retired shell shape (ADR-0024, v0.4.0) — use the bespoke tag (chat-thread / chat-composer / chat-empty · admin-sidebar / admin-command · editor-canvas · pane-ui)"))
142
+
143
+ if ext in STYLE_EXT:
144
+ for m in SCOPE_EXTENT.finditer(text):
145
+ ln = text.count("\n", 0, m.start()) + 1
146
+ findings.append(("SCOPE-EXTENT", ln, ":scope { … width/height … }",
147
+ "the component is size-agnostic — let the consumer own width/height; don't set extent on :scope"))
148
+
149
+ findings.sort(key=lambda f: (f[1], f[0]))
150
+ return findings
151
+
152
+
153
+ def _render(path, findings):
154
+ out = [f"adia-lint: {len(findings)} structural smell(s) in {path or '<stdin>'}"]
155
+ for name, ln, snip, why in findings:
156
+ out.append(f" [{name}] line {ln}: {snip}")
157
+ out.append(f" → {why}")
158
+ return "\n".join(out)
159
+
160
+
161
+ def _lint_path(path):
162
+ try:
163
+ with open(path, encoding="utf-8", errors="replace") as f:
164
+ return lint_text(f.read(), path)
165
+ except OSError as e:
166
+ return [("READ-ERROR", 0, path, f"failed to read file: {e}")]
167
+
168
+
169
+ def _hook():
170
+ try:
171
+ event = json.load(sys.stdin)
172
+ except (json.JSONDecodeError, ValueError):
173
+ return 0
174
+ ti = event.get("tool_input", {}) or {}
175
+ path = ti.get("file_path", "") or ""
176
+ if _ext(path) not in LINT_EXT:
177
+ return 0 # only component/page source; stay quiet otherwise
178
+ text = ti.get("content")
179
+ if text is None:
180
+ if not os.path.isfile(path):
181
+ return 0
182
+ try:
183
+ with open(path, encoding="utf-8", errors="replace") as f:
184
+ text = f.read()
185
+ except OSError:
186
+ return 0
187
+ findings = lint_text(text, path)
188
+ if findings:
189
+ print(_render(path, findings))
190
+ print(" (advisory — adia-ui authoring smells; the skills + a2ui MCP own the judgment)")
191
+ return 0 # NEVER block
192
+
193
+
194
+ def _selftest():
195
+ """Built-in fixtures: each seeded smell must fire, clean files must stay quiet, --hook must exit 0."""
196
+ import io
197
+ cases = [
198
+ ("components/x/x.css",
199
+ "@scope (x) {\n"
200
+ " :scope { color:#f00; width:100%; font-family:var(--a-font); padding:24px; }\n"
201
+ " :scope > [slot=a]::slotted(p) { margin:0; }\n"
202
+ "}",
203
+ {"RAW-COLOR", "SCOPE-EXTENT", "DEAD-FONT-TOKEN", "SLOTTED", "RAW-PX"}),
204
+ ("styles/theme.css", ":root { --a-bg: light-dark(#fff, #000); }", set()),
205
+ ("components/color-picker/color-picker.css",
206
+ "@scope (color-picker) {\n :scope { background:#abc; padding:40px; }\n}",
207
+ {"RAW-COLOR", "RAW-PX"}),
208
+ ("providers/p.tsx",
209
+ "'use client';\nimport '@adia-ai/web-components';\n"
210
+ "export default () => (<admin-shell><router-ui/><modal-ui open>x</modal-ui></admin-shell>);",
211
+ {"SSR-TOPLEVEL-IMPORT", "SSR-DOUBLE-ROUTER", "HARDCODED-OPEN"}),
212
+ ("core/clean.js",
213
+ "import { UIElement } from '@adia-ai/web-components/core/element.js';\n"
214
+ "class UIX extends UIElement { connected() { this.innerHTML = '<col-ui></col-ui>'; } }",
215
+ set()),
216
+ ("markup/natives.html",
217
+ "<col-ui>\n <button>Save</button>\n <input type=\"text\">\n <button-ui slot=\"trigger\">ok</button-ui>\n</col-ui>",
218
+ {"NATIVE-PRIMITIVE"}),
219
+ ("markup/legacy.html",
220
+ "<chat-shell>\n <section data-chat-messages></section>\n <chat-input-ui data-chat-input></chat-input-ui>\n</chat-shell>",
221
+ {"LEGACY-SHELL"}),
222
+ ]
223
+ ok = True
224
+ for name, text, expected in cases:
225
+ got = {f[0] for f in lint_text(text, name)}
226
+ if expected - got:
227
+ ok = False
228
+ print(f"selftest: {name} MISSING {sorted(expected - got)} (got {sorted(got)})", file=sys.stderr)
229
+ if not expected and got:
230
+ ok = False
231
+ print(f"selftest: {name} expected clean, got {sorted(got)}", file=sys.stderr)
232
+ saved_in, saved_out = sys.stdin, sys.stdout # never-block invariant: --hook exits 0 even on smelly input
233
+ try:
234
+ sys.stdin = io.StringIO(json.dumps({"tool_input": {"file_path": "x.css",
235
+ "content": "@scope (x) {\n :scope { color:#f00; }\n}"}}))
236
+ sys.stdout = io.StringIO()
237
+ rc = _hook()
238
+ finally:
239
+ sys.stdin, sys.stdout = saved_in, saved_out
240
+ if rc != 0:
241
+ ok = False
242
+ print("selftest: --hook did not exit 0 on smelly input", file=sys.stderr)
243
+ print("selftest: PASS" if ok else "selftest: FAIL")
244
+ return 0 if ok else 1
245
+
246
+
247
+ def main(argv):
248
+ if argv and argv[0] == "selftest":
249
+ return _selftest()
250
+ if "--hook" in argv:
251
+ return _hook()
252
+ args = [a for a in argv if a == "-" or not a.startswith("-")]
253
+ if not args:
254
+ print(__doc__.strip().split("\n\n", 1)[0], file=sys.stderr)
255
+ return 2
256
+ total = 0
257
+ for path in args:
258
+ if path == "-":
259
+ findings = lint_text(sys.stdin.read(), "<stdin>")
260
+ path = "<stdin>"
261
+ else:
262
+ findings = _lint_path(path)
263
+ if findings:
264
+ total += len(findings)
265
+ print(_render(path, findings))
266
+ return 1 if total else 0
267
+
268
+
269
+ if __name__ == "__main__":
270
+ sys.exit(main(sys.argv[1:]))