@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.
- package/.claude-plugin/plugin.json +12 -0
- package/.mcp.json +8 -0
- package/CHANGELOG.md +55 -0
- package/README.md +44 -0
- package/bin/adia-lint +270 -0
- package/bin/adia-scaffold +411 -0
- package/commands/adia-compose.md +10 -0
- package/commands/adia-genui.md +12 -0
- package/commands/adia-migrate.md +10 -0
- package/commands/adia-orient.md +14 -0
- package/commands/adia-scaffold.md +17 -0
- package/commands/adia-verify.md +10 -0
- package/commands/adia-wire.md +13 -0
- package/hooks/hooks.json +15 -0
- package/package.json +40 -0
- package/references/a2ui-mcp-tools.md +64 -0
- package/references/authoring-components.md +88 -0
- package/references/component-model.md +71 -0
- package/references/data-and-hydration.md +84 -0
- package/references/genui-a2ui.md +62 -0
- package/references/llm.md +79 -0
- package/references/migration.md +57 -0
- package/references/project-shapes.md +96 -0
- package/references/shell-admin.md +65 -0
- package/references/shell-chat.md +44 -0
- package/references/shell-editor.md +48 -0
- package/references/shell-embed.md +33 -0
- package/references/shell-simple.md +38 -0
- package/references/spa-architecture.md +116 -0
- package/references/ssr-integration.md +117 -0
- package/references/verification.md +39 -0
- package/skills/adia-ui-compose/SKILL.md +57 -0
- package/skills/adia-ui-data/SKILL.md +62 -0
- package/skills/adia-ui-factory/SKILL.md +113 -0
- package/skills/adia-ui-genui/SKILL.md +74 -0
- package/skills/adia-ui-llm/SKILL.md +51 -0
- package/skills/adia-ui-migrate/SKILL.md +64 -0
- package/skills/adia-ui-project/SKILL.md +77 -0
- package/skills/adia-ui-shells/SKILL.md +62 -0
- package/skills/adia-ui-spa/SKILL.md +52 -0
- package/skills/adia-ui-ssr/SKILL.md +52 -0
- 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
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:]))
|