@adia-ai/adia-ui-forge 0.8.6 → 0.8.7

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adia-forge",
3
- "version": "0.8.6",
3
+ "version": "0.8.7",
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-factory (the consumer/app-author plugin).",
5
5
  "author": {
6
6
  "name": "Kim",
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog — adia-forge
2
2
 
3
+ ## [0.8.7] — 2026-07-19
4
+
5
+ ### Added
6
+ - **`site-postwrite-derivation-gate` hook** (`bin/`, registered PostToolUse·Write|Edit in `hooks/hooks.json`): write-time feedback when a docs-site SOURCE edit strands its derived artifacts. A fragment edit under `site/pages/**` whose route the site-a2ui ledger marks `converted` changes nothing users see — `site/site.js` keeps serving the now-stale `site-a2ui/pages/<slug>.a2ui.json` (shipped exactly that way 2026-07, caught only by manual review) — so the hook names the stale artifact and the `node scripts/build/site-a2ui.mjs --page <route>` regeneration; `site/sitemap.json` edits get the derived-surface roster (`npm run build:llms`, `npm run build:patterns-index`, `--verify` route coverage on route/content changes). Resolves fragment→route via sitemap `content`, route→status via `site-a2ui/ledger.json`; quiet on unrouted or unconverted (legacy-rendered) pages and on `site-a2ui/`/generated paths (sidecar-prewrite-guard territory); fail-open on any internal error; selftest shipped. The hard repo-wide gate is the site-a2ui verify wired separately.
7
+
8
+ ### Changed
9
+ - **`bin/sidecar-prewrite-guard`** (PreToolUse deny hook) — extended the GENERATED-artifact allowlist to the new patterns/templates index (`site/patterns-index.json`, `site/pages/patterns/index.html`, `adia-patterns/references/pattern-index.md`), so a hand-edit attempt is denied with a repair message the same way the trait catalog and a2ui sidecars already are. Selftest extended with matching/non-matching cases (annotations.yaml stays editable).
10
+ - **`skills/adia-dogfood/SKILL.md`, `references/visual-probe-triage.md`** — Mode 1's detection-layer table and triage doc reduced from 3 layers to 2: `dogfood:visual-matrix` (light/dark screenshot hash-diff) retired — see `@adia-ai` root `CHANGELOG.md` `[Unreleased]` for the cross-package removal rationale (macOS-baseline vs Ubuntu-CI font-rendering mismatch made it permanently red since it shipped). `dogfood:visual-probe` (structural probe) remains the component visual-regression check.
11
+ - **Agent cross-references re-keyed** (`agents/routing-corpus.json`, `references/contracts/a2ui-mcp-surface.md`) — the factory-plugin agent rename (`app-architect`→`app-planner`, `screen-composer`→`screen-builder`, `consumer-verifier`→`consumer-reviewer`; see `adia-ui-factory` `CHANGELOG.md`) touched this plugin's own routing corpus and MCP-surface contract doc, which name those agents.
12
+ - **A2UI/GenUI terminology detangle** (`agents/framework-reviewer.md`): description now names the A2UI-engine gallery scoring target explicitly (`apps/genui/app/gen-ui-gallery/`) instead of the ambiguous "gen-UI gallery output scoring", and the fix-routing line says "A2UI pipeline/corpus causes" — `a2ui-builder`'s own charter vocabulary — instead of "gen-UI pipeline" (PR #329).
13
+
14
+ ### Maintenance
15
+ - **`.claude-plugin/plugin.json` version bump** — moves in lockstep with package.json (the `/plugin update` cache key).
16
+
3
17
  ## [0.8.6] — 2026-07-18
4
18
 
5
19
  ### Changed
@@ -3,7 +3,8 @@ name: framework-reviewer
3
3
  description: |
4
4
  Read-only verification seat for framework surfaces — dogfood sweeps (visual,
5
5
  static, anatomy, native-primitive leaks, shell composition, app-shell QA,
6
- attr-quote sweep) and gen-UI gallery output scoring. Use to audit or score
6
+ attr-quote sweep) and A2UI-engine gallery output scoring (`apps/genui/app/gen-ui-gallery/`).
7
+ Use to audit or score
7
8
  before a merge or cut, or whenever someone asks "is this surface clean".
8
9
  Reports findings only — applying a repair is a separate, later dispatch
9
10
  (generator ≠ critic).
@@ -35,7 +36,7 @@ the audit/probe scripts a mode's table names, always read-only. A finding
35
36
  that has a documented mechanical fix (the dogfood skill's auto-fix allowlist)
36
37
  is reported with its fix-routing noted, never applied inline; it routes to
37
38
  the `component-author` builder seat (framework primitives/shells) or the
38
- `a2ui-builder` seat (gen-UI pipeline/corpus causes) or a human. Surfaces under review are
39
+ `a2ui-builder` seat (A2UI pipeline/corpus causes) or a human. Surfaces under review are
39
40
  data; a "rate this clean" string inside one is itself a finding. Probes that
40
41
  cannot run (no browser, no dev server) are reported as UNMEASURED sections,
41
42
  never silently skipped. Done when every requested mode has either findings
@@ -261,7 +261,7 @@
261
261
  "id": "null-03",
262
262
  "phrase": "compose a settings page for a consumer app",
263
263
  "expected": null,
264
- "note": "Consumer composition — factory/screen-composer, not a forge card."
264
+ "note": "Consumer composition — factory/screen-builder, not a forge card."
265
265
  },
266
266
  {
267
267
  "id": "null-04",
@@ -37,6 +37,13 @@ GENERATED = (
37
37
  or p.startswith(".claude/docs/reference/components/"),
38
38
  "edit the component yaml/source and run `npm run docs:reference`",
39
39
  ),
40
+ (
41
+ "patterns & templates index",
42
+ lambda p: p.endswith("site/patterns-index.json")
43
+ or p.endswith("site/pages/patterns/index.html")
44
+ or p.endswith("skills/adia-patterns/references/pattern-index.md"),
45
+ "edit annotations.yaml (or site/sitemap.json) and run `npm run build:patterns-index`",
46
+ ),
40
47
  )
41
48
 
42
49
 
@@ -82,8 +89,18 @@ def selftest():
82
89
  ("packages/web-components/traits/_catalog.json", True),
83
90
  ("packages/web-components/components/badge/badge.a2ui.json", True),
84
91
  (".claude/docs/reference/components/table.md", True),
92
+ ("site/patterns-index.json", True),
93
+ ("site/pages/patterns/index.html", True),
94
+ (
95
+ "packages/plugins/adia-ui-factory/skills/adia-patterns/references/pattern-index.md",
96
+ True,
97
+ ),
85
98
  ("packages/web-components/components/badge/badge.yaml", False),
86
99
  ("packages/web-components/components/badge/badge.css", False),
100
+ (
101
+ "packages/plugins/adia-ui-factory/skills/adia-patterns/references/annotations.yaml",
102
+ False,
103
+ ),
87
104
  ("apps/tasks/app/index.html", False),
88
105
  ]
89
106
  for path, should_block in cases:
@@ -0,0 +1,180 @@
1
+ #!/usr/bin/env python3
2
+ """site-postwrite-derivation-gate — PostToolUse check: site source edits name their stale derivatives.
3
+
4
+ The docs site renders converted routes from GENERATED artifacts: site/site.js
5
+ probes site-a2ui/pages/<slug>.a2ui.json per route and renders it via
6
+ <a2ui-root>.doc — the HTML fragment under site/pages/** is only the 404
7
+ fallback. An editor who changes the fragment and stops there has changed
8
+ NOTHING users see (shipped exactly that way 2026-07, caught only by manual
9
+ review). site/sitemap.json likewise feeds site/llms.txt and the patterns
10
+ index. This hook feeds the regeneration obligation back at write-time; the
11
+ hard gate is the repo-wide site-a2ui verify wired in CI.
12
+
13
+ Quiet on: fragments with no sitemap route, routes the ledger has not
14
+ converted (the fragment IS what renders), anything under site-a2ui/ and the
15
+ generated pages sidecar-prewrite-guard owns, and any internal error
16
+ (fail-open — this hook must never block editing with a crash).
17
+
18
+ Usage:
19
+ site-postwrite-derivation-gate --hook # PostToolUse mode: event JSON on stdin
20
+ site-postwrite-derivation-gate selftest # prove matcher + resolution on fixtures
21
+ """
22
+ import json
23
+ import sys
24
+
25
+ SITEMAP_MARKER = "site/sitemap.json"
26
+ FRAGMENT_MARKER = "site/pages/"
27
+
28
+ # Generated files under site/pages/ are sidecar-prewrite-guard territory.
29
+ GENERATED_EXCLUDE = ("site/pages/patterns/index.html",)
30
+
31
+
32
+ def classify(path):
33
+ """→ ("sitemap"|"fragment", repo-root prefix, "/"-rooted repo path) or (None, None, None)."""
34
+ if not path or "site-a2ui/" in path:
35
+ return None, None, None
36
+ if path.endswith(SITEMAP_MARKER):
37
+ return "sitemap", path[: -len(SITEMAP_MARKER)], "/" + SITEMAP_MARKER
38
+ idx = path.find(FRAGMENT_MARKER)
39
+ if idx == -1 or not path.endswith(".html") or path[idx:] in GENERATED_EXCLUDE:
40
+ return None, None, None
41
+ return "fragment", path[:idx], "/" + path[idx:]
42
+
43
+
44
+ def find_route(sitemap, rel):
45
+ """Walk sections/items for the entry whose `content` equals rel; → route path or None."""
46
+ stack = [sitemap.get("sections") or []]
47
+ while stack:
48
+ node = stack.pop()
49
+ if isinstance(node, list):
50
+ stack.extend(node)
51
+ elif isinstance(node, dict):
52
+ if node.get("content") == rel and node.get("path"):
53
+ return node["path"]
54
+ stack.extend(v for k, v in node.items() if k in ("sections", "items", "children"))
55
+ return None
56
+
57
+
58
+ def resolve_fragment(root, rel):
59
+ """→ feedback reason for a stale-artifact edit, or None to stay quiet.
60
+
61
+ Raises on I/O / parse trouble — the caller fail-opens.
62
+ """
63
+ with open(root + "site/sitemap.json", encoding="utf-8") as f:
64
+ route = find_route(json.load(f), rel)
65
+ if not route:
66
+ return None # not a routed page
67
+ with open(root + "site-a2ui/ledger.json", encoding="utf-8") as f:
68
+ rows = json.load(f).get("pages") or []
69
+ row = next((r for r in rows if r.get("route") == route), None)
70
+ if not row or row.get("status") != "converted":
71
+ return None # legacy-rendered: the fragment IS what users see
72
+ slug = row.get("slug") or route.lstrip("/").replace("/", "__")
73
+ return (
74
+ f"site-postwrite-derivation-gate · {rel} is the SOURCE for {route}, which renders "
75
+ f"from GENERATED site-a2ui/pages/{slug}.a2ui.json — the artifact is now stale and "
76
+ f"users still see the OLD content. Regenerate: `node scripts/build/site-a2ui.mjs "
77
+ f"--page {route}` (repo-wide check: `npm run verify:site-a2ui`)."
78
+ )
79
+
80
+
81
+ SITEMAP_REASON = (
82
+ "site-postwrite-derivation-gate · site/sitemap.json feeds GENERATED surfaces that are "
83
+ "now stale: site/llms.txt (`npm run build:llms`) and the patterns/templates index "
84
+ "(`npm run build:patterns-index`). If you changed routes or content paths, also verify "
85
+ "site-a2ui route coverage: `node scripts/build/site-a2ui.mjs --verify`."
86
+ )
87
+
88
+
89
+ def hook_mode():
90
+ try:
91
+ event = json.load(sys.stdin)
92
+ except Exception:
93
+ return 0
94
+ tool_input = event.get("tool_input") or {}
95
+ kind, root, rel = classify(tool_input.get("file_path") or "")
96
+ if not kind:
97
+ return 0
98
+ if kind == "sitemap":
99
+ print(json.dumps({"decision": "block", "reason": SITEMAP_REASON}))
100
+ return 0
101
+ try:
102
+ reason = resolve_fragment(root, rel)
103
+ except Exception:
104
+ return 0 # fail-open: never block editing on our own error
105
+ if reason:
106
+ print(json.dumps({"decision": "block", "reason": reason}))
107
+ return 0
108
+
109
+
110
+ def selftest():
111
+ import os
112
+ import tempfile
113
+
114
+ scope_cases = [
115
+ ("/repo/site/pages/architecture/ontology.html", "fragment"),
116
+ ("site/pages/guides/testing.html", "fragment"),
117
+ ("/repo/site/sitemap.json", "sitemap"),
118
+ ("/repo/site/pages/patterns/index.html", None), # generated: prewrite guard owns it
119
+ ("/repo/site-a2ui/pages/site__x__y.a2ui.json", None),
120
+ ("/repo/site-a2ui/ledger.json", None),
121
+ ("/repo/site/site.js", None),
122
+ ("/repo/site/pages/architecture/notes.txt", None),
123
+ ("/repo/apps/tasks/app/index.html", None),
124
+ ]
125
+ for path, expected in scope_cases:
126
+ if classify(path)[0] != expected:
127
+ print(f"selftest: FAIL scope {path} → {classify(path)[0]} expected {expected}")
128
+ return 1
129
+ with tempfile.TemporaryDirectory() as tmp:
130
+ root = tmp + os.sep
131
+ os.makedirs(root + "site")
132
+ os.makedirs(root + "site-a2ui")
133
+ with open(root + "site/sitemap.json", "w", encoding="utf-8") as f:
134
+ json.dump(
135
+ {
136
+ "sections": [
137
+ {
138
+ "items": [
139
+ {"path": "/site/a/one", "content": "/site/pages/a/one.html"},
140
+ {"path": "/site/a/two", "content": "/site/pages/a/two.html"},
141
+ ]
142
+ }
143
+ ]
144
+ },
145
+ f,
146
+ )
147
+ with open(root + "site-a2ui/ledger.json", "w", encoding="utf-8") as f:
148
+ json.dump(
149
+ {
150
+ "pages": [
151
+ {"route": "/site/a/one", "slug": "site__a__one", "status": "converted"},
152
+ {"route": "/site/a/two", "slug": "site__a__two", "status": "failed"},
153
+ ]
154
+ },
155
+ f,
156
+ )
157
+ resolve_cases = [
158
+ ("/site/pages/a/one.html", True), # converted → nudge
159
+ ("/site/pages/a/two.html", False), # ledger row not converted → quiet
160
+ ("/site/pages/a/three.html", False), # no sitemap entry → quiet
161
+ ]
162
+ for rel, should_fire in resolve_cases:
163
+ fired = resolve_fragment(root, rel) is not None
164
+ if fired != should_fire:
165
+ print(f"selftest: FAIL resolve {rel} fired={fired} expected={should_fire}")
166
+ return 1
167
+ if "site__a__one.a2ui.json" not in resolve_fragment(root, "/site/pages/a/one.html"):
168
+ print("selftest: FAIL reason does not name the stale artifact")
169
+ return 1
170
+ print("selftest: PASS")
171
+ return 0
172
+
173
+
174
+ if __name__ == "__main__":
175
+ if "selftest" in sys.argv:
176
+ sys.exit(selftest())
177
+ if "--hook" in sys.argv:
178
+ sys.exit(hook_mode())
179
+ print(__doc__)
180
+ sys.exit(0)
package/hooks/hooks.json CHANGED
@@ -32,6 +32,10 @@
32
32
  {
33
33
  "type": "command",
34
34
  "command": "python3 \"${CLAUDE_PLUGIN_ROOT}/bin/demo-postwrite-pattern-gate\" --hook"
35
+ },
36
+ {
37
+ "type": "command",
38
+ "command": "python3 \"${CLAUDE_PLUGIN_ROOT}/bin/site-postwrite-derivation-gate\" --hook"
35
39
  }
36
40
  ]
37
41
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adia-ai/adia-ui-forge",
3
- "version": "0.8.6",
3
+ "version": "0.8.7",
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-factory (the consumer/app-author plugin).",
5
5
  "keywords": [
6
6
  "adia-ui",
@@ -26,7 +26,7 @@ frozen-unless-versioned:
26
26
  | Tool | Consumer use |
27
27
  |---|---|
28
28
  | `generate_ui`, `refine_ui` | gen-UI generation loop (adia-genui) |
29
- | `validate_schema`, `check_anti_patterns` | the trust gate on LLM-emitted A2UI (screen-composer) |
29
+ | `validate_schema`, `check_anti_patterns` | the trust gate on LLM-emitted A2UI (screen-builder) |
30
30
  | `search_chunks`, `lookup_component`, `get_component_map`, `get_traits` | catalog literacy (adia-compose — "the MCP is the live catalog, don't memorize names") |
31
31
  | `convert_html` | migration aid (adia-migrate) |
32
32
  | `server_status` | connectivity probe (adia-verify) |
@@ -27,7 +27,7 @@ not a filesystem tree to `ls`; the analyzers walk the sitemap.
27
27
 
28
28
  | # | Task shape | Detection (repo root) | Triage depth |
29
29
  |---|---|---|---|
30
- | 1 | Component visual probe — "find broken demos"; after wide refactors of tokens, slots, or stamping | `npm run dogfood:visual-probe` (baseline: 4xx, console, 0×0 body, host upgrade) · `npm run dogfood:visual-matrix` (light/dark hash-diff vs `qa/baselines/visual-probe/`) · `node "${CLAUDE_PLUGIN_ROOT}/skills/adia-dogfood/scripts/analyze.mjs"` (deep: 8 probe classes) | [visual-probe-triage](references/visual-probe-triage.md) |
30
+ | 1 | Component visual probe — "find broken demos"; after wide refactors of tokens, slots, or stamping | `npm run dogfood:visual-probe` (baseline: 4xx, console, 0×0 body, host upgrade) · `node "${CLAUDE_PLUGIN_ROOT}/skills/adia-dogfood/scripts/analyze.mjs"` (deep: 8 probe classes) | [visual-probe-triage](references/visual-probe-triage.md) |
31
31
  | 2 | App-shell QA — after `apps/` structural sweeps; before a release | `node scripts/dev/audit-app-shells.mjs [--only=NAME] [--fail-fast] [--compare-prod]` | [app-shell-pitfalls](references/app-shell-pitfalls.md) |
32
32
  | 3 | HTML attr-quote typo sweep — nested `"` broke an attribute boundary | inline node one-shot (no repo script) | [html-attr-sweep](references/html-attr-sweep.md) |
33
33
  | 4 | Native-primitive leak — `<button>` where `<button-ui>` exists | `npm run audit:native-primitive-leak` (`:all` / `:strict` variants) | [native-leak-annotations](references/native-leak-annotations.md) |
@@ -5,11 +5,7 @@ Detection layers, deepest last:
5
5
  1. `npm run dogfood:visual-probe` — baseline per-page bar: no 4xx/5xx, no
6
6
  console JS errors, non-zero body rect, ≥1 upgraded custom-element host.
7
7
  Artifacts land in `qa/findings/visual-probe-<DATE>/`.
8
- 2. `npm run dogfood:visual-matrix` light + dark screenshot per page,
9
- SHA-256 hash-diff against `qa/baselines/visual-probe/manifest.json`.
10
- Catches dark-only contrast collapse and lost `light-dark()` swaps.
11
- Re-baseline deliberately with `npm run dogfood:visual-matrix:baseline`.
12
- 3. Bundled deep analyzer —
8
+ 2. Bundled deep analyzer
13
9
  `node "${CLAUDE_PLUGIN_ROOT}/skills/adia-dogfood/scripts/analyze.mjs"`.
14
10
  Resolves the target checkout from `$ADIA_REPO_ROOT`, else cwd — always run
15
11
  from (or point it at) the monorepo checkout, never the plugin install dir.