@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.
Files changed (136) hide show
  1. package/.claude-plugin/plugin.json +23 -0
  2. package/CHANGELOG.md +26 -0
  3. package/README.md +54 -0
  4. package/bin/forge-lint +263 -0
  5. package/bin/lib/audit-axes.mjs +555 -0
  6. package/bin/lib/dry-run-irreversible.mjs +236 -0
  7. package/bin/lib/run-skill-evals.mjs +487 -0
  8. package/bin/lib/teach-router.mjs +250 -0
  9. package/commands/adia-forge-a2ui.md +10 -0
  10. package/commands/adia-forge-author.md +10 -0
  11. package/commands/adia-forge-dogfood.md +8 -0
  12. package/commands/adia-forge-llm.md +8 -0
  13. package/commands/adia-forge-orient.md +10 -0
  14. package/commands/adia-forge-release.md +8 -0
  15. package/commands/adia-forge-review.md +10 -0
  16. package/hooks/hooks.json +15 -0
  17. package/package.json +41 -0
  18. package/references/shared/content-trust.md +76 -0
  19. package/references/shared/pev-rationale.md +64 -0
  20. package/references/shared/skill-conventions.md +133 -0
  21. package/skills/adia-ui-a2ui/CHANGELOG.md +32 -0
  22. package/skills/adia-ui-a2ui/SKILL.md +243 -0
  23. package/skills/adia-ui-a2ui/evals/adversarial-corpus.json +75 -0
  24. package/skills/adia-ui-a2ui/evals/routing-corpus.json +31 -0
  25. package/skills/adia-ui-a2ui/evals/teach-routing-cases.json +100 -0
  26. package/skills/adia-ui-a2ui/references/anti-patterns.md +24 -0
  27. package/skills/adia-ui-a2ui/references/chunk-authoring.md +88 -0
  28. package/skills/adia-ui-a2ui/references/corpus-discipline.md +56 -0
  29. package/skills/adia-ui-a2ui/references/eval-diagnostics.md +127 -0
  30. package/skills/adia-ui-a2ui/references/fragment-graph.md +91 -0
  31. package/skills/adia-ui-a2ui/references/mcp-pipeline-ops.md +106 -0
  32. package/skills/adia-ui-a2ui/references/mcp-tool-reference.md +398 -0
  33. package/skills/adia-ui-a2ui/references/pipeline-overview.md +175 -0
  34. package/skills/adia-ui-a2ui/references/semantic-fail-lifting.md +120 -0
  35. package/skills/adia-ui-a2ui/references/strategy-engines.md +111 -0
  36. package/skills/adia-ui-a2ui/references/teach-protocol.md +220 -0
  37. package/skills/adia-ui-a2ui/references/zettel-calibration.md +93 -0
  38. package/skills/adia-ui-a2ui/scripts/audit-a2ui-roster.mjs +96 -0
  39. package/skills/adia-ui-a2ui/scripts/teach-route.mjs +157 -0
  40. package/skills/adia-ui-a2ui/skill.json +38 -0
  41. package/skills/adia-ui-authoring/CHANGELOG.md +32 -0
  42. package/skills/adia-ui-authoring/SKILL.md +256 -0
  43. package/skills/adia-ui-authoring/assets/case-studies/admin-shell-decomposition.md +101 -0
  44. package/skills/adia-ui-authoring/assets/case-studies/maxtokens-32768-discovery.md +109 -0
  45. package/skills/adia-ui-authoring/assets/case-studies/theme-panel-promotion.md +113 -0
  46. package/skills/adia-ui-authoring/evals/adversarial-design-plan-gates.json +138 -0
  47. package/skills/adia-ui-authoring/evals/routing-corpus.json +245 -0
  48. package/skills/adia-ui-authoring/references/anti-patterns.md +606 -0
  49. package/skills/adia-ui-authoring/references/api-contract.md +205 -0
  50. package/skills/adia-ui-authoring/references/authoring-cycle.md +211 -0
  51. package/skills/adia-ui-authoring/references/canonical-pattern-index.md +179 -0
  52. package/skills/adia-ui-authoring/references/code-style.md +329 -0
  53. package/skills/adia-ui-authoring/references/common-gotchas.md +93 -0
  54. package/skills/adia-ui-authoring/references/composite-demo-protocol.md +1084 -0
  55. package/skills/adia-ui-authoring/references/css-patterns.md +364 -0
  56. package/skills/adia-ui-authoring/references/lifecycle-patterns.md +302 -0
  57. package/skills/adia-ui-authoring/references/llm-bridge.md +254 -0
  58. package/skills/adia-ui-authoring/references/module-promotion.md +289 -0
  59. package/skills/adia-ui-authoring/references/primitive-audit.md +123 -0
  60. package/skills/adia-ui-authoring/references/shell-patterns.md +561 -0
  61. package/skills/adia-ui-authoring/references/teach-protocol.md +428 -0
  62. package/skills/adia-ui-authoring/references/token-contract.md +120 -0
  63. package/skills/adia-ui-authoring/references/worked-example.md +351 -0
  64. package/skills/adia-ui-authoring/references/yaml-contract.md +224 -0
  65. package/skills/adia-ui-authoring/scripts/audit-authoring-roster.mjs +148 -0
  66. package/skills/adia-ui-authoring/scripts/build-canonical-pattern-index.mjs +199 -0
  67. package/skills/adia-ui-authoring/skill.json +45 -0
  68. package/skills/adia-ui-dogfood/CHANGELOG.md +17 -0
  69. package/skills/adia-ui-dogfood/README.md +62 -0
  70. package/skills/adia-ui-dogfood/SKILL.md +866 -0
  71. package/skills/adia-ui-dogfood/scripts/analyze.mjs +603 -0
  72. package/skills/adia-ui-dogfood/skill.json +40 -0
  73. package/skills/adia-ui-forge/SKILL.md +88 -0
  74. package/skills/adia-ui-forge/evals/routing-corpus.json +30 -0
  75. package/skills/adia-ui-gen-review/CHANGELOG.md +108 -0
  76. package/skills/adia-ui-gen-review/SKILL.md +266 -0
  77. package/skills/adia-ui-gen-review/references/loop-protocol.md +712 -0
  78. package/skills/adia-ui-gen-review/references/rubric-cosmetic.md +144 -0
  79. package/skills/adia-ui-gen-review/references/rubric-decompose.md +117 -0
  80. package/skills/adia-ui-gen-review/references/rubric-score.md +249 -0
  81. package/skills/adia-ui-gen-review/references/scores.schema.json +125 -0
  82. package/skills/adia-ui-gen-review/references/teach-protocol.md +214 -0
  83. package/skills/adia-ui-gen-review/scripts/gen-review-coverage-audit.mjs +127 -0
  84. package/skills/adia-ui-gen-review/scripts/gen-review-decompose.mjs +569 -0
  85. package/skills/adia-ui-gen-review/scripts/gen-review-status.mjs +176 -0
  86. package/skills/adia-ui-gen-review/scripts/validate-cycle-scores.mjs +198 -0
  87. package/skills/adia-ui-gen-review/skill.json +22 -0
  88. package/skills/adia-ui-llm/CHANGELOG.md +25 -0
  89. package/skills/adia-ui-llm/SKILL.md +165 -0
  90. package/skills/adia-ui-llm/evals/adversarial-corpus.json +75 -0
  91. package/skills/adia-ui-llm/evals/routing-corpus.json +30 -0
  92. package/skills/adia-ui-llm/evals/teach-routing-cases.json +73 -0
  93. package/skills/adia-ui-llm/references/adapter-contract.md +99 -0
  94. package/skills/adia-ui-llm/references/add-a-provider.md +90 -0
  95. package/skills/adia-ui-llm/references/bridge-facade.md +94 -0
  96. package/skills/adia-ui-llm/references/browser-proxy-boundary.md +89 -0
  97. package/skills/adia-ui-llm/references/model-registry.md +75 -0
  98. package/skills/adia-ui-llm/references/streaming-sse.md +95 -0
  99. package/skills/adia-ui-llm/references/teach-protocol.md +78 -0
  100. package/skills/adia-ui-llm/scripts/audit-llm-roster.mjs +93 -0
  101. package/skills/adia-ui-llm/scripts/teach-route.mjs +119 -0
  102. package/skills/adia-ui-llm/skill.json +33 -0
  103. package/skills/adia-ui-release/CHANGELOG.md +23 -0
  104. package/skills/adia-ui-release/SKILL.md +295 -0
  105. package/skills/adia-ui-release/assets/case-studies/2026-05-20-batch-push-v0.6.14-v0.6.15.md +144 -0
  106. package/skills/adia-ui-release/assets/case-studies/2026-05-20-corpus-drift-remediation-v0.6.15.md +155 -0
  107. package/skills/adia-ui-release/assets/case-studies/2026-05-20-version-skip-correction-v0.6.12.md +114 -0
  108. package/skills/adia-ui-release/assets/case-studies/2026-05-21-author-from-scratch-v0.6.18.md +139 -0
  109. package/skills/adia-ui-release/assets/case-studies/2026-05-21-feedback37-retraction-v0.6.21.md +124 -0
  110. package/skills/adia-ui-release/assets/case-studies/2026-05-21-fn1-enrichment-pass-v0.6.19.md +125 -0
  111. package/skills/adia-ui-release/assets/case-studies/2026-05-21-stale-test-detection-v0.6.20.md +142 -0
  112. package/skills/adia-ui-release/assets/case-studies/2026-05-23-freshness-gate-recovery-v0.6.32.md +97 -0
  113. package/skills/adia-ui-release/assets/case-studies/2026-05-26-catalog-drift-recurring-v0.6.40.md +147 -0
  114. package/skills/adia-ui-release/assets/templates/stub-changelog.template.md +22 -0
  115. package/skills/adia-ui-release/evals/evals.json +164 -0
  116. package/skills/adia-ui-release/references/changelog-discipline.md +250 -0
  117. package/skills/adia-ui-release/references/cycle-happy-path.md +520 -0
  118. package/skills/adia-ui-release/references/exe-deploy.md +149 -0
  119. package/skills/adia-ui-release/references/gates-catalog.md +778 -0
  120. package/skills/adia-ui-release/references/ledger-discipline.md +232 -0
  121. package/skills/adia-ui-release/references/migration-guide-authoring.md +174 -0
  122. package/skills/adia-ui-release/references/multi-agent-baseline.md +207 -0
  123. package/skills/adia-ui-release/references/notes-authoring.md +212 -0
  124. package/skills/adia-ui-release/references/recovery-paths.md +215 -0
  125. package/skills/adia-ui-release/references/rollup-notes.md +208 -0
  126. package/skills/adia-ui-release/references/teach-protocol.md +468 -0
  127. package/skills/adia-ui-release/scripts/assert-monorepo-root.mjs +49 -0
  128. package/skills/adia-ui-release/scripts/audit-gate-roster.mjs +196 -0
  129. package/skills/adia-ui-release/scripts/bump.mjs +118 -0
  130. package/skills/adia-ui-release/scripts/dispatch-publish.mjs +151 -0
  131. package/skills/adia-ui-release/scripts/insert-stub.mjs +127 -0
  132. package/skills/adia-ui-release/scripts/make-ledger.mjs +179 -0
  133. package/skills/adia-ui-release/scripts/promote-unreleased.mjs +88 -0
  134. package/skills/adia-ui-release/scripts/release-pack.mjs +350 -0
  135. package/skills/adia-ui-release/scripts/tag-lockstep.mjs +110 -0
  136. package/skills/adia-ui-release/skill.json +75 -0
@@ -0,0 +1,254 @@
1
+ # LLM Bridge extension — Mode 6
2
+
3
+ Use when adding a 4th provider, modifying `createAdapter()`, changing the streaming chunk shape, or extending `packages/llm/server.js`.
4
+
5
+ This reference is about **modifying** the package. For day-to-day consumption (import + call), the README at `packages/llm/README.md` is sufficient.
6
+
7
+ Absorbed from the legacy `llm-bridge-extension` skill (folded earlier into the code-bestpractices content, now into `adia-ui-authoring` mode 6).
8
+
9
+ ---
10
+
11
+ ## When to use
12
+
13
+ - Adding a new provider adapter (e.g., DeepSeek, Cohere, Mistral)
14
+ - Changing `maxTokens`, `temperature`, or another bridge default
15
+ - Extending the streaming chunk type set (currently: `text` / `thinking` / `done` / `error`)
16
+ - Adding a new endpoint to `packages/llm/server.js`
17
+ - Modifying the bridge's `complete()` / `stream()` shape
18
+ - Debugging "why does my LLM call return truncated JSON?"
19
+
20
+ ## When NOT to use
21
+
22
+ - Just calling the LLM → `import { chat, streamChat } from '@adia-ai/llm'`
23
+ - A2UI pipeline integration → use `@adia-ai/llm/bridge` as-is
24
+ - Testing pipeline code without API keys → use `StubLLMAdapter` from `@adia-ai/llm/stub`
25
+
26
+ ## Architecture (memorize before extending)
27
+
28
+ ```text
29
+ @adia-ai/llm (9th lockstep package, leaf-shaped, no internal deps)
30
+
31
+ ├── adapters/index.js ← Layer 1: chat() + streamChat() facade
32
+ │ ├── anthropic.js ← Each adapter: { name, buildRequest, parseStream }
33
+ │ ├── openai.js
34
+ │ ├── gemini.js
35
+ │ └── sse.js ← shared SSE parser
36
+
37
+ ├── llm-bridge.js ← Layer 2: createAdapter() → AdiaUILLMBridge
38
+ │ (lazy-loads adapters/index.js to dodge Vite aliases)
39
+
40
+ ├── llm-stub.js ← StubLLMAdapter (no API calls, deterministic)
41
+ ├── models.js ← MODELS catalog + DEFAULT_MODEL (chat-input-ui shape)
42
+ └── server.js ← Local proxy + static server (NOT shipped to npm)
43
+ ```
44
+
45
+ ## Hard rules — defaults that bit us
46
+
47
+ ### Rule 1: `maxTokens: 32768` is intentional
48
+
49
+ The bridge hardcodes `maxTokens: 32768` in `complete()` and `stream()`. **Don't lower this.**
50
+
51
+ A2UI JSON for moderately complex UIs (kanban, dashboard, pricing tier table) routinely exceeds 8k. An 8k truncation was discovered producing silent fallbacks the validator rubber-stamped at ~89/100 — the generator looked healthy but was emitting incomplete component trees.
52
+
53
+ If you're adding a new provider that has a lower maxTokens ceiling (e.g., a model with 4k output limit), surface it as a model-level constraint in `MODELS`, **don't bypass the 32k bridge default**.
54
+
55
+ ### Rule 2: Anthropic prompt caching is auto-enabled
56
+
57
+ The bridge passes `cache: this.#provider === 'anthropic'`. This marks the ~23KB AdiaUI system prompt as a cache breakpoint (ephemeral, ~5min TTL).
58
+
59
+ - First call in window: cache write (+25% cost)
60
+ - Subsequent calls: cache read (−90% cost)
61
+ - Below provider's minimum cacheable size (1024 tok Sonnet/Opus, 2048 Haiku): no-op
62
+ - Other providers silently ignore the unknown opt
63
+
64
+ When adding a new provider, leave `cache` falsy unless that provider has its own caching mechanism — then add a parallel `case` in the adapter's `buildRequest`.
65
+
66
+ ### Rule 3: stopReason must propagate
67
+
68
+ The bridge surfaces `stopReason` from the upstream response in both `complete()` and `stream()`'s terminal `done` chunk. Values:
69
+
70
+ - `'end'` / `'stop'` / `'STOP'` — clean completion
71
+ - `'max_tokens'` / `'length'` / `'MAX_TOKENS'` (Gemini) — **truncation**
72
+ - `'tool_use'` / `'tool_calls'` — tool-call paused output
73
+ - `'error'` — provider-side error
74
+
75
+ **Downstream parser refuses silent fallback rendering when stopReason indicates truncation.** Don't normalize all values to `'end'` — the generator's truncation detector reads the raw value.
76
+
77
+ ### Rule 4: lazy-load adapters in browser-facing modules
78
+
79
+ `llm-bridge.js` lazy-loads `./adapters/index.js`:
80
+
81
+ ```js
82
+ let _createClient = null;
83
+ async function getCreateClient() {
84
+ if (!_createClient) {
85
+ try {
86
+ const mod = await import('./adapters/index.js');
87
+ _createClient = mod.createClient;
88
+ } catch {
89
+ _createClient = null; // fall back to stub
90
+ }
91
+ }
92
+ return _createClient;
93
+ }
94
+ ```
95
+
96
+ **Reason**: `adapters/index.js` transitively reaches `node:fs` / `node:path` constructs (via Vite's externalize behavior) that throw at module load time in pure-Node contexts without aliases. Top-level static imports break SSR / Node-script consumers.
97
+
98
+ If you add a new module under `adapters/` that imports `node:` builtins, keep the lazy-load gate intact.
99
+
100
+ ### Rule 5: `server.js` is NOT shipped to npm
101
+
102
+ `package.json:files` excludes `server.js`. It's a local dev / visual-eval convenience. Production consumers deploy their own proxy.
103
+
104
+ If you add a new endpoint, document it in the npm README under "Browser proxy mode" but **don't promise it as a published API**.
105
+
106
+ ## How to add a 4th provider
107
+
108
+ ### Step 1: Create the adapter
109
+
110
+ `packages/llm/adapters/<name>.js` exports a single object matching the shape used by anthropic / openai / gemini:
111
+
112
+ ```js
113
+ import { readSSE } from './sse.js';
114
+
115
+ export const myProvider = {
116
+ name: 'myprovider',
117
+ buildRequest(opts) {
118
+ return {
119
+ url: 'https://api.example.com/v1/chat',
120
+ headers: { 'content-type': 'application/json', 'x-api-key': opts.apiKey },
121
+ body: { model: opts.model, messages: opts.messages, stream: opts.stream },
122
+ };
123
+ },
124
+ async *parseStream(response) {
125
+ for await (const event of readSSE(response.body)) {
126
+ // yield { type: 'text', text, snapshot } | { type: 'done', text, usage, stopReason }
127
+ }
128
+ },
129
+ parseResponse(json) {
130
+ // returns { text, usage: { input, output }, stopReason }
131
+ },
132
+ };
133
+ ```
134
+
135
+ ### Step 2: Register in the facade
136
+
137
+ Edit `packages/llm/adapters/index.js`:
138
+
139
+ ```js
140
+ import { myProvider } from './myprovider.js';
141
+ const providers = { anthropic, openai, gemini, myprovider: myProvider };
142
+
143
+ function detectProvider(model) {
144
+ // ...existing branches
145
+ if (m.includes('myprovider-prefix') || m.startsWith('myprovider/')) return 'myprovider';
146
+ return null;
147
+ }
148
+ ```
149
+
150
+ ### Step 3: Subpath export
151
+
152
+ Edit `packages/llm/package.json`:
153
+
154
+ ```json
155
+ "exports": {
156
+ ".": "./index.js",
157
+ "./adapters/*": "./adapters/*.js",
158
+ ...
159
+ }
160
+ ```
161
+
162
+ The `./adapters/*` glob already covers new files — no edit needed unless you want a named subpath.
163
+
164
+ ### Step 4: Update `models.js` catalog
165
+
166
+ Add a new group:
167
+
168
+ ```js
169
+ export const MODELS = [
170
+ ...,
171
+ {
172
+ label: 'MyProvider',
173
+ options: [
174
+ { value: 'myprovider-flagship', label: 'Flagship' },
175
+ ],
176
+ },
177
+ ];
178
+ ```
179
+
180
+ ### Step 5: Wire Vite proxy (browser dev)
181
+
182
+ Edit `vite.config.*`:
183
+
184
+ ```js
185
+ '/api/llm/myprovider': {
186
+ target: 'https://api.example.com',
187
+ changeOrigin: true,
188
+ rewrite: (path) => path.replace(/^\/api\/llm\/myprovider/, ''),
189
+ }
190
+ ```
191
+
192
+ ### Step 6: Wire `server.js` (visual-eval proxy)
193
+
194
+ Add a `PROVIDERS.myprovider` config block matching anthropic / openai / gemini's shape (`url`, `keyEnv`, `buildHeaders`, `buildBody`).
195
+
196
+ ### Step 7: Bridge default model
197
+
198
+ Edit `llm-bridge.js`:
199
+
200
+ ```js
201
+ const DEFAULT_MODELS = {
202
+ anthropic: 'claude-sonnet-4-20250514',
203
+ openai: 'gpt-4o',
204
+ google: 'gemini-2.0-flash',
205
+ myprovider: 'myprovider-flagship',
206
+ };
207
+ ```
208
+
209
+ ### Step 8: Test surface
210
+
211
+ Run the smoke tests against the new provider:
212
+
213
+ ```bash
214
+ MYPROVIDER_API_KEY=*** node packages/a2ui/mcp/scripts/smoke-engine-registry.mjs
215
+ ```
216
+
217
+ Then run real-LLM eval at low intent count:
218
+
219
+ ```bash
220
+ LLM_PROVIDER=myprovider node packages/a2ui/mcp/scripts/eval-diff.mjs --semantic
221
+ ```
222
+
223
+ ## Pitfalls
224
+
225
+ - **Two proxy shapes — `proxyUrl` is overloaded.** The bridge supports two architecturally different proxies and the dispatcher in `packages/llm/adapters/index.js` chooses between them by URL shape. Get this wrong and you get silent 401s in the browser even though `node packages/llm/server.js` works fine.
226
+ - **Smart proxy** (`packages/llm/server.js`, route `POST /api/chat`): accepts a **provider-neutral body** `{ provider, model, messages, ... }`, holds the API key server-side, and dispatches internally to the right adapter. Headers are just `content-type: application/json`.
227
+ - **Passthrough proxy** (Vite dev `/api/llm/<provider>/<rest>` → real upstream URL `https://api.<provider>.com/<rest>`): expects the **real upstream body shape** (Anthropic-shaped for `/anthropic/`, OpenAI-shaped for `/openai/`, etc.) plus the **adapter's own auth headers** (`x-api-key` + `anthropic-version`, or `Authorization: Bearer …`). The proxy is dumb — it just rewrites the URL and forwards bytes.
228
+ - Dispatcher: `chat()` and `streamChat()` in `adapters/index.js` call `isPassthroughProxy(proxyUrl)` (regex `/\/api\/llm\/[a-z]+(\/|$)/`). If true, they call `passthroughRequest()` which builds the real upstream body via `adapter.buildRequest()` then swaps in `proxyUrl`. If false, they call `proxyRequest()` with the provider-neutral body.
229
+ - **When adding a new provider**, make sure: (1) `buildRequest()` omits the API key header when `proxyUrl` is the smart proxy (already handled), (2) `buildRequest()` _includes_ the API key header when called from `passthroughRequest()` (also already handled — passthroughRequest forwards adapter headers). The adapter's `buildRequest()` is the single source of truth for upstream-shape body + headers. Don't fork it for proxy mode.
230
+ - **Don't add `apiKey` to the proxy body**. The proxy holds the key server-side. Adapter's `buildRequest` should set the API-key header only when `proxyUrl` is unset (i.e., direct mode).
231
+ - **Don't normalize `stopReason`**. Each provider uses different terminal values (`max_tokens` vs `length` vs `MAX_TOKENS`); the truncation detector reads them raw.
232
+ - **Don't import the package from itself.** `index.js` re-exports from `./adapters/index.js`; consumers inside the package reference relative paths to dodge the package-name resolution loop.
233
+ - **`MODELS` is consumed by `<chat-input-ui>`** — its shape is `[{ label, options: [{ value, label }] }]` matching `<select-ui>` with `<optgroup>`s. Don't deviate from this shape or 3 apps break.
234
+ - **`DEFAULT_MODEL` is the cheapest option** — resist the urge to default to a flagship model — most consumers want fast/cheap by default.
235
+
236
+ ## Verification gates
237
+
238
+ After any extension:
239
+
240
+ ```bash
241
+ npm run check:lockstep # @adia-ai/llm version must match the others
242
+ npm run smoke:engines # engines still register; pick still works
243
+ npm run test:a2ui # 22 pass / 0 fail / 1 skipped
244
+ node packages/llm/server.js # boots cleanly; .env auto-loads
245
+ ```
246
+
247
+ If you added a new SSE chunk type to the streaming protocol, verify all 7 consumers (chat-shell, generator.js, synthesis.js, eval-chunk-synthesis, eval-refine-synthesis, playgrounds/chat, apps/genui) still handle it correctly or fall through gracefully.
248
+
249
+ ## Cross-references
250
+
251
+ - [code-style.md](code-style.md) — general AdiaUI conventions (this is a specialty path; start there if you need broader context)
252
+ - **adia-ui-a2ui** (sibling skill) — generator / MCP / zettel synthesis (consumes the bridge)
253
+ - Spec: `docs/specs/package-architecture.md` § 11 (Phase 5 — engine registry)
254
+ - README: `packages/llm/README.md` — consumer-facing docs
@@ -0,0 +1,289 @@
1
+ # Promote inline → module — Mode 4
2
+
3
+ Use when you find the same UI block (markup + controller JS) authored in two or more consumers — typically the docs shell (`site/`) and a playground (`playgrounds/<name>/`) — and need to lift it into a single reusable element under `packages/web-modules/<cluster>/<name>/`.
4
+
5
+ This reference codifies the **5-phase arc** proven on `<theme-panel>`. Phases are independent commits — trivially revertible.
6
+
7
+ Absorbed from the legacy `promote-inline-to-module` skill (now a redirect; this file replaces the daily-driver content).
8
+
9
+ > See [shell-patterns.md](shell-patterns.md) for the **different** activity of decomposing a single shell into a family of bespoke child elements per ADR-0023. This reference is for the _cross-cluster_ case where a single control surface (theme panel, command palette, future notification center, future user-menu) needs to live as one element used by many shells.
10
+
11
+ ---
12
+
13
+ ## When to use
14
+
15
+ - A consumer-authored inline block appears (with non-trivial drift) in 2+ surfaces, AND
16
+ - The block has a stable, semi-public API surface (4–10 props at most), AND
17
+ - The bug-surface from drift is real (e.g. behavior B is wrong relative to behavior A — playground "always shows 1,1" vs docs "reads computed").
18
+
19
+ ## When NOT to use
20
+
21
+ - The duplication is ≤ 10 lines and has zero behavior drift — leave it inline.
22
+ - The element is genuinely shell-internal (admin-only, chat-only) — use [shell-patterns.md](shell-patterns.md) instead.
23
+ - The block is a CSS-only template — promote to `catalog/ui-patterns/` not `packages/web-modules/`.
24
+ - The block is a primitive (composes < 3 other primitives, no state) — promote to `packages/web-components/components/` instead.
25
+
26
+ ## The 5-phase arc
27
+
28
+ Each phase = one commit. Each independently revertible. Verification gate runs at the end of every phase before the commit.
29
+
30
+ ### Phase 1 — Author the element
31
+
32
+ 1. `mkdir -p packages/web-modules/<cluster>/<name>` (new cluster, or under an existing one — see "Cluster placement" below).
33
+ 2. Author **7 files** mirroring `web-modules/shell/admin-sidebar/`:
34
+
35
+ ```text
36
+ <name>.yaml # source of truth — schema + a2ui rules + keywords
37
+ <name>.js # UIElement subclass; light-DOM; imperative DOM stamping
38
+ <name>.css # two-block @scope pattern per component-token-contract.md
39
+ <name>.html # standalone demo (loads <name>.examples.html via fetch)
40
+ <name>.examples.html # the matrix consumed by the live demo
41
+ <name>.test.js # vitest happy-dom — minimum 15 specs
42
+ <name>.a2ui.json # GENERATED — never hand-edit
43
+ ```
44
+
45
+ 3. Wire the **4 package.json + barrel touchpoints**:
46
+ - `packages/web-modules/<cluster>/index.js` — `export { Name } from './<name>/<name>.js';`
47
+ - `packages/web-modules/index.js` — add `export * from './<cluster>/index.js';`
48
+ - `packages/web-modules/package.json` `exports` — add `"./<cluster>": "./<cluster>/index.js"` + `"./<cluster>/*": "./<cluster>/*/*.js"`
49
+ - `packages/web-modules/package.json` `files` — add `"<cluster>/"`
50
+ - `packages/web-modules/package.json` `sideEffects` — add `"./<cluster>/**/*.js"`
51
+ 4. Regenerate catalog: `node scripts/build/components.mjs` — produces `<name>.a2ui.json` + updates the aggregate corpus catalog.
52
+ 5. **Gate** before commit:
53
+
54
+ ```bash
55
+ node scripts/build/components.mjs --verify # "clean — N files up-to-date"
56
+ npm run verify:traits # 100%
57
+ npm run smoke:engines # green
58
+ npm run smoke:register-engine # 11/11
59
+ npm run test:a2ui # 22/0/1
60
+ npx vitest run packages/web-modules/<cluster>/<name>/<name>.test.js
61
+ ```
62
+
63
+ ### Phase 2 — Migrate primary consumer (typically `site/`)
64
+
65
+ The "full-fat" consumer — the one with persistence, prefers-color-scheme listeners, etc. This is your reference behavior; replicate its full surface in the module.
66
+
67
+ 1. Replace inline markup with single-tag composition:
68
+
69
+ ```html
70
+ <popover-ui placement="bottom-end">
71
+ <button-ui icon="palette" variant="ghost" size="sm" slot="trigger"></button-ui>
72
+ <my-module slot="content" persist parametric presets scheme-toggle></my-module>
73
+ </popover-ui>
74
+ ```
75
+
76
+ 2. **If the consumer has a sibling button that the module now absorbs** (e.g. a standalone `#theme-toggle` next to the popover trigger, which now lives inside the panel as `[scheme-toggle]`): retire the sibling button per **OD-002 = A**. Don't leave both.
77
+ 3. Delete the controller blocks from `<consumer>.js`. Confirm via:
78
+
79
+ ```bash
80
+ grep -nE 'id="theme-default"|id="param-density"|id="preset-compact"' \
81
+ site/index.html site/site.js
82
+ # Expect zero hits.
83
+ ```
84
+
85
+ 4. **LS-key migration shim** — if the module uses a different storage namespace than the legacy controller, ship a one-shot read-old / write-new / drop-old block at the top of the consumer's boot script. Tested recipe:
86
+
87
+ ```js
88
+ // ── One-shot LS migration (YYYY-MM-DD) ──
89
+ // TODO(YYYY-MM+60d): remove once all users have visited.
90
+ {
91
+ const oldToNew = {
92
+ 'adia-theme': 'adia-theme-scheme',
93
+ 'adia-theme-name': 'adia-theme-theme',
94
+ 'adia---a-density': 'adia-theme-density',
95
+ 'adia---a-radius-k': 'adia-theme-radius',
96
+ };
97
+ for (const [oldKey, newKey] of Object.entries(oldToNew)) {
98
+ try {
99
+ const v = localStorage.getItem(oldKey);
100
+ if (v != null && !localStorage.getItem(newKey)) {
101
+ localStorage.setItem(newKey, v);
102
+ }
103
+ if (v != null) localStorage.removeItem(oldKey);
104
+ } catch {}
105
+ }
106
+ }
107
+ ```
108
+
109
+ Idempotent across reloads (second-run finds nothing to migrate). Add a removal TODO with a date 60+ days out.
110
+
111
+ 5. **Gate**: `node --check` the consumer JS, grep for stale `#`-ids, manual visual QA at `npm run dev` if the user agrees to a foreground dev session.
112
+
113
+ ### Phase 3 — Migrate secondary consumer (typically a playground)
114
+
115
+ The "stripped" consumer. Same structural change minus persistence.
116
+
117
+ 1. Same markup substitution — **omit `[persist]`** so the playground stays ephemeral and doesn't leak state into the primary consumer on the same origin.
118
+ 2. Delete the controller blocks. Drop any `const html = document.documentElement;` that is now unused. Keep primitive imports needed by the module's stamped children (text-ui, divider-ui, slider-ui, field-ui, button-ui, etc.).
119
+ 3. Update the playground's `spec/SPEC.md` ASCII diagram to show the new tag.
120
+ 4. **Gate**: same as Phase 2.
121
+
122
+ ### Phase 4 — Migrate the chunk corpus
123
+
124
+ The harvested training-corpus chunk (`packages/a2ui/corpus/chunks/<name>.json`) embeds the consumer's HTML as an escaped string. Update it so the LLM sees the new authoring shape.
125
+
126
+ 1. **Preferred** — re-run the harvester:
127
+
128
+ ```bash
129
+ npm run harvest:chunks
130
+ ```
131
+
132
+ Confirm `SOURCES` in `scripts/build/harvest-chunks.mjs` includes the consumer's directory (post-ADR-0026 it should include `site/pages`, `apps`, `playgrounds`, `catalog`).
133
+
134
+ 2. **Fallback** — if the harvester source list excludes your consumer, edit the chunk JSON directly via a Python regex script. The pattern that worked for theme-panel:
135
+
136
+ ```python
137
+ pat = re.compile(
138
+ r'<button-ui id="theme-toggle"[^>]*></button-ui>\s*'
139
+ r'<popover-ui id="theme-popover"[^>]*>.*?</popover-ui>',
140
+ re.DOTALL,
141
+ )
142
+ ```
143
+
144
+ Refresh `captured_at` to today's ISO timestamp.
145
+
146
+ 3. **Defensive grep across all chunks** for the legacy markers:
147
+
148
+ ```bash
149
+ grep -l 'id="<old-id>"' packages/a2ui/corpus/chunks/*.json
150
+ ```
151
+
152
+ 4. **Gotcha — hand-edit + later re-harvest** — if you used the fallback (step 2) AND a peer later adds your consumer's directory to harvester SOURCES, the next `harvest:chunks` run will overwrite your hand-edit with the live consumer HTML. Usually that's _good_ (the re-harvest captures the live state, including any post-edit consumer updates), but diff before assuming. Concrete example: theme-panel's `playground-app-shell.json` was hand-edited, then re-harvested after `playgrounds/` was added to SOURCES — the re-harvest captured the `<aside-ui>` → `<admin-sidebar>` bespoke conversion that the hand-edit missed.
153
+ 5. **Gate**:
154
+
155
+ ```bash
156
+ npm run test:a2ui # 22 pass — chunk loads
157
+ npm run smoke:chunks # tolerate pre-existing failures
158
+ # Optional (real-LLM, ~$2 per full eval):
159
+ npm run eval:diff -- --engine zettel # cov ≥ 40, avg ≥ 85
160
+ ```
161
+
162
+ ### Phase 5 — Release
163
+
164
+ Either ship as a standalone lockstep cut (per the **adia-ui-release** skill, lockstep release mode), OR ride a bundled cut that the user is already coordinating. Either way:
165
+
166
+ 1. **CHANGELOGs** — `packages/web-modules/CHANGELOG.md` `[Unreleased]` (or the dated bump block if you're cutting now) + root `CHANGELOG.md`.
167
+ 2. **README** — `packages/web-modules/README.md`: cluster table row, Layout tree entry, Quick start `import` line.
168
+ 3. **Spec status** — `docs/specs/<name>-module.md` Status field flips `Draft — proposed` → `Active — shipped vN.M.P`, plus the phase-table tick.
169
+ 4. **Specs INDEX** — `docs/specs/INDEX.md` row status column.
170
+ 5. **Journal** — per the repo's journal discipline, every non-trivial phase lands a `## §N — <topic>` in the dated journal the same turn it commits — don't batch-write at release. Phase 5 adds a _closing_ `§N` covering the release itself (lockstep cut, deploy, what changed at the package boundary). The release section's commit SHAs + verification table close the arc; future archeology starts here.
171
+ 6. **Plan** — front-matter Status → `Completed — shipped`.
172
+
173
+ ## Cluster placement decision (OD-001)
174
+
175
+ | Option | When to pick |
176
+ | --- | --- |
177
+ | **A. New cluster** | The element is its own product axis; ≥ 2 credible siblings are foreseeable (e.g. `theme/` cluster: theme-panel today, theme-toggle + theme-preview tomorrow). Cost: 4 single-line edits (cluster `index.js`, root barrel, 3 `package.json` entries). |
178
+ | **B. Existing cluster** | The element fits a current family with no expansion plans. Pick the **most cross-cutting** cluster (`simple/`, `runtime/`) before a shell-specific one (`shell/`, `chat/`, `editor/`). |
179
+ | **C. Inside a shell cluster** | Almost never — fails if the element is consumed by _other_ shells. Use only when the element is structurally shell-bound. |
180
+ | **D. Promote to primitive** | Only if it composes < 3 other primitives. Most controls fail this test. |
181
+
182
+ ## Two rules from observed bugs
183
+
184
+ ### OD-002 — Absorb the standalone-button-next-door
185
+
186
+ When the original duplicated markup has a sibling button that conceptually belongs with the panel (e.g. `<button-ui #theme-toggle>` flipping `color-scheme` next to the `<popover-ui>` that contains the theme panel), **absorb it into the module behind a boolean attribute**, don't leave it next door. Two duplications collapse into zero.
187
+
188
+ Module shape:
189
+
190
+ ```html
191
+ <my-module scheme-toggle></my-module>
192
+ <!-- renders an internal <segmented-ui> at the top of the panel -->
193
+ ```
194
+
195
+ The sibling button retires from consumer markup. Programmatic alternative stays available via `.apply({scheme})`.
196
+
197
+ ### OD-003 — Read computed values back into controls
198
+
199
+ When the module owns sliders that mirror CSS custom-property values that a _theme_ (or other parent) sets, the user mental model is "the slider shows the current value, regardless of how it got there." So on theme-change:
200
+
201
+ ```js
202
+ target.style.removeProperty('--a-density'); // clear local override
203
+ target.style.removeProperty('--a-radius-k');
204
+ // theme [data-theme] block now takes effect
205
+
206
+ requestAnimationFrame(() => {
207
+ const cs = getComputedStyle(target);
208
+ this.#densityEl.value = parseFloat(cs.getPropertyValue('--a-density')) || 1;
209
+ this.#radiusEl.value = parseFloat(cs.getPropertyValue('--a-radius-k')) || 1;
210
+ });
211
+ ```
212
+
213
+ The `requestAnimationFrame` matters — CSS application is async with attribute write; reading on the next frame guarantees the computed value reflects the new theme block.
214
+
215
+ The bug this fixes: a stripped-down playground variant that resets sliders to `1, 1` on theme click. Sliders then lie about the current value (`1.5` is the actual radius for `[data-theme="ocean"]`).
216
+
217
+ ## Peer-agent boundary discipline (cluster-promotion-specific notes)
218
+
219
+ **General peer-agent + stale-context rules are canonical in the repo's AGENTS.md "Multi-agent baseline assumption" guidance.** Read those first: turn-start `git status` / `log` / `fetch` checklist, never-`git add -A`, explicit-allowlist staging, re-baseline after peer commits, doc-currency coordination, "Excluded — peer-agent in-flight" commit-message rule.
220
+
221
+ **Journal cadence** is canonical in the repo's AGENTS.md "Journal docs (maintain as you go)" guidance — append `## §N — <topic>` to today's journal + INDEX bullet the same turn each phase commits, not batched at release. The "journal-sN-handoff" note pattern is for genuinely-blocked writers only (the file is locked by a peer mid-arc); default to direct edits.
222
+
223
+ What follows are the **module-promotion-specific** extensions, not covered by the generic AGENTS.md guidance:
224
+
225
+ 1. **Catalog regeneration risk** — `node scripts/build/components.mjs` reads ALL yamls in `packages/web-components/components/` AND `packages/web-modules/`, so a regenerated aggregate corpus catalog will absorb any of the peer's uncommitted yaml changes (e.g. an in-flight `input-ui` rewrite). **Don't stage the aggregate catalog** if it would bundle peer work; let the next person to run the build regenerate it cleanly. Your per-component `<name>.a2ui.json` sidecar inside `packages/web-modules/<cluster>/<name>/` is sufficient on its own — that's what the per-component verify gate actually compares against.
226
+
227
+ 2. **Chunk re-harvest may improve on your hand-edit** — if a peer adds your consumer's directory to `scripts/build/harvest-chunks.mjs` SOURCES while your migration is in flight, the next harvester run will pick up the live consumer HTML. That's usually **strictly better** than your hand-edit because it captures any in-flight bespoke shape updates the consumer received (e.g. `<aside-ui>` → `<admin-sidebar>` per ADR-0024). When you see a peer harvester run land, diff your Phase 4 hand-edit against the re-harvested chunk — if the re-harvest captures the new tag and is structurally cleaner, your hand-edit is superseded and nothing more to do.
228
+
229
+ 3. **Module-promotion paths the peer may be editing** — the highest-overlap surfaces during a module-promotion arc are:
230
+ - `packages/web-modules/index.js` (the root barrel — multiple clusters may want to add their `export *` lines in the same session)
231
+ - `packages/web-modules/package.json` (exports/files/sideEffects — same)
232
+ - The CHANGELOG block for the in-flight release
233
+
234
+ Stage these explicitly file-by-file; check each diff before committing.
235
+
236
+ ## Path-rebase awareness
237
+
238
+ Plans authored before architectural reorgs reference stale paths. Before executing, **grep for plan path references against the live tree**:
239
+
240
+ ```bash
241
+ # Example: plan says apps/app-shell/; tree has playgrounds/admin-shell/
242
+ for p in $(grep -oE 'apps/[a-z-]+/' docs/plans/<plan>.md | sort -u); do
243
+ test -d "$p" || echo "STALE: $p"
244
+ done
245
+ ```
246
+
247
+ For ADR-0026 specifically: `apps/app-shell/` → `playgrounds/admin-shell/`; `apps/generic-shells/` → `apps/page-shells/`; `apps/patterns/` → `catalog/ui-patterns/`.
248
+
249
+ Note the rebased paths in your commit messages so future archeology is easy:
250
+
251
+ ```text
252
+ Path note: plan references 'apps/app-shell/' (pre-ADR-0026 paths);
253
+ actual path is 'playgrounds/admin-shell/' under the 3-tier
254
+ apps/playgrounds/catalog layout.
255
+ ```
256
+
257
+ ## Pitfalls
258
+
259
+ 1. **Skipping the `<name>.test.js` file.** Mandatory — peer references ([shell-patterns.md](shell-patterns.md)) demand a behavior test. Minimum coverage: stamp + connect, attribute reflection, primary user action, programmatic API, persistence on/off, disconnect cleanup. Target ≥ 15 specs; theme-panel ships 23.
260
+ 2. **Forgetting one of the 4 `package.json` touchpoints.** Add to `exports` only, leave `files` out → `npm publish` ships a broken package (the directory isn't in the tarball). Add to `files` only, leave `sideEffects` out → bundlers tree-shake the `customElements.define()` call and the element silently never registers in production builds. All four are required.
261
+ 3. **`[persist]` on by default.** Embedded demos silently mutate the docs shell's stored preferences (same origin). Default to ephemeral; flip `[persist]` on per-consumer.
262
+ 4. **Editing the aggregate catalog by hand.** It's generated. Edit the `<name>.yaml` SoT and run `npm run build:components`.
263
+ 5. **Bundling the LS-key shim with no removal date.** Future you won't remember to remove it. Always add `TODO(YYYY-MM+60d): remove …` near the shim.
264
+ 6. **Promoting to a primitive instead of a module.** If the element composes existing primitives (button, slider, field, popover, etc.), it's a module per ADR-0012. Primitives are atomic by contract.
265
+
266
+ ## Verification checklist
267
+
268
+ - [ ] All 5 phases land as separate commits (revertible)
269
+ - [ ] `node scripts/build/components.mjs --verify` clean (N+1 files vs baseline)
270
+ - [ ] `<name>.test.js` ≥ 15 specs passing
271
+ - [ ] `npm run test:a2ui` 22 pass / 0 fail / 1 skipped
272
+ - [ ] `npm run check:lockstep` green (if you cut a release)
273
+ - [ ] All 4 `package.json` touchpoints + cluster barrel + root barrel updated
274
+ - [ ] Spec status flipped Draft → Active in same arc
275
+ - [ ] CHANGELOGs (package + root) + README + journal updated
276
+ - [ ] No stale `#<old-id>` references in consumers (`grep -nE 'id="<old-prefix>-'`)
277
+ - [ ] LS-key shim has a removal TODO date (if applicable)
278
+ - [ ] Peer agent's files NOT staged in your commits
279
+
280
+ ## Cross-references
281
+
282
+ - [shell-patterns.md](shell-patterns.md) — mode 3 (bespoke shell tier decomposition, ADR-0023; the _different_ activity)
283
+ - [authoring-cycle.md](authoring-cycle.md) — the standard 5-step authoring cycle (mode 1 / mode 2)
284
+ - [api-contract.md](api-contract.md) — prop naming, reflection, the legacy forms
285
+ - **adia-ui-release** (sibling skill) — lockstep release cycle (Phase 5)
286
+ - **adia-ui-a2ui** (sibling skill) — chunk corpus harvest + smoke gates (Phase 4)
287
+ - Spec: `docs/specs/package-architecture.md` — three-tier package layout
288
+ - ADR-0012 — primitives vs modules vs themes
289
+ - ADR-0023 — bespoke shell pattern (mode 3 reference)