@adia-ai/adia-ui-forge 0.1.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 +23 -0
- package/CHANGELOG.md +26 -0
- package/README.md +54 -0
- package/bin/forge-lint +263 -0
- package/bin/lib/audit-axes.mjs +555 -0
- package/bin/lib/dry-run-irreversible.mjs +236 -0
- package/bin/lib/run-skill-evals.mjs +487 -0
- package/bin/lib/teach-router.mjs +250 -0
- package/commands/adia-forge-a2ui.md +10 -0
- package/commands/adia-forge-author.md +10 -0
- package/commands/adia-forge-dogfood.md +8 -0
- package/commands/adia-forge-llm.md +8 -0
- package/commands/adia-forge-orient.md +10 -0
- package/commands/adia-forge-release.md +8 -0
- package/commands/adia-forge-review.md +10 -0
- package/hooks/hooks.json +15 -0
- package/package.json +41 -0
- package/references/shared/content-trust.md +76 -0
- package/references/shared/pev-rationale.md +64 -0
- package/references/shared/skill-conventions.md +133 -0
- package/skills/adia-ui-a2ui/CHANGELOG.md +32 -0
- package/skills/adia-ui-a2ui/SKILL.md +243 -0
- package/skills/adia-ui-a2ui/evals/adversarial-corpus.json +75 -0
- package/skills/adia-ui-a2ui/evals/routing-corpus.json +31 -0
- package/skills/adia-ui-a2ui/evals/teach-routing-cases.json +100 -0
- package/skills/adia-ui-a2ui/references/anti-patterns.md +24 -0
- package/skills/adia-ui-a2ui/references/chunk-authoring.md +88 -0
- package/skills/adia-ui-a2ui/references/corpus-discipline.md +56 -0
- package/skills/adia-ui-a2ui/references/eval-diagnostics.md +127 -0
- package/skills/adia-ui-a2ui/references/fragment-graph.md +91 -0
- package/skills/adia-ui-a2ui/references/mcp-pipeline-ops.md +106 -0
- package/skills/adia-ui-a2ui/references/mcp-tool-reference.md +398 -0
- package/skills/adia-ui-a2ui/references/pipeline-overview.md +175 -0
- package/skills/adia-ui-a2ui/references/semantic-fail-lifting.md +120 -0
- package/skills/adia-ui-a2ui/references/strategy-engines.md +111 -0
- package/skills/adia-ui-a2ui/references/teach-protocol.md +220 -0
- package/skills/adia-ui-a2ui/references/zettel-calibration.md +93 -0
- package/skills/adia-ui-a2ui/scripts/audit-a2ui-roster.mjs +96 -0
- package/skills/adia-ui-a2ui/scripts/teach-route.mjs +157 -0
- package/skills/adia-ui-a2ui/skill.json +38 -0
- package/skills/adia-ui-authoring/CHANGELOG.md +32 -0
- package/skills/adia-ui-authoring/SKILL.md +256 -0
- package/skills/adia-ui-authoring/assets/case-studies/admin-shell-decomposition.md +101 -0
- package/skills/adia-ui-authoring/assets/case-studies/maxtokens-32768-discovery.md +109 -0
- package/skills/adia-ui-authoring/assets/case-studies/theme-panel-promotion.md +113 -0
- package/skills/adia-ui-authoring/evals/adversarial-design-plan-gates.json +138 -0
- package/skills/adia-ui-authoring/evals/routing-corpus.json +245 -0
- package/skills/adia-ui-authoring/references/anti-patterns.md +606 -0
- package/skills/adia-ui-authoring/references/api-contract.md +205 -0
- package/skills/adia-ui-authoring/references/authoring-cycle.md +211 -0
- package/skills/adia-ui-authoring/references/canonical-pattern-index.md +179 -0
- package/skills/adia-ui-authoring/references/code-style.md +329 -0
- package/skills/adia-ui-authoring/references/common-gotchas.md +93 -0
- package/skills/adia-ui-authoring/references/composite-demo-protocol.md +1084 -0
- package/skills/adia-ui-authoring/references/css-patterns.md +364 -0
- package/skills/adia-ui-authoring/references/lifecycle-patterns.md +302 -0
- package/skills/adia-ui-authoring/references/llm-bridge.md +254 -0
- package/skills/adia-ui-authoring/references/module-promotion.md +289 -0
- package/skills/adia-ui-authoring/references/primitive-audit.md +123 -0
- package/skills/adia-ui-authoring/references/shell-patterns.md +561 -0
- package/skills/adia-ui-authoring/references/teach-protocol.md +428 -0
- package/skills/adia-ui-authoring/references/token-contract.md +120 -0
- package/skills/adia-ui-authoring/references/worked-example.md +351 -0
- package/skills/adia-ui-authoring/references/yaml-contract.md +224 -0
- package/skills/adia-ui-authoring/scripts/audit-authoring-roster.mjs +148 -0
- package/skills/adia-ui-authoring/scripts/build-canonical-pattern-index.mjs +199 -0
- package/skills/adia-ui-authoring/skill.json +45 -0
- package/skills/adia-ui-dogfood/CHANGELOG.md +17 -0
- package/skills/adia-ui-dogfood/README.md +62 -0
- package/skills/adia-ui-dogfood/SKILL.md +866 -0
- package/skills/adia-ui-dogfood/scripts/analyze.mjs +603 -0
- package/skills/adia-ui-dogfood/skill.json +40 -0
- package/skills/adia-ui-forge/SKILL.md +88 -0
- package/skills/adia-ui-forge/evals/routing-corpus.json +30 -0
- package/skills/adia-ui-gen-review/CHANGELOG.md +108 -0
- package/skills/adia-ui-gen-review/SKILL.md +266 -0
- package/skills/adia-ui-gen-review/references/loop-protocol.md +712 -0
- package/skills/adia-ui-gen-review/references/rubric-cosmetic.md +144 -0
- package/skills/adia-ui-gen-review/references/rubric-decompose.md +117 -0
- package/skills/adia-ui-gen-review/references/rubric-score.md +249 -0
- package/skills/adia-ui-gen-review/references/scores.schema.json +125 -0
- package/skills/adia-ui-gen-review/references/teach-protocol.md +214 -0
- package/skills/adia-ui-gen-review/scripts/gen-review-coverage-audit.mjs +127 -0
- package/skills/adia-ui-gen-review/scripts/gen-review-decompose.mjs +569 -0
- package/skills/adia-ui-gen-review/scripts/gen-review-status.mjs +176 -0
- package/skills/adia-ui-gen-review/scripts/validate-cycle-scores.mjs +198 -0
- package/skills/adia-ui-gen-review/skill.json +22 -0
- package/skills/adia-ui-llm/CHANGELOG.md +25 -0
- package/skills/adia-ui-llm/SKILL.md +165 -0
- package/skills/adia-ui-llm/evals/adversarial-corpus.json +75 -0
- package/skills/adia-ui-llm/evals/routing-corpus.json +30 -0
- package/skills/adia-ui-llm/evals/teach-routing-cases.json +73 -0
- package/skills/adia-ui-llm/references/adapter-contract.md +99 -0
- package/skills/adia-ui-llm/references/add-a-provider.md +90 -0
- package/skills/adia-ui-llm/references/bridge-facade.md +94 -0
- package/skills/adia-ui-llm/references/browser-proxy-boundary.md +89 -0
- package/skills/adia-ui-llm/references/model-registry.md +75 -0
- package/skills/adia-ui-llm/references/streaming-sse.md +95 -0
- package/skills/adia-ui-llm/references/teach-protocol.md +78 -0
- package/skills/adia-ui-llm/scripts/audit-llm-roster.mjs +93 -0
- package/skills/adia-ui-llm/scripts/teach-route.mjs +119 -0
- package/skills/adia-ui-llm/skill.json +33 -0
- package/skills/adia-ui-release/CHANGELOG.md +23 -0
- package/skills/adia-ui-release/SKILL.md +295 -0
- package/skills/adia-ui-release/assets/case-studies/2026-05-20-batch-push-v0.6.14-v0.6.15.md +144 -0
- package/skills/adia-ui-release/assets/case-studies/2026-05-20-corpus-drift-remediation-v0.6.15.md +155 -0
- package/skills/adia-ui-release/assets/case-studies/2026-05-20-version-skip-correction-v0.6.12.md +114 -0
- package/skills/adia-ui-release/assets/case-studies/2026-05-21-author-from-scratch-v0.6.18.md +139 -0
- package/skills/adia-ui-release/assets/case-studies/2026-05-21-feedback37-retraction-v0.6.21.md +124 -0
- package/skills/adia-ui-release/assets/case-studies/2026-05-21-fn1-enrichment-pass-v0.6.19.md +125 -0
- package/skills/adia-ui-release/assets/case-studies/2026-05-21-stale-test-detection-v0.6.20.md +142 -0
- package/skills/adia-ui-release/assets/case-studies/2026-05-23-freshness-gate-recovery-v0.6.32.md +97 -0
- package/skills/adia-ui-release/assets/case-studies/2026-05-26-catalog-drift-recurring-v0.6.40.md +147 -0
- package/skills/adia-ui-release/assets/templates/stub-changelog.template.md +22 -0
- package/skills/adia-ui-release/evals/evals.json +164 -0
- package/skills/adia-ui-release/references/changelog-discipline.md +250 -0
- package/skills/adia-ui-release/references/cycle-happy-path.md +520 -0
- package/skills/adia-ui-release/references/exe-deploy.md +149 -0
- package/skills/adia-ui-release/references/gates-catalog.md +778 -0
- package/skills/adia-ui-release/references/ledger-discipline.md +232 -0
- package/skills/adia-ui-release/references/migration-guide-authoring.md +174 -0
- package/skills/adia-ui-release/references/multi-agent-baseline.md +207 -0
- package/skills/adia-ui-release/references/notes-authoring.md +212 -0
- package/skills/adia-ui-release/references/recovery-paths.md +215 -0
- package/skills/adia-ui-release/references/rollup-notes.md +208 -0
- package/skills/adia-ui-release/references/teach-protocol.md +468 -0
- package/skills/adia-ui-release/scripts/assert-monorepo-root.mjs +49 -0
- package/skills/adia-ui-release/scripts/audit-gate-roster.mjs +196 -0
- package/skills/adia-ui-release/scripts/bump.mjs +118 -0
- package/skills/adia-ui-release/scripts/dispatch-publish.mjs +151 -0
- package/skills/adia-ui-release/scripts/insert-stub.mjs +127 -0
- package/skills/adia-ui-release/scripts/make-ledger.mjs +179 -0
- package/skills/adia-ui-release/scripts/promote-unreleased.mjs +88 -0
- package/skills/adia-ui-release/scripts/release-pack.mjs +350 -0
- package/skills/adia-ui-release/scripts/tag-lockstep.mjs +110 -0
- package/skills/adia-ui-release/skill.json +75 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "adia-ui-forge",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Author and maintain the adia-ui (@adia-ai) framework itself — primitives (web-components), composite shells (web-modules), the A2UI / gen-ui generation engine and its training corpus, the @adia-ai/llm client, quality sweeps, and release engineering across all @adia-ai packages. The maintainer counterpart to adia-ui-factory (the consumer/app-author plugin).",
|
|
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": [
|
|
12
|
+
"adia-ui",
|
|
13
|
+
"a2ui",
|
|
14
|
+
"gen-ui",
|
|
15
|
+
"web-components",
|
|
16
|
+
"web-modules",
|
|
17
|
+
"llm",
|
|
18
|
+
"framework-authoring",
|
|
19
|
+
"component-library",
|
|
20
|
+
"release-engineering",
|
|
21
|
+
"maintainer"
|
|
22
|
+
]
|
|
23
|
+
}
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to **adia-ui-forge** are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/); versioning is [SemVer](https://semver.org/).
|
|
4
|
+
|
|
5
|
+
## [0.1.2] — 2026-06-04
|
|
6
|
+
|
|
7
|
+
- **Aspiration is now a precondition of making — a soft gate.** Before a maker skill converges, the **design principles** (the framework philosophy the change is reasoned toward — light-DOM composability, token-driven styling, contract-first authoring, no lifecycle leaks) must be at least lightly named; authoring reasoned toward _nothing_ drifts to the average primitive. `adia-ui-authoring` gains a **§DesignPrinciplesSoftGate** section before its cold-start menu; `adia-ui-a2ui` and `adia-ui-llm` get a brief domain-fitted equivalent at their cold-start (the generation philosophy; the client-contract philosophy); `/adia-forge-author` gains a one-line **"name the design principles before you converge"** gate. Because this plugin has no standalone design-principles document — only per-artifact guardrails — the gate also nudges the maintainer to **name the principles themselves, even provisionally** (they were previously implicit). It is a _soft_ blocker throughout — cleared by **naming** a provisional, revisable direction, never by stopping. Mirrors brand-forge v0.4.5 and the generalized rule in plugins-factory `operational-roles.md`.
|
|
8
|
+
|
|
9
|
+
## [0.1.1] — 2026-06-04
|
|
10
|
+
|
|
11
|
+
- **Quoted `argument-hint` frontmatter** across all commands — normalizes the value to a string (YAML was parsing the unquoted `[..]` as a flow list) and satisfies plugins-factory's new frontmatter flow-collection lint. No behavior change.
|
|
12
|
+
|
|
13
|
+
## [0.1.0] — 2026-06-03
|
|
14
|
+
|
|
15
|
+
Initial release — the maintainer-side counterpart to `adia-ui-factory`, carved and de-repo'd from the `@adia-ai` monorepo's `.agents/` system (scope A: the framework's own authoring toolkit, assuming `@adia-ai` monorepo conventions).
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- **7 skills** — `adia-ui-forge` (orchestrator: classify subsystem → route) + `adia-ui-authoring` (web-components / web-modules), `adia-ui-a2ui` (the A2UI / gen-ui engine), `adia-ui-llm` (the `@adia-ai/llm` client), `adia-ui-gen-review` (generated-UI quality scoring), `adia-ui-dogfood` (QA sweep), `adia-ui-release` (release engineering + migration-guide authoring).
|
|
20
|
+
- **7 commands** — `/adia-forge-orient·author·a2ui·llm·review·dogfood·release`.
|
|
21
|
+
- **Advisory authoring-lint hook** — `bin/forge-lint` (11 component-authoring smells; `PostToolUse` on `Write|Edit`, never blocks).
|
|
22
|
+
- **Co-located shared infra** — `references/shared/` (content-trust, plan-execute-verify, skill-conventions) + `bin/lib/` (audit-axes, teach-router, dry-run-irreversible, run-skill-evals). Self-contained, zero cross-plugin paths.
|
|
23
|
+
|
|
24
|
+
### Reviewed
|
|
25
|
+
|
|
26
|
+
- Red-teamed with the `plugins-factory` 9-critic council (CONDITIONAL → folded): reconciled the build-state docs, cut the always-on context tax (removed a redundant `trigger:` block, trimmed the longest skill descriptions), disclosed and guarded the release scripts' publish / network reach, synced the four description surfaces, and labeled the `forge-lint` ↔ `adia-lint` shared-core duplication. Full record under `reviews/`.
|
package/README.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# adia-ui-forge
|
|
2
|
+
|
|
3
|
+
**Author and maintain the adia-ui (`@adia-ai`) framework itself.** This is the producer/maintainer counterpart to `adia-ui-factory` — where the factory builds _apps on_ the framework, the forge builds and ships _the framework_: its primitives, composite shells, the A2UI / gen-ui generation engine and training corpus, the LLM client, quality sweeps, and releases — across every `@adia-ai` package.
|
|
4
|
+
|
|
5
|
+
> **Status: 0.1.2 — built and red-teamed.** All 7 skills, 7 commands, the advisory authoring-lint hook, and shared infra are in place and pass the harness gates (`validate_plugin.py --strict`, `reference-lint.py`, `forge-lint selftest`, markdownlint). The build record is in [ROADMAP.md](ROADMAP.md); the `plugins-factory` council critique is recorded under `reviews/`.
|
|
6
|
+
|
|
7
|
+
## Who it's for
|
|
8
|
+
|
|
9
|
+
Anyone working **on** the adia-ui framework — in the `@adia-ai` monorepo or a fork. It assumes the monorepo's package conventions (`packages/web-components`, `packages/web-modules`, `packages/a2ui`, `packages/llm`); it is not a generic "build any component library" toolkit. App authors who _consume_ the framework want `adia-ui-factory` instead.
|
|
10
|
+
|
|
11
|
+
## Requirements & what it ships
|
|
12
|
+
|
|
13
|
+
- **Two runtimes.** The hook + `forge-lint` are Python (3.8+); every skill script and `bin/lib/*` are Node ESM. Both must be on PATH — the skills' `§SelfAudit` and eval scripts need Node; the hook needs Python.
|
|
14
|
+
- **The release skill bundles real capability.** `adia-ui-release` ships scripts that shell `git`, `npm publish`, `gh`, and `curl` (the deploy verify). They are operator-run (never hook-wired), go through a dry-run gate, and **fail loudly unless run inside an `@adia-ai`-style monorepo checkout** (a `packages/web-components` guard). The deploy host and npm scope are overridable (`--host`/`$ADIA_DEPLOY_HOST`, `--scope`/`$ADIA_NPM_SCOPE`).
|
|
15
|
+
- **Skill versions are lineage, not the plugin version.** Per-skill `version` fields (e.g. `adia-ui-authoring` 1.9.2) are carried from the monorepo source skills; the plugin's own version is canonical for the bundle.
|
|
16
|
+
- **Gates run via plugins-factory's harness.** `validate_plugin.py` / `reference-lint.py` live in `plugins-factory` (`/plugin install plugins-factory@plugins-forge` to run them; wire them into `adia-plugins` CI); `forge-lint selftest` is self-contained in the bundle.
|
|
17
|
+
|
|
18
|
+
## The package surface it covers
|
|
19
|
+
|
|
20
|
+
| Package / system | What the forge does with it |
|
|
21
|
+
| --- | --- |
|
|
22
|
+
| `@adia-ai/web-components` | author & evolve light-DOM primitives (props, `@scope` tokens, lifecycle, traits) |
|
|
23
|
+
| `@adia-ai/web-modules` | author composite shells & clusters (admin / chat / editor / embed) |
|
|
24
|
+
| `@adia-ai/a2ui` (6-pkg cluster: compose · corpus · retrieval · runtime · validator · mcp) | evolve the A2UI / gen-ui generation engine, chunk corpus, and MCP server |
|
|
25
|
+
| gen-ui training data (`gen-ui-kit/data`) | author and curate the generation corpus / seed data |
|
|
26
|
+
| `@adia-ai/llm` | maintain the provider-agnostic client (anthropic / openai / gemini adapters, SSE, models) |
|
|
27
|
+
| all packages | gen-ui output quality review, cross-surface dogfood QA, and lockstep release engineering |
|
|
28
|
+
|
|
29
|
+
## Skills (7)
|
|
30
|
+
|
|
31
|
+
| Skill | Job |
|
|
32
|
+
| --- | --- |
|
|
33
|
+
| `adia-ui-forge` | cold-start orchestrator — classify the package/concern and route to the owning skill |
|
|
34
|
+
| `adia-ui-authoring` | author primitives (`web-components`) + composite shells (`web-modules`), tokens, traits |
|
|
35
|
+
| `adia-ui-a2ui` | the A2UI / gen-ui generation engine — compose strategies, chunk corpus, retrieval, validator, runtime, MCP server |
|
|
36
|
+
| `adia-ui-llm` | the `@adia-ai/llm` client — provider adapters, SSE streaming, model registry, the bridge |
|
|
37
|
+
| `adia-ui-gen-review` | closed-loop quality scoring of generated gen-ui output, driving corpus fixes |
|
|
38
|
+
| `adia-ui-dogfood` | static + visual QA sweep across components, apps, playgrounds, and catalog |
|
|
39
|
+
| `adia-ui-release` | release-engineering discipline (gates, changelog/notes authoring) + migration-guide authoring |
|
|
40
|
+
|
|
41
|
+
## Relationship to adia-ui-factory
|
|
42
|
+
|
|
43
|
+
Producer ↔ consumer, fully independent (zero cross-plugin dependencies):
|
|
44
|
+
|
|
45
|
+
| | `adia-ui-forge` (maintainer) | `adia-ui-factory` (consumer) |
|
|
46
|
+
| --- | --- | --- |
|
|
47
|
+
| **authoring** | builds the primitives & shells | composes screens from them |
|
|
48
|
+
| **a2ui / gen-ui** | builds the engine + corpus | renders generated UI via the published MCP |
|
|
49
|
+
| **llm** | builds the `@adia-ai/llm` package | wires it into an app |
|
|
50
|
+
| **migration** | authors the MIGRATION GUIDE | applies it to consumer code |
|
|
51
|
+
|
|
52
|
+
## Provenance
|
|
53
|
+
|
|
54
|
+
Carved from the `@adia-ai` monorepo's `.agents/` maintainer system, then authored and red-teamed with [`plugins-factory`](https://github.com/kimgranlund/plugins-forge/tree/main/plugins-factory) against its 9-dimension architecture standard.
|
package/bin/forge-lint
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""forge-lint — advisory authoring-smell checker for the adia-ui FRAMEWORK source (maintainer slice).
|
|
3
|
+
|
|
4
|
+
This is the producer-side counterpart to the consumer plugin's app-lint: it checks the framework's
|
|
5
|
+
OWN component source as a maintainer authors it (primitives in packages/web-components, composites in
|
|
6
|
+
packages/web-modules) for the framework's structural invariants — light-DOM only (no attachShadow,
|
|
7
|
+
no ::slotted), component CSS is `@scope`-wrapped and token-only (no raw colors, no raw px >= 3, no
|
|
8
|
+
extent on :scope, no dead `--a-font`), property definitions are well-formed (no `default: true`, no
|
|
9
|
+
`attr:` typo), composites use `*-ui` primitives (no native-primitive leak), and no retired shell
|
|
10
|
+
shapes (ADR-0024 data-attribute forms). Foundation/token sheets (a styles/ or tokens/ dir, a
|
|
11
|
+
tokens/theme/host/palette/… stem, or `/* forge-lint: foundation */`) are exempt from the CSS-literal
|
|
12
|
+
and scope checks.
|
|
13
|
+
|
|
14
|
+
It deliberately does NOT carry the consumer/app traps (SSR double-router, top-level kit import,
|
|
15
|
+
hardcoded overlay open) — those are about CONSUMING the framework in an app and live in the
|
|
16
|
+
consumer plugin. forge-lint judges only authoring structure, never whether a component is good,
|
|
17
|
+
accessible, or on-spec — that lives in the skills (adia-ui-authoring / -a2ui / -gen-review / -dogfood).
|
|
18
|
+
A clean forge-lint says "no structural tells," never "this is right."
|
|
19
|
+
|
|
20
|
+
Shared core: the regex bank below (RAW-COLOR/PX, SCOPE-EXTENT, NATIVE-PRIMITIVE, LEGACY-SHELL,
|
|
21
|
+
the _is_foundation_css exemption) is mirrored in the sibling CONSUMER plugin's
|
|
22
|
+
adia-ui-factory/bin/adia-lint. They are deliberate VENDORED copies — the catalog forbids
|
|
23
|
+
cross-plugin imports (each plugin installs copy-alone) — so any change to a shared rule must be
|
|
24
|
+
reconciled in BOTH files. forge-lint drops the consumer/app traps (SSR double-router, top-level
|
|
25
|
+
import, hardcoded overlay open) and adds MISSING-SCOPE; that divergence is the point.
|
|
26
|
+
|
|
27
|
+
Usage:
|
|
28
|
+
forge-lint <file>... # lint files; exit 1 if any smell found, else 0
|
|
29
|
+
forge-lint - # lint stdin as a generic source file
|
|
30
|
+
forge-lint --hook # PostToolUse hook mode: read event JSON on stdin, lint the written
|
|
31
|
+
# source file, print advisory findings, ALWAYS exit 0 (never blocks)
|
|
32
|
+
forge-lint selftest # run built-in fixtures (seeded smells + clean files + the hook exit-0 invariant)
|
|
33
|
+
Stdlib only (Python 3.8+).
|
|
34
|
+
"""
|
|
35
|
+
import json
|
|
36
|
+
import os
|
|
37
|
+
import re
|
|
38
|
+
import sys
|
|
39
|
+
|
|
40
|
+
CODE_EXT = (".js", ".mjs", ".cjs", ".ts", ".tsx", ".jsx")
|
|
41
|
+
STYLE_EXT = (".css",)
|
|
42
|
+
MARKUP_EXT = (".html", ".htm", ".vue", ".svelte", ".astro", ".tsx", ".jsx")
|
|
43
|
+
LINT_EXT = tuple(sorted(set(CODE_EXT + STYLE_EXT + MARKUP_EXT)))
|
|
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
|
+
NATIVE_PRIMITIVE = re.compile(r"<(?:button|input|select|textarea|dialog)(?![\w-])") # raw native, not a *-ui
|
|
53
|
+
LEGACY_SHELL = re.compile(
|
|
54
|
+
r"data-chat-(?:messages|input|empty|name)|data-editor-body|data-canvas\b|data-sidebar="
|
|
55
|
+
r"|data-pane-(?:side|grow)|<aside-ui\b|<dialog\s+data-command") # retired shell shapes (ADR-0024)
|
|
56
|
+
PX_GE = re.compile(r"(?<![\w.-])(\d+)px\b") # integer px; fractional (1.5px) deliberately not matched
|
|
57
|
+
SELECTOR_RULE = re.compile(r"(?m)^\s*[.#:\[&\w][^{}\n]*\{") # a selector opening a rule block
|
|
58
|
+
FOUNDATION_OPT_IN = re.compile(r"forge-lint:\s*foundation", re.I)
|
|
59
|
+
# Genuine token/foundation SHEETS are exempt from the CSS-literal + scope checks — matched by exact
|
|
60
|
+
# stem or a styles/tokens dir, NOT a path substring (so a `color-picker` component is still linted).
|
|
61
|
+
FOUNDATION_STEMS = {"tokens", "token", "theme", "themes", "foundation", "foundations",
|
|
62
|
+
"palette", "palettes", "host", "reset", "resets", "scheme", "schemes",
|
|
63
|
+
"color", "colors"}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _ext(path):
|
|
67
|
+
return os.path.splitext(path or "")[1].lower()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _is_foundation_css(path, text):
|
|
71
|
+
"""A genuine token/foundation sheet — exempt from the CSS-literal + scope checks."""
|
|
72
|
+
segs = (path or "").replace("\\", "/").split("/")
|
|
73
|
+
if "styles" in segs or "tokens" in segs:
|
|
74
|
+
return True
|
|
75
|
+
stem = os.path.splitext(segs[-1])[0].lower() if segs else ""
|
|
76
|
+
if stem in FOUNDATION_STEMS:
|
|
77
|
+
return True
|
|
78
|
+
return bool(FOUNDATION_OPT_IN.search(text[:1000]))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def lint_text(text, path=""):
|
|
82
|
+
"""Return a list of (NAME, line, snippet, why) advisory findings."""
|
|
83
|
+
ext = _ext(path)
|
|
84
|
+
findings = []
|
|
85
|
+
is_tokenish = _is_foundation_css(path, text)
|
|
86
|
+
|
|
87
|
+
for i, line in enumerate(text.splitlines(), 1):
|
|
88
|
+
s = line.strip()[:90]
|
|
89
|
+
if "attachShadow" in line:
|
|
90
|
+
findings.append(("SHADOW-DOM", i, s,
|
|
91
|
+
"adia-ui is light-DOM — never attachShadow; it breaks the token cascade + @scope"))
|
|
92
|
+
if "::slotted(" in line:
|
|
93
|
+
findings.append(("SLOTTED", i, s,
|
|
94
|
+
"light DOM has no ::slotted — style projected content via :scope > [slot=\"x\"]"))
|
|
95
|
+
if ext in STYLE_EXT:
|
|
96
|
+
if DEAD_FONT.search(line):
|
|
97
|
+
findings.append(("DEAD-FONT-TOKEN", i, s,
|
|
98
|
+
"--a-font is not a real token (resolves to UA serif) — use var(--a-font-family-ui)"))
|
|
99
|
+
if not is_tokenish:
|
|
100
|
+
for decl in line.split(";"): # per-declaration: a literal on a line that also has a var() still counts
|
|
101
|
+
d = decl.strip()
|
|
102
|
+
if not d or d.startswith(("//", "/*", "*")) or "var(" in decl or "light-dark(" in decl:
|
|
103
|
+
continue
|
|
104
|
+
if HEXCOLOR.search(decl) or FUNCCOLOR.search(decl):
|
|
105
|
+
findings.append(("RAW-COLOR", i, s,
|
|
106
|
+
"component CSS is token-only — replace the literal with var(--a-*) (foundation/token files excepted)"))
|
|
107
|
+
break
|
|
108
|
+
if not is_tokenish and not line.lstrip().startswith("@"): # skip @media/@container/@scope at-rules
|
|
109
|
+
for decl in line.split(";"):
|
|
110
|
+
if "/*" in decl: # author-annotated carve-out
|
|
111
|
+
continue
|
|
112
|
+
if any(int(v) >= 3 for v in PX_GE.findall(decl)):
|
|
113
|
+
findings.append(("RAW-PX", i, s,
|
|
114
|
+
"no raw px >= 3 in component CSS — use var(--a-space-*); 1-2px hairlines exempt, annotate a deliberate exception with a comment"))
|
|
115
|
+
break
|
|
116
|
+
if ext in CODE_EXT:
|
|
117
|
+
if BOOL_TRUE.search(line):
|
|
118
|
+
findings.append(("BOOL-DEFAULT-TRUE", i, s,
|
|
119
|
+
"a boolean prop defaulting true can't be turned off by absence — flip the name so absent = false"))
|
|
120
|
+
if ATTR_TYPO.search(line):
|
|
121
|
+
findings.append(("ATTR-TYPO", i, s,
|
|
122
|
+
"did you mean `attribute:`? `attr:` is silently ignored in a property definition"))
|
|
123
|
+
if (ext in MARKUP_EXT and "slot=" not in line and not s.startswith(("<!--", "//", "*", "/*"))
|
|
124
|
+
and NATIVE_PRIMITIVE.search(line)):
|
|
125
|
+
findings.append(("NATIVE-PRIMITIVE", i, s,
|
|
126
|
+
"a composite must build from *-ui primitives (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"))
|
|
127
|
+
if LEGACY_SHELL.search(line):
|
|
128
|
+
findings.append(("LEGACY-SHELL", i, s,
|
|
129
|
+
"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)"))
|
|
130
|
+
|
|
131
|
+
if ext in STYLE_EXT:
|
|
132
|
+
for m in SCOPE_EXTENT.finditer(text):
|
|
133
|
+
ln = text.count("\n", 0, m.start()) + 1
|
|
134
|
+
findings.append(("SCOPE-EXTENT", ln, ":scope { … width/height … }",
|
|
135
|
+
"a primitive is size-agnostic — let the consumer own width/height; don't set extent on :scope"))
|
|
136
|
+
# MISSING-SCOPE (maintainer-authoring): a component sheet with rules but no @scope wrapper.
|
|
137
|
+
if not is_tokenish and "@scope" not in text and SELECTOR_RULE.search(text):
|
|
138
|
+
findings.append(("MISSING-SCOPE", 1, "(file has CSS rules but no @scope block)",
|
|
139
|
+
"component CSS must be wrapped in `@scope (<tag>) { … }` so styles don't leak in light DOM (foundation/token sheets excepted; opt out with /* forge-lint: foundation */)"))
|
|
140
|
+
|
|
141
|
+
findings.sort(key=lambda f: (f[1], f[0]))
|
|
142
|
+
return findings
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _render(path, findings):
|
|
146
|
+
out = [f"forge-lint: {len(findings)} structural smell(s) in {path or '<stdin>'}"]
|
|
147
|
+
for name, ln, snip, why in findings:
|
|
148
|
+
out.append(f" [{name}] line {ln}: {snip}")
|
|
149
|
+
out.append(f" → {why}")
|
|
150
|
+
return "\n".join(out)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _lint_path(path):
|
|
154
|
+
try:
|
|
155
|
+
with open(path, encoding="utf-8", errors="replace") as f:
|
|
156
|
+
return lint_text(f.read(), path)
|
|
157
|
+
except OSError as err:
|
|
158
|
+
msg = getattr(err, "strerror", None) or str(err)
|
|
159
|
+
return [("READ-ERROR", 1, path[:90], f"failed to read file: {msg}")]
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _hook():
|
|
163
|
+
try:
|
|
164
|
+
event = json.load(sys.stdin)
|
|
165
|
+
except (json.JSONDecodeError, ValueError):
|
|
166
|
+
return 0
|
|
167
|
+
ti = event.get("tool_input", {}) or {}
|
|
168
|
+
path = ti.get("file_path", "") or ""
|
|
169
|
+
if _ext(path) not in LINT_EXT:
|
|
170
|
+
return 0 # only component source; stay quiet otherwise
|
|
171
|
+
text = ti.get("content")
|
|
172
|
+
if text is None:
|
|
173
|
+
if not os.path.isfile(path):
|
|
174
|
+
return 0
|
|
175
|
+
try:
|
|
176
|
+
with open(path, encoding="utf-8", errors="replace") as f:
|
|
177
|
+
text = f.read()
|
|
178
|
+
except OSError:
|
|
179
|
+
return 0
|
|
180
|
+
findings = lint_text(text, path)
|
|
181
|
+
if findings:
|
|
182
|
+
print(_render(path, findings))
|
|
183
|
+
print(" (advisory — adia-ui framework authoring smells; the skills own the judgment)")
|
|
184
|
+
return 0 # NEVER block
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _selftest():
|
|
188
|
+
"""Built-in fixtures: each seeded smell must fire, clean files must stay quiet, --hook must exit 0."""
|
|
189
|
+
import io
|
|
190
|
+
cases = [
|
|
191
|
+
("packages/web-components/components/x/x.css",
|
|
192
|
+
"@scope (x) {\n"
|
|
193
|
+
" :scope { color:#f00; width:100%; font-family:var(--a-font); padding:24px; }\n"
|
|
194
|
+
" :scope > [slot=a]::slotted(p) { margin:0; }\n"
|
|
195
|
+
"}",
|
|
196
|
+
{"RAW-COLOR", "SCOPE-EXTENT", "DEAD-FONT-TOKEN", "SLOTTED", "RAW-PX"}),
|
|
197
|
+
("packages/web-components/styles/theme.css", ":root { --a-bg: light-dark(#fff, #000); }", set()),
|
|
198
|
+
("packages/web-components/components/y/y.css",
|
|
199
|
+
".y-thing { margin: 0; }\n.y-thing__row { gap: 4px; }",
|
|
200
|
+
{"MISSING-SCOPE"}),
|
|
201
|
+
("packages/web-components/components/z/z.js",
|
|
202
|
+
"class Z extends UIElement {\n static props = { open: { default: true }, label: { attr: 'label' } };\n"
|
|
203
|
+
" connected() { this.attachShadow({ mode: 'open' }); }\n}",
|
|
204
|
+
{"SHADOW-DOM", "BOOL-DEFAULT-TRUE", "ATTR-TYPO"}),
|
|
205
|
+
("packages/web-components/components/clean/clean.js",
|
|
206
|
+
"import { UIElement } from '../../core/element.js';\n"
|
|
207
|
+
"class Clean extends UIElement { connected() { this.innerHTML = '<col-ui></col-ui>'; } }",
|
|
208
|
+
set()),
|
|
209
|
+
("packages/web-modules/shell/markup.html",
|
|
210
|
+
"<admin-shell>\n <button>Save</button>\n <input type=\"text\">\n <button-ui slot=\"trigger\">ok</button-ui>\n</admin-shell>",
|
|
211
|
+
{"NATIVE-PRIMITIVE"}),
|
|
212
|
+
("packages/web-modules/chat/legacy.html",
|
|
213
|
+
"<chat-shell>\n <section data-chat-messages></section>\n <chat-input-ui data-chat-input></chat-input-ui>\n</chat-shell>",
|
|
214
|
+
{"LEGACY-SHELL"}),
|
|
215
|
+
]
|
|
216
|
+
ok = True
|
|
217
|
+
for name, text, expected in cases:
|
|
218
|
+
got = {f[0] for f in lint_text(text, name)}
|
|
219
|
+
if expected - got:
|
|
220
|
+
ok = False
|
|
221
|
+
print(f"selftest: {name} MISSING {sorted(expected - got)} (got {sorted(got)})", file=sys.stderr)
|
|
222
|
+
if not expected and got:
|
|
223
|
+
ok = False
|
|
224
|
+
print(f"selftest: {name} expected clean, got {sorted(got)}", file=sys.stderr)
|
|
225
|
+
saved_in, saved_out = sys.stdin, sys.stdout # never-block invariant: --hook exits 0 even on smelly input
|
|
226
|
+
try:
|
|
227
|
+
sys.stdin = io.StringIO(json.dumps({"tool_input": {"file_path": "c.css",
|
|
228
|
+
"content": ".a { color:#f00; }"}}))
|
|
229
|
+
sys.stdout = io.StringIO()
|
|
230
|
+
rc = _hook()
|
|
231
|
+
finally:
|
|
232
|
+
sys.stdin, sys.stdout = saved_in, saved_out
|
|
233
|
+
if rc != 0:
|
|
234
|
+
ok = False
|
|
235
|
+
print("selftest: --hook did not exit 0 on smelly input", file=sys.stderr)
|
|
236
|
+
print("selftest: PASS" if ok else "selftest: FAIL")
|
|
237
|
+
return 0 if ok else 1
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def main(argv):
|
|
241
|
+
if argv and argv[0] == "selftest":
|
|
242
|
+
return _selftest()
|
|
243
|
+
if "--hook" in argv:
|
|
244
|
+
return _hook()
|
|
245
|
+
args = [a for a in argv if a == "-" or not a.startswith("-")]
|
|
246
|
+
if not args:
|
|
247
|
+
print(__doc__.strip().split("\n\n", 1)[0], file=sys.stderr)
|
|
248
|
+
return 2
|
|
249
|
+
total = 0
|
|
250
|
+
for path in args:
|
|
251
|
+
if path == "-":
|
|
252
|
+
findings = lint_text(sys.stdin.read(), "<stdin>")
|
|
253
|
+
path = "<stdin>"
|
|
254
|
+
else:
|
|
255
|
+
findings = _lint_path(path)
|
|
256
|
+
if findings:
|
|
257
|
+
total += len(findings)
|
|
258
|
+
print(_render(path, findings))
|
|
259
|
+
return 1 if total else 0
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
if __name__ == "__main__":
|
|
263
|
+
sys.exit(main(sys.argv[1:]))
|