@adia-ai/adia-ui-forge 0.8.55 → 0.8.57

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 (44) hide show
  1. package/.claude-plugin/plugin.json +3 -2
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +40 -0
  4. package/README.md +1 -1
  5. package/__init__.py +5 -0
  6. package/commands/demo-audit.md +1 -1
  7. package/commands/gen-ui-review.md +1 -1
  8. package/commands/package-release.md +1 -1
  9. package/commands/site-deployment.md +1 -1
  10. package/package.json +1 -1
  11. package/plugin.yaml +1 -1
  12. package/prompts/demo-audit.md +1 -1
  13. package/prompts/gen-ui-review.md +1 -1
  14. package/prompts/package-release.md +1 -1
  15. package/prompts/site-deployment.md +1 -1
  16. package/scripts/forge-lint.mjs +168 -0
  17. package/scripts/lint-rules.generated.mjs +1700 -0
  18. package/scripts/site-postwrite-derivation-gate +23 -127
  19. package/skills/a2ui-maintenance/SKILL.md +1 -1
  20. package/skills/a2ui-maintenance/references/data-model-reactivity.md +49 -29
  21. package/skills/a2ui-maintenance/references/pipeline-overview.md +58 -22
  22. package/skills/a2ui-maintenance/references/surface-lifecycle.md +14 -7
  23. package/skills/component-md-authoring/SKILL.md +116 -0
  24. package/skills/component-md-authoring/agents/openai.yaml +3 -0
  25. package/skills/demo-audit/references/visual-probe-triage.md +4 -1
  26. package/skills/gen-ui-review/SKILL.md +4 -1
  27. package/skills/gen-ui-review/references/loop-protocol.md +6 -5
  28. package/skills/package-release/references/changelog-discipline.md +6 -3
  29. package/skills/package-release/references/cut-procedure.md +61 -14
  30. package/skills/package-release/references/gates-catalog.md +6 -2
  31. package/skills/package-release/references/recovery-paths.md +6 -3
  32. package/skills/package-release/scripts/gate-roster.mjs +10 -7
  33. package/skills/package-release/scripts/release-pack.mjs +289 -17
  34. package/skills/primitive-authoring/references/anti-patterns.md +2 -2
  35. package/skills/primitive-authoring/references/api-contract.md +17 -4
  36. package/skills/primitive-authoring/references/authoring-cycle.md +1 -1
  37. package/skills/primitive-authoring/references/code-style.md +1 -1
  38. package/skills/primitive-authoring/references/common-gotchas.md +6 -6
  39. package/skills/primitive-authoring/references/form-control-sizing.md +22 -7
  40. package/skills/primitive-authoring/references/token-contract.md +5 -0
  41. package/skills/primitive-authoring/references/yaml-contract.md +132 -14
  42. package/skills/site-docs-authoring/SKILL.md +7 -9
  43. package/hooks/hooks.json +0 -44
  44. package/scripts/forge-lint +0 -315
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adia-ui-kit-forge",
3
- "version": "0.8.55",
3
+ "version": "0.8.57",
4
4
  "description": "Maintain the adia-ui (@adia-ai) framework itself \u2014 author primitives and shells, run the A2UI generation pipeline and its corpus, review gen-UI quality, sweep QA, cut releases, deploy. The maintainer counterpart to adia-ui-kit-factory (the consumer/app-author plugin).",
5
5
  "author": {
6
6
  "name": "Kim",
@@ -16,5 +16,6 @@
16
16
  "framework-authoring",
17
17
  "release-engineering",
18
18
  "maintainer"
19
- ]
19
+ ],
20
+ "hooks": "./adapters/claude/hooks.json"
20
21
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adia-ui-kit-forge",
3
- "version": "0.8.55",
3
+ "version": "0.8.57",
4
4
  "description": "Maintain the adia-ui (@adia-ai) framework itself — author primitives and shells, run the A2UI generation pipeline and its corpus, review gen-UI quality, sweep QA, cut releases, deploy. The maintainer counterpart to adia-ui-kit-factory (the consumer/app-author plugin).",
5
5
  "author": {
6
6
  "name": "Kim",
package/CHANGELOG.md CHANGED
@@ -1,5 +1,45 @@
1
1
  # Changelog — adia-ui-kit-forge
2
2
 
3
+ ## [0.8.57] — 2026-09-01
4
+
5
+ ### Removed
6
+ - **`package-release`'s release-roster gate 19 (`verify:site-a2ui`) retired with the site-a2ui mechanism itself (ADR-0072 Decision 2, gh#2410 active drain).** `gate-roster.mjs` drops the gate 19 entry (33 gates → 32; gate number left unassigned rather than renumbering 20+, per the roster's own no-renumber discipline for gate identities cited in shipped release notes); `cut-procedure.md` §3.1's transcription and its `.examples.html`-generator trigger prose updated to match. The `site-postwrite-derivation-gate` hook's fragment-staleness half (the site-a2ui regeneration nudge) is removed; its sitemap-staleness half (`site/sitemap.json` → `build:llms`/`build:patterns-index`), unrelated to site-a2ui, is unchanged. `a2ui-maintenance`'s `pipeline-overview.md` and `site-docs-authoring`'s `SKILL.md`/`primitive-authoring`'s `yaml-contract.md` no longer instruct regenerating a compiled A2UI artifact that no longer exists.
7
+
8
+ ### Fixed
9
+ - **Plugin passes `author-cross-harness-plugins`'s cross-harness validator, modulo one documented, permanent, irreconcilable exception (gh#2668).** `${CLAUDE_PLUGIN_ROOT}`/`$ARGUMENTS`/the `` !`command` `` auto-injection syntax removed from shared `gen-ui-review/SKILL.md`, the four `commands/*.md` bodies, and their linked reference docs (`demo-audit/references/visual-probe-triage.md`, `package-release/references/{cut-procedure,changelog-discipline,gates-catalog,recovery-paths}.md`, `gen-ui-review/references/loop-protocol.md`) — portable `<plugin-root>` prose / "named in the current request" phrasing throughout; command frontmatter, including `package-release`/`site-deployment`'s deliberate `disable-model-invocation: true` (AGENTS.md-ruled), is untouched since the validator never scans command frontmatter. `hooks/hooks.json` moved to `adapters/claude/hooks.json` (unchanged, `${CLAUDE_PLUGIN_ROOT}`-based); new root `hooks.json` mirrors the same event/matcher/action signature with monorepo-root-relative command paths for Codex's convention-discovered hook surface — an unverified-but-honest fallback documented in `HARNESS-NOTES.md`. **The one exception:** every skill's `disable-model-invocation:`/`user-invocable:` frontmatter — required, explicitly, by this repo's own blocking `check:skills` gate (`plugin-estate-v2.md`'s species-pinning charter) — collides head-on with the validator's opposite requirement on the exact same file, and Codex is hard-pinned to read root `skills/` directly (no adapter escape). The repo's own charter is not weakened; `npm run check:cross-harness-plugins` (`scripts/verify/check-cross-harness-plugins.mjs`, wrapping the vendored `scripts/verify/vendor/validate_cross_harness.py`) treats only this one exact finding shape as accepted (logged as INFO) and still fails on every other portability class. Full reasoning in `HARNESS-NOTES.md`.
10
+
11
+ ### Changed
12
+ - **`forge-lint` rewritten as a thin Node CLI (`forge-lint.mjs`) consuming a shared generated lint-rule bank, retiring the Python script (gh#2638, LLD-0016, ADR-0099).** The framework-authoring structural checks (SHADOW-DOM, SLOTTED, RAW-COLOR, RAW-PX, SCOPE-EXTENT, DEAD-FONT-TOKEN, BOOL-DEFAULT-TRUE, ATTR-TYPO, NATIVE-PRIMITIVE, LEGACY-SHELL, MISSING-SCOPE) now live as individually-fixtured rule modules under `scripts/lint/rules/` in the repo root, built into `scripts/lint/generated/rule-bank.mjs` and vendored byte-identically into this plugin, `adia-ui-factory`, and the gen-ui MCP's factory vendor tree (`npm run build:lint-rules`, freshness gated by `npm run check:lint-rules-fresh`). `hooks/hooks.json`'s PostToolUse command changes from `python3 ".../forge-lint" --hook` to `node ".../forge-lint.mjs" --hook`; the CLI contract, exit codes, and PostToolUse never-block invariant are unchanged. `npm run check:lint-rules-selftest` proves finding-parity against the retired script's own `selftest()` fixtures. New `npm run lint` (`scripts/lint/lint-repo.mjs`) runs the full bank against every git-tracked lintable file — standalone for now, not yet wired into `npm run check` (a first repo-wide run surfaced 330 error-severity pre-existing findings that need triage first — gh#2646). The suppression comment (`/* adia-lint-disable <rule-id> — <reason> */`) is now load-bearing, not cosmetic: it drops the matching finding, and a bare disable with no reason is itself a finding (`SUPPRESS-NO-REASON`).
13
+
14
+ ### Maintenance
15
+ - **`.claude-plugin/plugin.json` version bump** — moves in lockstep with package.json (the `/plugin update` cache key).
16
+ - **`.codex-plugin/` touched in this release window** (1 file(s), e.g. `.codex-plugin/plugin.json`) — carried by the entries above.
17
+ - **`prompts/` touched in this release window** (4 file(s), e.g. `prompts/demo-audit.md`) — carried by the entries above.
18
+
19
+ ## [0.8.56] — 2026-08-31
20
+
21
+ ### Added
22
+ - **New `component-md-authoring` skill (gh#2615).** Authors the two judgment sections of a component's `component.md` — Screen-reader spec and Behavioral spec — typed directly into the yaml's new `screenReader:`/`behavioral:` fields (component.md itself is a generated shell, never hand-edited). Sits next to `primitive-authoring` in `packages/plugins/adia-ui-forge/skills/`; Codex (`agents/openai.yaml`) and Hermes/Pi (`__init__.py`) manifests regenerated to register it.
23
+ - **`primitive-authoring`'s `yaml-contract.md` gains a semantic-id grammar for `examples[].a2ui` component ids (gh#2492 Phase 1).** The `.claude/docs/specs/a2ui-editor.md:170` "free-form" id convention was written for editor-generated ids only, not authored yaml examples — reading it as license for both produced a corpus-wide drift toward cryptic, positional ids (`q`, `k1`, `k1v`) reported against `blockquote.yaml`, `badge.yaml`, `aside.yaml`, `alert.yaml`, and `anchor-bar.yaml`. New `## examples: field` section documents the grammar (kebab-case, role-descriptive, unique per example, a short-real-word root/wrapper exception) with before/after examples; `a2ui-editor.md:170` amended to scope its own claim to editor-generated ids and cross-reference the new grammar. Phase 2 (a separate dispatch) sweeps the corpus to match.
24
+ - **Phase 2 (gh#2492): the corpus-wide sweep landed and `check-example-ids.mjs` flips from advisory to `npm run check:example-ids:strict` in the `npm run check` chain.** 526 violations across 48 yamls (`packages/web-components`, `packages/web-modules`) renamed to the Phase 1 grammar — see `packages/web-components/CHANGELOG.md` and `packages/web-modules/CHANGELOG.md` for the file list. `yaml-contract.md`'s `## examples: field` section updated to describe the gate as enforced, plus a noted known gap (the checker's positional regex doesn't catch a `letter-digit-letter-digit` chain like `g1i1`, out of scope for this sweep since nothing flagged it). `check-example-ids.mjs` itself gained an entrypoint guard (`if (import.meta.url === ...)`) — the CLI's `main()` was running as a side effect of importing `checkExampleIds` as a library function, which the sweep tooling does.
25
+
26
+ ### Fixed
27
+ - **`package-release`'s Step 5 staging allowlist was missing the factory README's own prose echo of the generation-MCP pin — bump.mjs's `A2UI_MCP_PIN` rewrites both `adia-ui-factory/.mcp.json` and its README, but Step 5 only ever staged `.mcp.json`, so the README edit rode uncommitted through every cut (gh#2473).** `release-pack.mjs`'s Step 5 now also stages `packages/plugins/adia-ui-factory/README.md`. Added a new Step 5.6 unstaged-tracked-files guard right after staging (numbered 5.6, not 5.5 — `cut-procedure.md`'s own §Step 5.5 already names a different, pre-existing manual freshness trip-wire): it fails the cut loudly if any tracked file bump.mjs or a cut-hygiene step touched is still modified-in-the-worktree after Step 5's allowlist ran, so the next allowlist gap is caught at cut time instead of discovered by hand later. `selftest` extended with a direct unit test of the new `unstagedTrackedLeftovers()` parser (plain modifies/deletes, staged-only and untracked exclusions, C-quoted paths, rename/copy new-side reporting) plus a marker asserting the guard's `--dry` code path runs.
28
+
29
+ ### Changed
30
+ - **`package-release`: gate 4 retries once on host-contention timeout (Mode A, gh#2195, #2362)**; `cut-procedure.md` fails fast on a pnpm-shaped `node_modules` in Step 1 and adds Step 12 (restore the pnpm dev layout after a cut) (#2197, #2202); `gates-catalog.md` follows.
31
+ - **`a2ui-maintenance` references follow the A2UI runtime.** `data-model-reactivity.md`, `surface-lifecycle.md`, and `pipeline-overview.md` describe the vendored v1 data model after ADR-0078 P3/P4 (walker retirement, #2271), the `v0_9`→`v1_0` dialect rename (gh#2212, #2241), and the five opt-out-scoped catalogs (gh#2211, #2233, #2216).
32
+ - **`primitive-authoring` references absorb the ADR-drift harvest (#2441) and ADR-0063 wave 2 (#2443)**: `anti-patterns.md`, `api-contract.md`, `authoring-cycle.md`, `code-style.md`, `form-control-sizing.md`, `token-contract.md`, and `yaml-contract.md` now state the ratified spellings (`no-*` negation prefix, `closable`, `outlined`, `status=completed`) and the amended ADR-0047/0056/0059/0061 rulings inline.
33
+ - **`HARNESS-NOTES.md`** pointer for non-Claude harnesses (gh#2449).
34
+
35
+ ### Fixed
36
+ - **`generate-release-notes.mjs` (gh#2474): two bugs found and hand-fixed live during the v0.8.56 cut, now fixed at the source.** (1) When a CHANGELOG `[X.Y.Z]` section carries two same-named `### Changed`/`### Fixed` blocks (one at cut time, another added by a later hand-fix), the generator now merges them into one heading in first-occurrence order before demoting to `####` — the duplicate `####` heading that tripped CodeRabbit's MD024 (db13eaebb) can't recur. (2) The Slack post's per-package headline now prefers a bullet starting with `- **BREAKING` over document order, falling back to the first bullet only when none is BREAKING (b378b5fd9). Added `mergeDuplicateHeadings()`, unit coverage in `scripts/release/generate-release-notes.test.js`, and `selftest` regression cases for both. `scripts/release/generate-release-notes.mjs`.
37
+
38
+ ### Maintenance
39
+ - **`.claude-plugin/plugin.json` version bump** — moves in lockstep with package.json (the `/plugin update` cache key).
40
+ - **`.codex-plugin/` touched in this release window** (1 file(s), e.g. `.codex-plugin/plugin.json`) — carried by the entries above.
41
+ - **`skills/` touched in this release window** (14 file(s), e.g. `a2ui-maintenance/SKILL.md`) — carried by the entries above.
42
+
3
43
  ## [0.8.55] — 2026-08-28
4
44
 
5
45
  ### Changed
package/README.md CHANGED
@@ -36,7 +36,7 @@ same decomposition scoped to A2UI catalog/tier cohesion, never touches corpus or
36
36
  `framework-checker` (fable, **no Write/Edit** — read-only corpus-wide cohesion
37
37
  audit: color tokens, typography, state naming, attrs-as-api, implementation patterns).
38
38
 
39
- ## Hooks (5 — `hooks/hooks.json` is the roster; `check:plugin-count-claims` guards this header count)
39
+ ## Hooks (5 — `adapters/claude/hooks.json` is the roster; `check:plugin-count-claims` guards this header count)
40
40
 
41
41
  `sidecar-prewrite-guard` (PreToolUse deny on hand-edits to generated
42
42
  artifacts) · `release-pretag-docs-gate` (PreToolUse: release-tagging Bash
package/__init__.py CHANGED
@@ -11,6 +11,11 @@ def register(ctx):
11
11
  path=Path(os.path.join(_HERE, "skills", "a2ui-maintenance", "SKILL.md")),
12
12
  description="Maintains the A2UI pipeline (packages/gen-ui/a2ui/): the chunk corpus, compose strategies (zettel, chunk-zettel, free-form, monolithic), retrieval, validator, calibration, evals, the a2ui MCP server. Use to author/harvest/fix chunks, tune STRONG_MATCH or zettel thresholds, validate an A2UI document, diagnose an eval gap/regression or lift a semantic fail, change MCP tools (generate_ui, compose_from_chunks, check_anti_patterns, refine_composition), scan anti-patterns, run pipeline ops, or when a contract can't express a shape. NOT for app screens (screen-composition), runtime gen-UI features (gen-ui-wiring), primitive authoring (primitive-authoring), or gallery scoring (gen-ui-review).",
13
13
  )
14
+ ctx.register_skill(
15
+ name="component-md-authoring",
16
+ path=Path(os.path.join(_HERE, "skills", "component-md-authoring", "SKILL.md")),
17
+ description="Author the two judgment sections of a component's `component.md` — Screen-reader spec and Behavioral spec — and keep it PR-fresh. Use when a component's states, composed children, aria behavior, or error/empty/ loading handling changes and it already has (or should grow) a `component.md`, or when asked to \"add component.md for X\" / \"write the screen-reader spec for X\" / \"why did check:component-md-fresh fail\". NOT the yaml prop/slot/event/token contract itself (primitive-authoring owns that — this skill only owns the two authored yaml fields, `screenReader`/`behavioral`, plus the optional `intent` field); NOT gen-ui corpus/retrieval wiring (a2ui-maintenance); NOT a component's CSS token audit (component-token-audit).",
18
+ )
14
19
  ctx.register_skill(
15
20
  name="cross-harness-authoring-standards",
16
21
  path=Path(os.path.join(_HERE, "skills", "cross-harness-authoring-standards", "SKILL.md")),
@@ -6,7 +6,7 @@ disable-model-invocation: false
6
6
  user-invocable: true
7
7
  ---
8
8
 
9
- Run a dogfood sweep. **$ARGUMENTS**
9
+ Run a dogfood sweep against the mode/surface named in the current request (all, if unscoped).
10
10
 
11
11
  Invoke **`demo-audit`**; run the requested modes (all, if unscoped), then
12
12
  triage findings per its severity contract. Detection is script-driven; the
@@ -6,7 +6,7 @@ disable-model-invocation: false
6
6
  user-invocable: true
7
7
  ---
8
8
 
9
- Run a gen-UI review cycle. **$ARGUMENTS**
9
+ Run a gen-UI review cycle, scoped to the prompt subset named in the current request (if any).
10
10
 
11
11
  Invoke **`gen-ui-review`** and run its loop: derive ideal → decompose actual
12
12
  → score → root-cause → plan. Corpus fixes route to `a2ui-maintenance`.
@@ -6,7 +6,7 @@ disable-model-invocation: true
6
6
  user-invocable: true
7
7
  ---
8
8
 
9
- Run a release. **$ARGUMENTS**
9
+ Run a release for the version/package named in the current request.
10
10
 
11
11
  Invoke **`package-release`** and run its cut procedure end-to-end: pre-flight
12
12
  gates → cut → tag → publish → deploy → notes. This command's invocation is
@@ -6,7 +6,7 @@ disable-model-invocation: true
6
6
  user-invocable: true
7
7
  ---
8
8
 
9
- Deploy or diagnose an exe.dev surface. **$ARGUMENTS**
9
+ Deploy or diagnose the exe.dev host/service named in the current request.
10
10
 
11
11
  Invoke **`site-deployment`** and follow its playbook for the named host; verify
12
12
  against the live endpoint before reporting done.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adia-ai/adia-ui-forge",
3
- "version": "0.8.55",
3
+ "version": "0.8.57",
4
4
  "description": "Maintain the adia-ui (@adia-ai) framework itself — author primitives and shells, run the A2UI generation pipeline and its corpus, review gen-UI quality, sweep QA, cut releases, deploy. The maintainer counterpart to adia-factory (the consumer/app-author plugin).",
5
5
  "keywords": [
6
6
  "adia-ui",
package/plugin.yaml CHANGED
@@ -1,4 +1,4 @@
1
1
  name: "adia-ui-kit-forge"
2
- version: "0.8.55"
2
+ version: "0.8.57"
3
3
  description: "Maintain the adia-ui (@adia-ai) framework itself — author primitives and shells, run the A2UI generation pipeline and its corpus, review gen-UI quality, sweep QA, cut releases, deploy. The maintainer counterpart to adia-ui-kit-factory (the consumer/app-author plugin)."
4
4
  manifest_version: 1
@@ -3,7 +3,7 @@ description: "Run the multi-mode QA sweep (visual probe, static QA, typo, native
3
3
  argument-hint: "[mode or surface]"
4
4
  ---
5
5
 
6
- Run a dogfood sweep. **$ARGUMENTS**
6
+ Run a dogfood sweep against the mode/surface named in the current request (all, if unscoped).
7
7
 
8
8
  Invoke **`demo-audit`**; run the requested modes (all, if unscoped), then
9
9
  triage findings per its severity contract. Detection is script-driven; the
@@ -3,7 +3,7 @@ description: "Run the closed-loop gen-UI quality review — score gallery output
3
3
  argument-hint: "[prompt subset]"
4
4
  ---
5
5
 
6
- Run a gen-UI review cycle. **$ARGUMENTS**
6
+ Run a gen-UI review cycle, scoped to the prompt subset named in the current request (if any).
7
7
 
8
8
  Invoke **`gen-ui-review`** and run its loop: derive ideal → decompose actual
9
9
  → score → root-cause → plan. Corpus fixes route to `a2ui-maintenance`.
@@ -3,7 +3,7 @@ description: "Cut, tag, publish, and deploy an @adia-ai release (lockstep roster
3
3
  argument-hint: "[version or package]"
4
4
  ---
5
5
 
6
- Run a release. **$ARGUMENTS**
6
+ Run a release for the version/package named in the current request.
7
7
 
8
8
  Invoke **`package-release`** and run its cut procedure end-to-end: pre-flight
9
9
  gates → cut → tag → publish → deploy → notes. This command's invocation is
@@ -3,7 +3,7 @@ description: "Deploy a service or dist to the exe.dev VMs (ui-kit.exe.xyz and si
3
3
  argument-hint: "[host or service]"
4
4
  ---
5
5
 
6
- Deploy or diagnose an exe.dev surface. **$ARGUMENTS**
6
+ Deploy or diagnose the exe.dev host/service named in the current request.
7
7
 
8
8
  Invoke **`site-deployment`** and follow its playbook for the named host; verify
9
9
  against the live endpoint before reporting done.
@@ -0,0 +1,168 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * forge-lint.mjs — thin Node CLI consuming the vendored generated rule bank
4
+ * (lint-rules.generated.mjs, LLD-0016 §C5) for the adia-ui FRAMEWORK source
5
+ * (maintainer slice): the same structural-smell checks forge-lint (Python)
6
+ * ran, now run: 'shared' + 'forge-only' scope rules only — no consumer/app
7
+ * traps (SSR, hardcoded overlay open, LLM key, genui). See ADR-0099 for why
8
+ * this replaced the Python script as the hooks.json entry point.
9
+ *
10
+ * Usage:
11
+ * forge-lint.mjs <file>... lint files; exit 1 if any error-severity finding, else 0
12
+ * forge-lint.mjs - lint stdin as a generic source file
13
+ * forge-lint.mjs --hook PostToolUse hook mode: read event JSON on stdin, lint the
14
+ * written source file, print advisory findings, ALWAYS exit 0
15
+ * forge-lint.mjs selftest run the inherited parity fixtures + the hook exit-0 invariant
16
+ */
17
+ import { readFileSync, existsSync } from 'node:fs';
18
+ import { dirname, join } from 'node:path';
19
+ import { fileURLToPath } from 'node:url';
20
+
21
+ const __dirname = dirname(fileURLToPath(import.meta.url));
22
+ const { makeLinter, ext, LINT_EXT, RULES } = await import(join(__dirname, 'lint-rules.generated.mjs'));
23
+
24
+ /**
25
+ * Repo-root-relative parity fixtures are only reachable when this CLI is
26
+ * running from inside the monorepo checkout (the plugin's own source tree,
27
+ * not a packed/vendored install elsewhere) — walk up from this file looking
28
+ * for scripts/lint/rule-bank.selftest.mjs rather than a fixed `../..` depth,
29
+ * since this same CLI shape is vendored to more than one directory depth
30
+ * (ADR-0099). A vendored copy outside the monorepo has no fixtures to find
31
+ * and `selftest` reports that plainly instead of crashing.
32
+ */
33
+ function findParityFixtures() {
34
+ let dir = __dirname;
35
+ for (let i = 0; i < 8; i += 1) {
36
+ const candidate = join(dir, 'scripts/lint/rule-bank.selftest.mjs');
37
+ if (existsSync(candidate)) return candidate;
38
+ dir = join(dir, '..');
39
+ }
40
+ return null;
41
+ }
42
+
43
+ const SCOPE_ANCHOR = ['packages/web-components/', 'packages/web-modules/'];
44
+ const { lintText } = makeLinter(RULES, { scopes: ['shared', 'forge-only'] });
45
+
46
+ function render(path, findings) {
47
+ const out = [`forge-lint: ${findings.length} structural smell(s) in ${path || '<stdin>'}`];
48
+ for (const f of findings) {
49
+ out.push(` [${f.name}] line ${f.line}: ${f.snippet}`);
50
+ out.push(` → ${f.why}`);
51
+ }
52
+ return out.join('\n');
53
+ }
54
+
55
+ function lintPath(path) {
56
+ try {
57
+ return lintText(readFileSync(path, 'utf8'), path);
58
+ } catch (err) {
59
+ return [{ name: 'READ-ERROR', line: 1, snippet: path.slice(0, 90), why: `failed to read file: ${err.message}` }];
60
+ }
61
+ }
62
+
63
+ async function readStdin() {
64
+ const chunks = [];
65
+ for await (const c of process.stdin) chunks.push(c);
66
+ return Buffer.concat(chunks).toString('utf8');
67
+ }
68
+
69
+ async function hook() {
70
+ let event;
71
+ try {
72
+ event = JSON.parse(await readStdin());
73
+ } catch {
74
+ return 0;
75
+ }
76
+ const ti = event.tool_input || {};
77
+ const path = ti.file_path || '';
78
+ const norm = path.replace(/\\/g, '/');
79
+ if (!SCOPE_ANCHOR.some((a) => norm.includes(a))) return 0;
80
+ if (!LINT_EXT.includes(ext(path))) return 0;
81
+ let text = ti.content;
82
+ if (text === undefined || text === null) {
83
+ if (!existsSync(path)) return 0;
84
+ try {
85
+ text = readFileSync(path, 'utf8');
86
+ } catch {
87
+ return 0;
88
+ }
89
+ }
90
+ const findings = lintText(text, path);
91
+ if (findings.length) {
92
+ const context = render(path, findings)
93
+ + '\n (forge-lint advisory — adia-ui framework authoring smells; the skills own the judgment)';
94
+ process.stdout.write(JSON.stringify({
95
+ hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: context },
96
+ }) + '\n');
97
+ }
98
+ return 0; // NEVER block
99
+ }
100
+
101
+ async function selftest() {
102
+ const fixturesPath = findParityFixtures();
103
+ if (!fixturesPath) {
104
+ console.log('selftest: SKIP — no monorepo checkout found (parity fixtures live in scripts/lint/, not vendored)');
105
+ return 0;
106
+ }
107
+ const { PARITY_CASES } = await import(fixturesPath);
108
+ let ok = true;
109
+ for (const { name, text, expected, scopes } of PARITY_CASES) {
110
+ if (scopes && !(scopes.includes('forge-only'))) continue; // this CLI's own cases only
111
+ const got = new Set(lintText(text, name).map((f) => f.name));
112
+ const missing = [...expected].filter((e) => !got.has(e));
113
+ if (missing.length) {
114
+ ok = false;
115
+ console.error(`selftest: ${name} MISSING ${JSON.stringify(missing)} (got ${JSON.stringify([...got])})`);
116
+ }
117
+ if (expected.size === 0 && got.size) {
118
+ ok = false;
119
+ console.error(`selftest: ${name} expected clean, got ${JSON.stringify([...got])}`);
120
+ }
121
+ }
122
+ // never-block invariant
123
+ const savedWrite = process.stdout.write.bind(process.stdout);
124
+ let hookOut = '';
125
+ process.stdout.write = (chunk, ...rest) => { hookOut += chunk; return true; };
126
+ const stdinMock = Buffer.from(JSON.stringify({ tool_input: { file_path: 'packages/web-components/c.css', content: '.a { color:#f00; }' } }));
127
+ const origStdin = process.stdin;
128
+ Object.defineProperty(process, 'stdin', { value: (async function* () { yield stdinMock; })(), configurable: true });
129
+ let rc;
130
+ try {
131
+ rc = await hook();
132
+ } finally {
133
+ process.stdout.write = savedWrite;
134
+ Object.defineProperty(process, 'stdin', { value: origStdin, configurable: true });
135
+ }
136
+ if (rc !== 0) {
137
+ ok = false;
138
+ console.error('selftest: --hook did not exit 0 on smelly input');
139
+ }
140
+ if (rc === 0 && hookOut && !hookOut.includes('"hookSpecificOutput"')) {
141
+ ok = false;
142
+ console.error('selftest: --hook stdout is not structured hookSpecificOutput JSON');
143
+ }
144
+ console.log(ok ? 'selftest: PASS' : 'selftest: FAIL');
145
+ return ok ? 0 : 1;
146
+ }
147
+
148
+ async function main(argv) {
149
+ if (argv[0] === 'selftest') return selftest();
150
+ if (argv.includes('--hook')) return hook();
151
+ const args = argv.filter((a) => a === '-' || !a.startsWith('-'));
152
+ if (!args.length) {
153
+ console.error('usage: forge-lint.mjs <file>... | forge-lint.mjs - | forge-lint.mjs --hook | forge-lint.mjs selftest');
154
+ return 2;
155
+ }
156
+ let total = 0;
157
+ for (let path of args) {
158
+ const findings = path === '-' ? lintText(await readStdin(), '<stdin>') : lintPath(path);
159
+ if (path === '-') path = '<stdin>';
160
+ if (findings.length) {
161
+ total += findings.length;
162
+ console.log(render(path, findings));
163
+ }
164
+ }
165
+ return total ? 1 : 0;
166
+ }
167
+
168
+ process.exit(await main(process.argv.slice(2)));