@adia-ai/gen-ui 0.8.62 → 0.8.64

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/CHANGELOG.md CHANGED
@@ -1,5 +1,41 @@
1
1
  # Changelog — @adia-ai/gen-ui
2
2
 
3
+ ## [0.8.64] — 2026-09-13
4
+
5
+ ### Fixed
6
+
7
+ - fix: generateUIStream() now wires currentCanvas through to buildCanvasDiffPrompt() the same way generatePro() does, instead of discarding it, so a thinking-mode iteration turn diffs against the prior canvas instead of regenerating fresh every time (ticket 10032) (local ticket 10032)
8
+ - fix: generateUIStream() now merges its diff-shaped LLM response against the prior canvas via mergeCanvasDiff before repair, instead of treating the diff as the complete canvas, so components the LLM left out of a partial-diff response above the 300-component threshold are carried forward instead of silently dropped, matching generatePro's mergeCanvasDiffDetailed behavior (ticket 10050) (local ticket 10050)
9
+
10
+ ### Maintenance
11
+ - **`compose/` touched in this release window** (2 file(s), e.g. `core/generator.js`): carried by the entries above.
12
+ - **`corpus/` touched in this release window** (1 file(s), e.g. `chunks/_source-hashes.json`): carried by the entries above.
13
+
14
+ ## [0.8.63] — 2026-09-12
15
+
16
+ ### Changed
17
+
18
+ - chore: BREAKING, remove the deprecated `./compose/evals` export alias (ticket 4299, ADR-0098 retirement path) (#4386)
19
+ The one-release deprecation window LLD-3751 opened for the `@adia-ai/gen-ui/compose/evals`
20
+ subpath alias (introduced by commit `76789d172`, first carried by `0.8.61`, after `0.8.60`
21
+ shipped with the original non-aliased path) has closed: `0.8.62`, the release after `0.8.61`,
22
+ is published. The `./compose/evals` export entry is removed from the engine
23
+ `package.json`'s `exports` map outright, no successor alias; `@adia-ai/gen-ui/evals` is the
24
+ sole subpath. This completes the MIGRATION GUIDE lifecycle LLD-3751 started rather than
25
+ opening a new one: `.claude/docs/MIGRATION GUIDE.md` gains a "Shipped" entry documenting the
26
+ closure (the original alias-introduction entry stays as historical record, matching this
27
+ repo's own admin-page/admin-scroll ADR-0098 removal precedent), and its generated
28
+ `packages/web-components/MIGRATION.md` copy is resynced. No in-repo consumer still imported
29
+ the old subpath.
30
+
31
+ ### Fixed
32
+
33
+ - fix: core/generator.js's streaming path now substitutes fallbackMessage() when validateSchema flags isRawText, matching generate-pro.js/generate-thinking.js, so a raw-text leak surviving the repair loop on that path no longer reaches the canvas verbatim (ticket 10028) (local ticket 10028)
34
+ - fix: every producer strategy (zettel, chunk-zettel, free-form-composer, monolithic) now imports one shared `SURFACE_ID` constant instead of three disagreeing literals (`'default'`, `'main'`, or none at all), closing a hole where a zero-named-surface or mismatched-surfaceId doc fell outside a2ui-root's single-surface inference and tore down its own throwaway surface, taking `<a2ui-root>` itself out of the DOM (ticket 10037) (local ticket 10037)
35
+
36
+ ### Maintenance
37
+ - **`corpus/` touched in this release window** (2 file(s), e.g. `chunks/_output-hashes.json`): carried by the entries above.
38
+
3
39
  ## [0.8.62] — 2026-09-10
4
40
 
5
41
  ### Changed
@@ -38,6 +38,7 @@ import {
38
38
  buildRepairPrompt,
39
39
  generateSuggestions,
40
40
  foldInAntiPatterns,
41
+ fallbackMessage,
41
42
  } from '../strategies/monolithic/_shared.js';
42
43
  import { validateAndRepair } from '../shared/validate-and-repair.js';
43
44
  import { generateInstant } from '../strategies/monolithic/generate-instant.js';
@@ -269,11 +270,19 @@ export async function generateUI({ intent, engine: engineName = 'monolithic', mo
269
270
  * @yields {object}
270
271
  */
271
272
  export async function* generateUIStream({ intent, executionId, llmAdapter, model, search, currentCanvas, context, signal }) {
272
- // currentCanvas accepted for API parity with generateUI. The streaming
273
- // path's downstream consumers don't yet read it, but parity prevents the
274
- // client from having to branch its call shape per mode. Tier-2 will
275
- // wire it through the same way generatePro does.
276
- void currentCanvas;
273
+ // ticket 10032: normalize the client-owned canvas payload into a flat
274
+ // prior-components array, the same shape collapse generateUI() does
275
+ // (above) before dispatching to generatePro. Pre-fix this path discarded
276
+ // `currentCanvas` entirely (`void currentCanvas;`), so thinking-mode
277
+ // iteration never diffed against the prior canvas at all.
278
+ let priorComponentsFromPayload = null;
279
+ if (currentCanvas) {
280
+ if (Array.isArray(currentCanvas.components)) {
281
+ priorComponentsFromPayload = currentCanvas.components;
282
+ } else if (Array.isArray(currentCanvas.messages)) {
283
+ priorComponentsFromPayload = currentCanvas.messages.flatMap((m) => m?.components || []);
284
+ }
285
+ }
277
286
  // ── Intent gate: reject non-UI prompts before entering the pipeline ──
278
287
  // Same iteration bypass as generateUI() — modifiers don't need to clear the
279
288
  // conversational gate when there's a prior turn to refine.
@@ -479,7 +488,19 @@ export async function* generateUIStream({ intent, executionId, llmAdapter, model
479
488
 
480
489
  // ── Stage 4: Generate via LLM stream ──
481
490
  const systemPrompt = await buildSystemPrompt(catalogContext, patterns, researchContext, intent, null, context);
482
- const chatMessages = buildChatMessages(intent, previousExecId || executionId);
491
+ // ticket 10032: when the client sent a canvas payload, diff against it
492
+ // (same helper generatePro uses) instead of the legacy store-lookup path
493
+ // buildChatMessages() takes: the payload is the source of truth (it may
494
+ // be running stateless, across a server restart, or in a different tab).
495
+ const hasPriorCanvasFromPayload = Array.isArray(priorComponentsFromPayload) && priorComponentsFromPayload.length > 0;
496
+ const chatMessages = hasPriorCanvasFromPayload
497
+ ? [{
498
+ role: 'user',
499
+ content: buildCanvasDiffPrompt(intent, priorComponentsFromPayload, {
500
+ originalIntent: store.getOriginalIntent(previousExecId || executionId) || null,
501
+ }),
502
+ }]
503
+ : buildChatMessages(intent, previousExecId || executionId);
483
504
 
484
505
  yield { type: 'status', stage: 'generate', message: 'Streaming from LLM...' };
485
506
 
@@ -516,6 +537,23 @@ export async function* generateUIStream({ intent, executionId, llmAdapter, model
516
537
 
517
538
  // ── Stage 5: Parse final + validate ──
518
539
  let messages = parseA2UIResponse(fullText, { executionId, mode: 'stream', intent });
540
+
541
+ // ticket 10050: above the 300-component threshold, buildCanvasDiffPrompt
542
+ // (monolithic/_shared.js) switches to asking for a sparse diff instead of
543
+ // the full canvas, the same threshold generatePro uses. Unlike generatePro
544
+ // (generate-pro.js's own mergeCanvasDiffDetailed call), this path never
545
+ // reconstituted the diff against the prior canvas, so components the LLM
546
+ // left out of the diff were silently dropped instead of carried forward.
547
+ // Merge here, before repair, so validation/storage below see the same
548
+ // full canvas generatePro would produce.
549
+ if (hasPriorCanvasFromPayload && messages && messages.length > 0) {
550
+ for (const msg of messages) {
551
+ if (msg.type === 'updateComponents' && msg.components) {
552
+ msg.components = mergeCanvasDiff(priorComponentsFromPayload, msg.components);
553
+ }
554
+ }
555
+ }
556
+
519
557
  engine.submitStage(executionId, 'generate', {
520
558
  messages, source: 'llm-stream', confidence: 0.8,
521
559
  });
@@ -551,6 +589,17 @@ export async function* generateUIStream({ intent, executionId, llmAdapter, model
551
589
  });
552
590
  messages = repairResult.messages;
553
591
  let validation = repairResult.validation;
592
+ // ticket 10028: this streaming path parses raw LLM text via
593
+ // parseA2UIResponse the same way generate-pro.js/generate-thinking.js do,
594
+ // so it is exposed to the same raw-text-leak surface `isRawText` flags,
595
+ // but unlike those two engines, it never substituted fallbackMessage(),
596
+ // so a leak surviving the repair loop here would have reached the wire
597
+ // verbatim instead of the same honest failure card every other engine
598
+ // shows.
599
+ if (validation.isRawText) {
600
+ messages = fallbackMessage('Generation produced unstructured raw text instead of a UI', { executionId, intent, mode: 'stream' });
601
+ validation = validateSchema(messages, { intent, context });
602
+ }
554
603
  if (repairResult.attempts > 0) {
555
604
  yield { type: 'status', stage: 'validate', message: `Score ${validation.score}/100 — repaired in ${repairResult.attempts} attempt(s)` };
556
605
  }
@@ -0,0 +1,19 @@
1
+ // gh#10037: the ONE canonical surfaceId every gen-ui producer strategy must
2
+ // target for its single-surface output. Before this module existed, three
3
+ // different literals were in play across the roster:
4
+ // - monolithic (generate-instant.js, _shared.js): 'default'
5
+ // - zettel's chunk-refiner.js (the chunk-zettel iteration path): 'main'
6
+ // - free-form-composer/transpile.js: 'main'
7
+ // - zettel's generator-adapter.js (pre-fix): no surfaceId at all
8
+ //
9
+ // A doc's messages must agree on which surface they target across turns:
10
+ // handleAuto.js reads the PRIOR turn's own surfaceId back off
11
+ // state.allMessages to decide whether to bracket a regenerate
12
+ // (beginSurfaceUpdate) or fall back to a full replaceDoc. A mismatched or
13
+ // missing surfaceId between two turns' engines defeats that entirely: the
14
+ // new doc never declares the surface the prior turn created, so
15
+ // a2ui-root.js's replaceDoc() tears the prior surface down as undeclared
16
+ // (packages/gen-ui/a2ui/renderer.js's #deleteSurface). One shared constant,
17
+ // imported everywhere a producer strategy builds an A2UI message, closes
18
+ // that gap for every present and future tier at once.
19
+ export const SURFACE_ID = 'default';
@@ -53,6 +53,8 @@
53
53
  * - Structural substitutions (swap Card → Section, etc.).
54
54
  */
55
55
 
56
+ import { SURFACE_ID } from '../_shared/surface-id.js';
57
+
56
58
  const VALID_LAYOUTS = new Set(['column', 'row', 'grid']);
57
59
 
58
60
  /**
@@ -190,7 +192,7 @@ export function transpilePlan(plan, chunkLookup) {
190
192
  return {
191
193
  messages: [{
192
194
  type: 'updateComponents',
193
- surfaceId: 'main',
195
+ surfaceId: SURFACE_ID,
194
196
  components: [root, ...flatComponents],
195
197
  }],
196
198
  warnings,
@@ -16,6 +16,7 @@ import { composeSubtasks } from '../../../retrieval/intent/decomposer.js';
16
16
  import { getWiringCatalog } from '../../../retrieval/wiring-catalog.js';
17
17
  import { getComponentData } from '../../../retrieval/component-catalog.js';
18
18
  import { assessIterationFidelity } from '../../shared/iteration-fidelity.js';
19
+ import { SURFACE_ID } from '../_shared/surface-id.js';
19
20
 
20
21
  // ── v0.9 .a2ui.json sidecar → prompt-catalog adapter ─────────────
21
22
  //
@@ -1217,14 +1218,14 @@ function wrapAsMessages(parsed) {
1217
1218
  if (parsed.length > 0 && validTypes.has(parsed[0].type)) return parsed;
1218
1219
  // Bare components array
1219
1220
  if (parsed.length > 0 && parsed[0].id && parsed[0].component) {
1220
- return [{ type: 'updateComponents', surfaceId: 'default', components: parsed }];
1221
+ return [{ type: 'updateComponents', surfaceId: SURFACE_ID, components: parsed }];
1221
1222
  }
1222
1223
  return parsed;
1223
1224
  }
1224
1225
  if (parsed && typeof parsed === 'object' && parsed.type === 'updateComponents') return [parsed];
1225
1226
  if (parsed && typeof parsed === 'object' && parsed.type === 'wireComponents') return [parsed];
1226
1227
  if (parsed && typeof parsed === 'object' && parsed.id && parsed.component) {
1227
- return [{ type: 'updateComponents', surfaceId: 'default', components: [parsed] }];
1228
+ return [{ type: 'updateComponents', surfaceId: SURFACE_ID, components: [parsed] }];
1228
1229
  }
1229
1230
  return null;
1230
1231
  }
@@ -1531,7 +1532,7 @@ export function fallbackMessage(reason, opts = {}) {
1531
1532
 
1532
1533
  const updateMsg = {
1533
1534
  type: 'updateComponents',
1534
- surfaceId: 'default',
1535
+ surfaceId: SURFACE_ID,
1535
1536
  // Marker so validators / scorers / consumers can detect that this is a
1536
1537
  // generation-failure surface (NOT a successful UI). Without this, the
1537
1538
  // validator was scoring fallbacks ~89/100 because they're structurally
@@ -1558,7 +1559,7 @@ export function fallbackMessage(reason, opts = {}) {
1558
1559
 
1559
1560
  const wireMsg = {
1560
1561
  type: 'wireComponents',
1561
- surfaceId: 'default',
1562
+ surfaceId: SURFACE_ID,
1562
1563
  actions,
1563
1564
  };
1564
1565
 
@@ -12,6 +12,7 @@ import { assessClarity } from '../../../retrieval/intent/clarity.js';
12
12
  import { feedbackStore } from '../../../retrieval/feedback/feedback-store.js';
13
13
  import { store, engine } from '../../core/state.js';
14
14
  import { isRecording } from '../../../retrieval/feedback/dialog-recorder.js';
15
+ import { SURFACE_ID } from '../_shared/surface-id.js';
15
16
  import {
16
17
  generateSuggestions, foldInAntiPatterns } from './_shared.js';
17
18
 
@@ -156,7 +157,7 @@ export async function generateInstant({ intent, executionId, storeId, analysis,
156
157
  const messages = [
157
158
  {
158
159
  type: 'updateComponents',
159
- surfaceId: 'default',
160
+ surfaceId: SURFACE_ID,
160
161
  components,
161
162
  ...(isPlaceholderStub ? {
162
163
  _fallback: true,
@@ -169,7 +170,7 @@ export async function generateInstant({ intent, executionId, storeId, analysis,
169
170
  if (bestMatch?.wiring) {
170
171
  messages.push({
171
172
  type: 'wireComponents',
172
- surfaceId: 'default',
173
+ surfaceId: SURFACE_ID,
173
174
  ...(bestMatch.wiring.data_sources ? { data: { sources: bestMatch.wiring.data_sources } } : {}),
174
175
  ...(bestMatch.wiring.controllers ? { state: { controllers: bestMatch.wiring.controllers } } : {}),
175
176
  ...(bestMatch.wiring.actions ? { actions: bestMatch.wiring.actions } : {}),
@@ -18,6 +18,11 @@
18
18
  * { executionId, messages, validation, suggestions?, strategy?, ... }
19
19
  */
20
20
 
21
+ // Pure constant, no node:fs/node:path deps, safe to static-import even
22
+ // though the strategies that use it (zettel, chunk-zettel) are lazy-loaded
23
+ // below for browser-safety reasons that don't apply to this module.
24
+ import { SURFACE_ID } from './_shared/surface-id.js';
25
+
21
26
  // Zettel is lazy-loaded — it transitively imports node:fs / node:path / node:url
22
27
  // (strategies/zettel/composition-library.js), which Vite externalizes in the browser.
23
28
  // Static-importing it here would break browser loads of core/generator.js.
@@ -324,10 +329,15 @@ async function generateChunkZettelAdapter(ctx) {
324
329
  // use it directly; the pre-fix fallback (a single node carrying an
325
330
  // UNREGISTERED `component: 'article'` type) only fires if transpilation
326
331
  // itself somehow produced nothing, which should not happen in practice.
332
+ // gh#10037: this adapter's two branches used to omit `surfaceId`
333
+ // entirely, the same defect fixed in zettel/generator-adapter.js: a
334
+ // doc naming zero surfaces falls outside a2ui-root.js's single-surface
335
+ // inference and tears down its own throwaway surface on regenerate,
336
+ // taking the <a2ui-root> host element with it.
327
337
  const messages = result.template
328
- ? [{ type: 'updateComponents', components: result.template }]
338
+ ? [{ type: 'updateComponents', surfaceId: SURFACE_ID, components: result.template }]
329
339
  : result.html
330
- ? [{ type: 'updateComponents', components: [{ id: 'chunk-root', component: 'article', html: result.html }] }]
340
+ ? [{ type: 'updateComponents', surfaceId: SURFACE_ID, components: [{ id: 'chunk-root', component: 'article', html: result.html }] }]
331
341
  : [];
332
342
 
333
343
  // Full schema/catalog/anti-pattern conformance — the same final pass
@@ -27,10 +27,10 @@ import {
27
27
  searchChunksAsync,
28
28
  } from '../../../corpus/scripts/chunk-library.js';
29
29
  import { composeFromPlan } from './chunk-composer.js';
30
+ import { SURFACE_ID } from '../_shared/surface-id.js';
30
31
 
31
32
  const DEFAULT_MAX_ATTEMPTS = 2;
32
33
  const PRE_SEARCH_LIMIT = 30;
33
- const SURFACE_ID = 'main';
34
34
 
35
35
  const VALID_OP_TYPES = new Set(['rebindSlot', 'appendToSlot', 'removeFromSlot', 'replacePage']);
36
36
 
@@ -1,11 +1,11 @@
1
1
  /**
2
- * Generator adapter for zettel MCP conforms to the eval harness contract:
2
+ * Generator adapter for zettel MCP: conforms to the eval harness contract:
3
3
  *
4
4
  * generate({ intent, mode, llmAdapter, sessionId, signal }) -> { messages, validation, strategy, ...extra }
5
5
  *
6
6
  * Two reasoning layers (post-§195, v0.5.6):
7
- * 1. Retrieval (always available) keyword-rank the corpus, resolve top composition.
8
- * 2. LLM synthesis (when llmAdapter provided AND retrieval weak) bridge to
7
+ * 1. Retrieval (always available) , keyword-rank the corpus, resolve top composition.
8
+ * 2. LLM synthesis (when llmAdapter provided AND retrieval weak) , bridge to
9
9
  * chunk-zettel's `composeFromIntent` (the §37 successor codepath) which
10
10
  * produces a single-shot html composition from the chunk corpus.
11
11
  *
@@ -13,19 +13,19 @@
13
13
  * are recorded for analytics + drift tracking, but turn≥2 takes the same code
14
14
  * path as turn 1 (fresh retrieval → fresh synthesis). Full history-aware
15
15
  * iteration (multi-turn refinement modifying an existing canvas) lives in the
16
- * `chunk-zettel` engine via `chunk-refiner.js::refineFromIntent` wire it via
16
+ * `chunk-zettel` engine via `chunk-refiner.js::refineFromIntent` , wire it via
17
17
  * `engine: 'chunk-zettel'` rather than `engine: 'zettel'`.
18
18
  *
19
19
  * The prior fragment-graph iteration codepath (`synthesizeComposition` with
20
20
  * `historySummary`) was retired in §37 (2026-05-12) when fragments retired.
21
- * §195 (v0.5.6) cleaned up the dangling caller turn≥2 no longer throws +
21
+ * §195 (v0.5.6) cleaned up the dangling caller , turn≥2 no longer throws +
22
22
  * auto-fires a `iteration-synthesis-failure` issue on every multi-turn turn.
23
23
  *
24
24
  * Strategy labels in the return:
25
- * - composition-match fresh retrieval, strong match, emitted verbatim
26
- * - composition-synthesized chunk-zettel single-shot synthesis
27
- * - fragment-candidates retrieval weak + no LLM, returning atoms only
28
- * - synthesis-failed chunk-zettel tried and failed validation
25
+ * - composition-match , fresh retrieval, strong match, emitted verbatim
26
+ * - composition-synthesized , chunk-zettel single-shot synthesis
27
+ * - fragment-candidates , retrieval weak + no LLM, returning atoms only
28
+ * - synthesis-failed , chunk-zettel tried and failed validation
29
29
  */
30
30
  import {
31
31
  getComposition,
@@ -38,6 +38,7 @@ import {
38
38
  getTurns,
39
39
  } from './session-store.js';
40
40
  import { validateSchema } from '@adia-ai/a2ui/validate';
41
+ import { SURFACE_ID } from '../_shared/surface-id.js';
41
42
 
42
43
  // Lazy-load the chunk-synthesizer bridge. It imports chunk-corpus data
43
44
  // via composition-library's dual-mode loader, which is already top-level
@@ -51,13 +52,13 @@ async function bridgeToChunkSynthesis({ intent, llmAdapter, signal }) {
51
52
  // to the synthesis-branch's prior `{ messages, template, synthesis }`
52
53
  // contract. TKT-0009: prefer the real component array (`result.template`)
53
54
  // over the pre-fix fallback of wrapping raw HTML in a single node with an
54
- // UNREGISTERED `component: 'article'` type that fallback made this
55
+ // UNREGISTERED `component: 'article'` type , that fallback made this
55
56
  // caller's validate-and-repair pass below structurally unable to ever
56
57
  // report a clean pass.
57
58
  const messages = result.template
58
- ? [{ type: 'updateComponents', components: result.template }]
59
+ ? [{ type: 'updateComponents', surfaceId: SURFACE_ID, components: result.template }]
59
60
  : result.html
60
- ? [{ type: 'updateComponents', components: [{ id: 'chunk-root', component: 'article', html: result.html }] }]
61
+ ? [{ type: 'updateComponents', surfaceId: SURFACE_ID, components: [{ id: 'chunk-root', component: 'article', html: result.html }] }]
61
62
  : [];
62
63
  return {
63
64
  messages,
@@ -74,7 +75,7 @@ async function bridgeToChunkSynthesis({ intent, llmAdapter, signal }) {
74
75
  // Composition library auto-loads at module import time via top-level `await
75
76
  // loadAll()` in composition-library.js (§72 dual-mode loader, commit
76
77
  // `76dbcff2`). The previous `ensureBooted()` here called `loadAll()` WITHOUT
77
- // `await`, which after §72 made it async clearing the compositions map +
78
+ // `await`, which after §72 made it async , clearing the compositions map +
78
79
  // racing the synchronous `searchAll` below. Symptom (caught in §87):
79
80
  // eval:diff zettel coverage dropped 5% → 1% because the race-winner pattern
80
81
  // emitted composition-match only for the intent whose `searchAll` happened
@@ -83,7 +84,7 @@ async function bridgeToChunkSynthesis({ intent, llmAdapter, signal }) {
83
84
  // Fix: rely entirely on the top-level await. Tests that want a forced reload
84
85
  // can call `loadAll()` directly with `await` (it's idempotent).
85
86
 
86
- // Retrieval score threshold above this we trust the match and emit verbatim;
87
+ // Retrieval score threshold , above this we trust the match and emit verbatim;
87
88
  // below, fall through to LLM synthesis (creative composition from fragments).
88
89
  // Calibrated on the 100-intent held-out set:
89
90
  // strong matches: login-form=24 (exact), signup-form=27, chart-dashboard=48, pricing-tiers=54
@@ -95,9 +96,23 @@ async function bridgeToChunkSynthesis({ intent, llmAdapter, signal }) {
95
96
  // threshold maximized verbatim-cache hits at the cost of repetitive output.
96
97
  const STRONG_MATCH_THRESHOLD = 40;
97
98
 
99
+ // gh#10037: every emitted `updateComponents` message must name a `surfaceId`
100
+ // (the shared `SURFACE_ID` imported above, so every tier agrees across
101
+ // turns). Without it, a2ui-root.js's replaceDoc() has zero named
102
+ // surfaceIds to infer a sole surface from (its inference only covers the
103
+ // single-named-surface case, not zero), so the message ends up applied
104
+ // against surfaceId `undefined`, renderer.js synthesizes a throwaway
105
+ // surface rooted at the <a2ui-root> element itself for that write, and
106
+ // replaceDoc's own tear-down sweep then deletes it as undeclared, which
107
+ // removes a2ui-root from the DOM (renderer.js's `deleteSurface` calls
108
+ // `surface.root.remove()`). This is the same class of bug as gh#2426's
109
+ // fix in a2ui-root.js, reintroduced here on the zettel producer side of
110
+ // the contract instead of the consumer side.
111
+
98
112
  function toUpdateComponentsMessages(template) {
99
113
  return [{
100
114
  type: 'updateComponents',
115
+ surfaceId: SURFACE_ID,
101
116
  components: template.map((n) => {
102
117
  const {
103
118
  id, component, children,
@@ -114,12 +129,12 @@ export async function generateZettel({ intent, mode = 'instant', llmAdapter = nu
114
129
  // ── Session iteration note (post-§195, v0.5.6) ──
115
130
  // Fragment-graph history-aware iteration (`synthesizeComposition` with
116
131
  // `historySummary`) was retired in §37 when fragments retired. Turn≥2 now
117
- // takes the same path as turn 1 fresh retrieval → fresh chunk-synthesis.
132
+ // takes the same path as turn 1 , fresh retrieval → fresh chunk-synthesis.
118
133
  // For true history-aware iteration (modify-an-existing-canvas), wire the
119
134
  // request via `engine: 'chunk-zettel'` which uses `chunk-refiner.js`.
120
135
  //
121
136
  // Prior turns are still recorded (analytics + drift tracking via `getDrift`)
122
- // but the iteration BRANCH is gone no more spurious
137
+ // but the iteration BRANCH is gone , no more spurious
123
138
  // `iteration-synthesis-failure` auto-fires on every multi-turn request.
124
139
  const priorTurns = sessionId ? getTurns(sessionId) : [];
125
140
 
@@ -127,10 +142,10 @@ export async function generateZettel({ intent, mode = 'instant', llmAdapter = nu
127
142
  const composition = hits.find((h) => h.type === 'composition');
128
143
  const strongMatch = composition && composition.score >= STRONG_MATCH_THRESHOLD;
129
144
 
130
- // ── Strong retrieval match: emit verbatim (turn 1 only iteration branch above handles repeats) ──
145
+ // ── Strong retrieval match: emit verbatim (turn 1 only , iteration branch above handles repeats) ──
131
146
  if (strongMatch) {
132
147
  // On a static deploy the composition was registered from `_index.json`
133
- // for SCORING only its `template` is fetched lazily here, right before
148
+ // for SCORING only , its `template` is fetched lazily here, right before
134
149
  // we resolve it. No-op in Node / Vite (template already populated). This
135
150
  // keeps the static page-load to a single index request; only the
136
151
  // actually-emitted match pays for its body. 2026-06-10.
@@ -142,7 +157,7 @@ export async function generateZettel({ intent, mode = 'instant', llmAdapter = nu
142
157
  // is looser (it can't see templates in `_index.json`), so a handful of
143
158
  // body-less partials (auth-card-header, onb-step-*, …) COULD surface as a
144
159
  // strong match on a static deploy. If hydration produced no usable
145
- // template, treat it as a non-match and fall through keeping the
160
+ // template, treat it as a non-match and fall through , keeping the
146
161
  // static result identical to what Node would emit (synthesis / atoms),
147
162
  // never an empty verbatim surface.
148
163
  if (!Array.isArray(template) || template.length === 0) {
@@ -179,7 +194,7 @@ export async function generateZettel({ intent, mode = 'instant', llmAdapter = nu
179
194
  if (llmAdapter && mode !== 'instant-only') {
180
195
  try {
181
196
  const synth = await bridgeToChunkSynthesis({ intent, llmAdapter, signal });
182
- // Full schema/catalog/anti-pattern conformance (validate-only see
197
+ // Full schema/catalog/anti-pattern conformance (validate-only , see
183
198
  // validate-and-repair.js; TKT-0009 made this meaningful for the first
184
199
  // time by giving synth.messages a real component graph instead of an
185
200
  // opaque HTML wrapper).
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schema": "genui-chunk-index@2",
3
- "captured_at": "2026-09-09T16:07:52.181Z",
3
+ "captured_at": "2026-09-13T22:07:39.419Z",
4
4
  "total_instances": 746,
5
5
  "unique_names": 591,
6
6
  "by_kind": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schema": "genui-chunk-output-hashes@1",
3
3
  "algo": "sha256",
4
- "capturedAt": "2026-09-10T23:55:57.033Z",
4
+ "capturedAt": "2026-09-13T22:07:39.419Z",
5
5
  "aggregateDigest": "9eaca6f62e9dc45f78ebb2d22889b53071405a51a9bcd4410c737f076e57e8d3",
6
6
  "harvestEnv": {
7
7
  "nodeMajor": "24",
@@ -283,7 +283,7 @@
283
283
  "catalog/ui-patterns/v050-marketing-blocks/v050-marketing-blocks.html": "7833b21ee84643978cd3ca0e6ff20caa20ec8cbc9a55e371aa295a532f8ed48a",
284
284
  "catalog/ui-patterns/v050-nav-blocks/v050-nav-blocks.contents.html": "86f492db19b8aafc2bf25620f52377cd7a6cb92cfa2d0d3bc9dd4c5ade3e88c6",
285
285
  "catalog/ui-patterns/v050-nav-blocks/v050-nav-blocks.html": "196862f06f495547d85f542e6a0eee152ab40a40ca969c93b0cbd0733de5a5e2",
286
- "packages/gen-ui/a2ui/catalog/tier-index.json": "e373144f8bc693ed6cba3d910352b6513a8bd948e7383331bceb5d151c74d815",
286
+ "packages/gen-ui/a2ui/catalog/tier-index.json": "850ec77e8b2f33ea9756e22e27976cd1011ccce618d92b7e9ecf0461501302bd",
287
287
  "packages/web-components/components/accordion/accordion.examples.html": "0218a098bc9db38f6ad5a73b7fbbd8755882d92f47790488928384b79b2e7868",
288
288
  "packages/web-components/components/action-list/action-list.examples.html": "abcb4897fc8143a4c32b15bf82eb3340304cfa22d8498198328c1570102d7bc2",
289
289
  "packages/web-components/components/adia-mark/adia-mark.examples.html": "2124ad37660d157e7a85fdc57eb7725ac02d629a76a47a58ff53703ed5b06238",
@@ -611,7 +611,7 @@
611
611
  "site/pages/getting-started/usage.html": "23f477ad6e505876e36d6c774cd49acc3bb483f7e4872ea8d6d5261fdab02b4f",
612
612
  "site/pages/guides/agent-skills.html": "647864bb9c6e43ed123c95b48201f3e55a5bb26e8a77cd0ee65ad361df47bd97",
613
613
  "site/pages/guides/bundle-loading.html": "dc08497d9621c090e1581289ebf9a706ae05421a55dc9f20841c107969b3b829",
614
- "site/pages/guides/bundle-sizes.html": "1f649468e9b5be807bec4e72d2898a6f71b779346b483b802ab73eb4811d16a8",
614
+ "site/pages/guides/bundle-sizes.html": "b8b067e5e3c89bfb6ea809a971a5a7d13816b27f550a8a9e391dafa2d4421551",
615
615
  "site/pages/guides/creating-components.html": "f34020a12d91a0239b6303e32b3c39ecc47d7a96ccfa7ff10fc12e313065a5d6",
616
616
  "site/pages/guides/css-architecture.html": "42b4258cefc5f98d3b1297362aa090fe1310b92032871fc3144b33a42f03f331",
617
617
  "site/pages/guides/data-flow.html": "c1965674271df66cfebf95eb012048996fc8c844179fcb1816d49aae98621219",
@@ -23,7 +23,7 @@
23
23
  "agent"
24
24
  ]
25
25
  },
26
- "captured_at": "2026-09-07T13:06:17.368Z",
26
+ "captured_at": "2026-09-13T22:07:39.419Z",
27
27
  "template": [
28
28
  {
29
29
  "id": "text",
@@ -1,5 +1,5 @@
1
1
  {
2
- "timestamp": "2026-09-09T16:07:52.181Z",
2
+ "timestamp": "2026-09-13T22:07:39.419Z",
3
3
  "pages": 221,
4
4
  "chunks": 591,
5
5
  "skipped": 1680
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adia-ai/gen-ui",
3
- "version": "0.8.62",
3
+ "version": "0.8.64",
4
4
  "description": "AdiaUI generative-UI system — compose strategies, retrieval, the harvested training corpus, and catalog-aware + LLM-judge validation. Emits A2UI protocol messages; pairs with @adia-ai/a2ui (the protocol runtime). Folded from @adia-ai/a2ui-{compose,retrieval,corpus} + the catalog/semantic halves of the former @adia-ai/a2ui-validator (ADR-0048).",
5
5
  "type": "module",
6
6
  "main": "./compose/index.js",
@@ -45,11 +45,6 @@
45
45
  "import": "./compose/transpiler/transpiler.js",
46
46
  "default": "./compose/transpiler/transpiler.js"
47
47
  },
48
- "./compose/evals": {
49
- "types": "./evals/harness.d.ts",
50
- "import": "./evals/harness.mjs",
51
- "default": "./evals/harness.mjs"
52
- },
53
48
  "./evals": {
54
49
  "types": "./evals/harness.d.ts",
55
50
  "import": "./evals/harness.mjs",