@adia-ai/a2ui-compose 0.1.0 → 0.2.1

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 (31) hide show
  1. package/CHANGELOG.md +97 -1
  2. package/README.md +12 -8
  3. package/{engine → core}/generator.js +13 -13
  4. package/{engine → core}/state.js +2 -2
  5. package/index.js +4 -4
  6. package/package.json +17 -14
  7. package/{engines → strategies}/monolithic/_shared.js +9 -9
  8. package/{engines → strategies}/monolithic/generate-instant.js +8 -8
  9. package/{engines → strategies}/monolithic/generate-pro.js +10 -10
  10. package/{engines → strategies}/monolithic/generate-thinking.js +8 -8
  11. package/{engines → strategies}/registry.js +6 -6
  12. package/{engines → strategies}/zettel/chunk-synthesizer.js +121 -5
  13. package/{engines → strategies}/zettel/generate.js +1 -1
  14. package/{engines → strategies}/zettel/issue-reporter.js +220 -3
  15. package/{engines → strategies}/zettel/session-store.js +1 -1
  16. package/transpiler/transpiler.js +1 -1
  17. package/engine/constitution.md +0 -78
  18. /package/{engine → core}/artifacts.js +0 -0
  19. /package/{engine → core}/context-store.js +0 -0
  20. /package/{engine → core}/pattern-export.js +0 -0
  21. /package/{engine → core}/pipeline/engine.js +0 -0
  22. /package/{engine → core}/pipeline/types.js +0 -0
  23. /package/{engine → core}/reference.js +0 -0
  24. /package/{engines → strategies}/zettel/_smoke.js +0 -0
  25. /package/{engines → strategies}/zettel/chunk-composer.js +0 -0
  26. /package/{engines → strategies}/zettel/chunk-refiner.js +0 -0
  27. /package/{engines → strategies}/zettel/composer.js +0 -0
  28. /package/{engines → strategies}/zettel/fragment-library.js +0 -0
  29. /package/{engines → strategies}/zettel/generator-adapter.js +0 -0
  30. /package/{engines → strategies}/zettel/state-cache.js +0 -0
  31. /package/{engines → strategies}/zettel/synthesizer.js +0 -0
package/CHANGELOG.md CHANGED
@@ -10,6 +10,102 @@ generator graph.
10
10
 
11
11
  ## [Unreleased]
12
12
 
13
+ _Nothing yet._
14
+
15
+ ---
16
+
17
+ ## [0.2.1] - 2026-05-02
18
+
19
+ **Lockstep cut + scope-drift gate at composer time + skeleton harvest + Tier-1 block filter + high-resolution session ticket trace.** All 8 published `@adia-ai/*` packages bump 0.2.0 → 0.2.1 per [`docs/specs/package-architecture.md` § 15](../../../docs/specs/package-architecture.md#15-versioning-policy). Patch cut — no breaking changes.
20
+
21
+ ### Changed
22
+
23
+ - `version`: `0.2.0` → `0.2.1`.
24
+ - `dependencies["@adia-ai/a2ui-corpus"]`: `^0.2.0` (covers `0.2.1`).
25
+ - `dependencies["@adia-ai/a2ui-utils"]`: `^0.2.0` (covers `0.2.1`).
26
+ - `dependencies["@adia-ai/a2ui-validator"]`: `^0.2.0` (covers `0.2.1`).
27
+ - `dependencies["@adia-ai/a2ui-retrieval"]`: `^0.2.0` (covers `0.2.1`).
28
+ - `strategies/zettel/chunk-synthesizer.js` and `strategies/zettel/issue-reporter.js` are the surfaces that gained the new behavior below; no other directories under `strategies/` were touched.
29
+
30
+ ### Added — Scope-drift gate at composer time (2026-05-02)
31
+
32
+ `composeFromIntent` now computes a scope-drift signal after every successful composition: composed-HTML component count vs. the sum of bound chunks' component counts. When `actual / expected > 1.5×` (with a 20-component floor to suppress small-UI noise), the synthesizer emits a warning with the ratio. The MCP layer auto-fires a `scope-drift` issue on detection, which writes both a JSON ticket and a high-resolution Markdown ticket showing the bound-chunk envelope, ratio, and a callout naming the canvas-drift regression class.
33
+
34
+ - `chunk-synthesizer.js`: `computeScopeDrift(html, boundChunks)`, `SCOPE_DRIFT_RATIO = 1.5`, `SCOPE_DRIFT_MIN_ACTUAL = 20`. Tier-1 (single bound block) and Tier-2 (page chunk + every block resolved into a slot) both instrumented.
35
+ - `issue-reporter.js`: new `'scope-drift'` reason in `AUTO_FIRE_POLICY` (type `bug`, severity `drift`, owner `synthesis`); `scopeDrift` flowed into the trace; ticket renderer extended with envelope + ratio + drift callout.
36
+
37
+ Closes the loop on the canvas-drift regression class — the §37 fix patched the immediate symptom (84 components for a 4-stat retrieval), this trip-wire makes the class self-detecting.
38
+
39
+ ### Added — Skeleton harvest + Tier-1 block filter (2026-05-02)
40
+
41
+ Two coordinated changes to the chunk-corpus pipeline that bound the synthesizer's output envelope.
42
+
43
+ - **Skeleton harvest for nested chunks.** `harvest-chunks.mjs` now collapses each nested `data-chunk` element's inner content to `<!-- nested: <name> -->`, so page/panel chunks become compact skeletons. `dashboard-admin-page` shrank from 26,555c → 1,522c (-94%); `dashboard-overview-panel` from 12,493c → 898c (-93%); xl-bucket chunks (>10K chars) went from 2 → 0.
44
+ - **Tier-1 retrieval `kind: 'block'` filter.** `composeFromIntent` restricts the fast-path retrieval to block-kind chunks; page/panel composition still works via Tier-2 LLM synthesis with slot-binding, where the skeleton is the right input.
45
+
46
+ End-to-end: "trace documentation UI with stat cards" now produces 4 components (was 84). "kpi dashboard with stat cards" → 13 components. The retrieved chunk's component count is now an upper bound on the Tier-1 fast-path output.
47
+
48
+ ### Added — High-resolution session ticket trace (2026-05-02)
49
+
50
+ The synthesizer now captures a `retrievalTrace` per attempt: Tier-1 hits with scores + kinds, Tier-2 catalog summary (page/panel/block counts, top-N block candidates), user-prompt char count, system-prompt hash, attempt-by-attempt LLM raw responses, validation results, plan, and HTML preview. The state cache stores it on every entry. `attachTrace('full')` (now the default when `state_id` is provided) returns the full session-replay payload.
51
+
52
+ `issue-reporter.js` renders this as a sibling Markdown ticket alongside the JSON; sections cover header, description, reproduction, component count, retrieval log table, LLM attempts (raw responses), user prompt, composer plan, generated HTML preview, warnings, ops history, environment. A maintainer can replay any flagged session from the ticket alone.
53
+
54
+ ---
55
+
56
+ ## [0.2.0] - 2026-05-02
57
+
58
+ **Lockstep cut + boundary cleanup.** All 8 published `@adia-ai/*`
59
+ packages now share one version, governed by
60
+ [`docs/specs/package-architecture.md` § 15 (Versioning Policy)](../../../docs/specs/package-architecture.md#15-versioning-policy).
61
+ This release also lands the `engine/` ↔ `engines/` collision fix from
62
+ T3 of the
63
+ [`docs/plans/packages-architecture-fixes-2026-05-02.md`](../../../.brain/archive/2026-Q2/PLAN-packages-architecture-fixes-2026-05-02.md)
64
+ plan.
65
+
66
+ ### Changed
67
+
68
+ - `version`: `0.1.0` → `0.2.0`.
69
+ - `dependencies["@adia-ai/a2ui-utils"]`: `^0.0.2` → `^0.2.0`.
70
+ - `dependencies["@adia-ai/a2ui-retrieval"]`: `^0.0.1` → `^0.2.0`.
71
+ - `dependencies["@adia-ai/a2ui-validator"]`: `^0.0.1` → `^0.2.0`.
72
+
73
+ - **Directory renames — `engine/` → `core/`, `engines/` → `strategies/`**
74
+ to remove the singular/plural collision that read as a typo. The
75
+ orchestrator (`generator.js`, `state.js`, `context-store.js`,
76
+ `artifacts.js`, `pattern-export.js`, `reference.js`, `pipeline/`) now
77
+ lives at `core/`; the pluggable strategies (`monolithic/`, `zettel/`,
78
+ `registry.js`) live at `strategies/`. The `transpiler/` directory's
79
+ internal imports were also rewritten by the same sweep (no behavior
80
+ change). All internal imports updated in one sweep.
81
+ - **`constitution.md` moved out of the package source** to
82
+ [`docs/specs/compose-constitution.md`](../../../docs/specs/compose-constitution.md)
83
+ so it doesn't ship in the published tarball. Pointer added in
84
+ README.
85
+ - **`package.json` `exports` map updated** with new keys (`./core`,
86
+ `./strategies/registry`, `./strategies/zettel`); old keys
87
+ (`./engine`, `./engines/registry`, `./engines/zettel`) retained as
88
+ aliases for one release cycle and removed in the next major.
89
+ - Spec [`docs/specs/package-architecture.md`](../../../docs/specs/package-architecture.md)
90
+ bumped 0.5.0 → 0.6.0 with the directory tree updated to reflect the
91
+ new layout; new § 15 documents the lockstep versioning policy.
92
+
93
+ ### Migration
94
+
95
+ Consumers importing from the old paths still work for one release
96
+ cycle, but should update on next touch:
97
+
98
+ ```diff
99
+ - import { generateUI } from '@adia-ai/a2ui-compose/engine';
100
+ + import { generateUI } from '@adia-ai/a2ui-compose/core';
101
+
102
+ - import { pick, registerEngine } from '@adia-ai/a2ui-compose/engines/registry';
103
+ + import { pick, registerEngine } from '@adia-ai/a2ui-compose/strategies/registry';
104
+
105
+ - import { generateZettel } from '@adia-ai/a2ui-compose/engines/zettel';
106
+ + import { generateZettel } from '@adia-ai/a2ui-compose/strategies/zettel';
107
+ ```
108
+
13
109
  ---
14
110
 
15
111
  ## [0.1.0] - 2026-04-28
@@ -85,7 +181,7 @@ thin until Phase B introduces typed component-tree refinements.
85
181
 
86
182
  ### Out of scope (Phase B / C)
87
183
 
88
- - Persistent state across server restarts (SQLite-backed cache).
184
+ - Persistent state across server restarts (filesystem-JSON-backed cache at `.brain/state-cache/`; mirrors the existing audit-ledger convention; design in plan §2).
89
185
  - Branching / version trees.
90
186
  - LLM-streamed op emission.
91
187
  - Multi-user concurrent refinement (CRDT / OT).
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # @adia-ai/a2ui-compose
2
2
 
3
- Framework-agnostic UI generation engine. Takes a natural-language intent +
3
+ Framework-agnostic UI generation engine. Constitution doc:
4
+ [`docs/specs/compose-constitution.md`](../../../docs/specs/compose-constitution.md).
5
+ Takes a natural-language intent +
4
6
  an A2UI component catalog and produces a tree of A2UI protocol messages
5
7
  ready for a renderer.
6
8
 
@@ -24,7 +26,7 @@ intent ─▶ classify ─▶ retrieve ─▶ compose / adapt ─▶ val
24
26
  One entry point, two generation strategies, pluggable LLM back-end.
25
27
 
26
28
  ```javascript
27
- import { generateUI } from '@adia-ai/a2ui-compose/engine';
29
+ import { generateUI } from '@adia-ai/a2ui-compose/core';
28
30
 
29
31
  const result = await generateUI({
30
32
  intent: 'login form with email, password, and remember-me',
@@ -41,11 +43,13 @@ const result = await generateUI({
41
43
  ## Layout
42
44
 
43
45
  ```
44
- gen-ui/
45
- ├── engine/
46
- └── generator.js generateUI() — the one public entry point
46
+ a2ui-compose/
47
+ ├── core/ orchestrator — state, dispatch, pipeline (formerly engine/)
48
+ ├── generator.js generateUI() — the one public entry point
49
+ │ ├── state.js ArtifactStore + PipelineEngine singletons
50
+ │ └── pipeline/ 6-stage pipeline engine
47
51
 
48
- ├── engines/ pluggable strategies via registerEngine()
52
+ ├── strategies/ pluggable engines via registerEngine() (formerly engines/)
49
53
  │ ├── registry.js engine selector + reserved-name guard
50
54
  │ ├── monolithic/ pattern-match + LLM-adapt (3 modes)
51
55
  │ │ ├── generate-instant.js no LLM — pattern-match only
@@ -54,7 +58,7 @@ gen-ui/
54
58
  │ └── zettel/ fragment-graph composition
55
59
  │ ├── generator-adapter.js entry point
56
60
  │ ├── composer.js assembles fragments → compositions
57
- │ └── session.js multi-turn state
61
+ │ └── session-store.js multi-turn state (Phase A)
58
62
 
59
63
  ├── retrieval/
60
64
  │ ├── catalog.js loads component schemas from sibling .a2ui.json
@@ -98,7 +102,7 @@ and assembles them into compositions. Verbatim retrieval above a threshold
98
102
  across multi-turn iterations.
99
103
 
100
104
  ```javascript
101
- import { registerEngine } from '@adia-ai/a2ui-compose/engines/registry';
105
+ import { registerEngine } from '@adia-ai/a2ui-compose/strategies/registry';
102
106
 
103
107
  registerEngine('my-engine', async (ctx) => {
104
108
  // ctx: intent, catalog, patterns, concepts, session, llm, …
@@ -14,21 +14,21 @@
14
14
  import { validateSchema } from '../../validator/validator.js';
15
15
  import { getContext, searchBlocks, searchBlocksSemantic, lookupDomain, listPatterns } from './reference.js';
16
16
  import { store, engine } from './state.js';
17
- import { checkIntentAlignment } from '../../retrieval/intent-alignment.js';
18
- import { decomposeIntent, composeSubtasks } from '../../retrieval/decomposer.js';
17
+ import { checkIntentAlignment } from '../../retrieval/intent/intent-alignment.js';
18
+ import { decomposeIntent, composeSubtasks } from '../../retrieval/intent/decomposer.js';
19
19
  import { getWiringCatalog } from '../../retrieval/wiring-catalog.js';
20
20
  import { getComponentData } from '../../retrieval/pattern-library.js';
21
21
 
22
22
  import { StubLLMAdapter } from '../llm/llm-stub.js';
23
23
  import { createAdapter } from '../llm/llm-bridge.js';
24
- import { assessClarity } from '../../retrieval/clarity.js';
25
- import { isConversational } from '../../retrieval/intent-gate.js';
24
+ import { assessClarity } from '../../retrieval/intent/clarity.js';
25
+ import { isConversational } from '../../retrieval/intent/intent-gate.js';
26
26
  import { classifyIntent } from '../../retrieval/domain-router.js';
27
- import { researchIntent, detectReferences } from '../../retrieval/web-research.js';
28
- import { feedbackStore } from '../../retrieval/feedback-store.js';
29
- import { pick as pickEngine, registerMonolithicEngines } from '../engines/registry.js';
30
- import { analyzePrompt } from '../../retrieval/prompt-analyzer.js';
31
- import { recordTurn, isRecording } from '../../retrieval/dialog-recorder.js';
27
+ import { researchIntent, detectReferences } from '../../retrieval/authoring/web-research.js';
28
+ import { feedbackStore } from '../../retrieval/feedback/feedback-store.js';
29
+ import { pick as pickEngine, registerMonolithicEngines } from '../strategies/registry.js';
30
+ import { analyzePrompt } from '../../retrieval/intent/prompt-analyzer.js';
31
+ import { recordTurn, isRecording } from '../../retrieval/feedback/dialog-recorder.js';
32
32
  import {
33
33
  buildSystemPrompt,
34
34
  buildChatMessages,
@@ -38,10 +38,10 @@ import {
38
38
  parseA2UIResponse,
39
39
  buildRepairPrompt,
40
40
  generateSuggestions,
41
- } from '../engines/monolithic/_shared.js';
42
- import { generateInstant } from '../engines/monolithic/generate-instant.js';
43
- import { generatePro } from '../engines/monolithic/generate-pro.js';
44
- import { generateThinking } from '../engines/monolithic/generate-thinking.js';
41
+ } from '../strategies/monolithic/_shared.js';
42
+ import { generateInstant } from '../strategies/monolithic/generate-instant.js';
43
+ import { generatePro } from '../strategies/monolithic/generate-pro.js';
44
+ import { generateThinking } from '../strategies/monolithic/generate-thinking.js';
45
45
 
46
46
 
47
47
  /**
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Shared module-level singletons for the monolithic engine.
3
3
  *
4
- * These were previously private to engine/generator.js. Extracted so that
5
- * engines/monolithic/generate-{instant,pro,thinking}.js can share the same
4
+ * These were previously private to core/generator.js. Extracted so that
5
+ * strategies/monolithic/generate-{instant,pro,thinking}.js can share the same
6
6
  * artifact store and pipeline engine without going through the dispatcher.
7
7
  *
8
8
  * Same instances, imported by reference — do NOT re-instantiate per-call.
package/index.js CHANGED
@@ -5,12 +5,12 @@
5
5
  * via subpaths:
6
6
  *
7
7
  * import { generateUI } from '@adia-ai/a2ui-compose';
8
- * import { pick, registerEngine } from '@adia-ai/a2ui-compose/engines/registry';
9
- * import { generateZettel } from '@adia-ai/a2ui-compose/engines/zettel';
8
+ * import { pick, registerEngine } from '@adia-ai/a2ui-compose/strategies/registry';
9
+ * import { generateZettel } from '@adia-ai/a2ui-compose/strategies/zettel';
10
10
  * import { llmBridge } from '@adia-ai/a2ui-compose/llm';
11
11
  *
12
12
  * See README for the full public surface.
13
13
  */
14
14
 
15
- export { generateUI, generateUIStream } from './engine/generator.js';
16
- export { pick, listEngines, registerEngine, unregisterEngine, ENGINES } from './engines/registry.js';
15
+ export { generateUI, generateUIStream } from './core/generator.js';
16
+ export { pick, listEngines, registerEngine, unregisterEngine, ENGINES } from './strategies/registry.js';
package/package.json CHANGED
@@ -1,21 +1,24 @@
1
1
  {
2
2
  "name": "@adia-ai/a2ui-compose",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "AdiaUI A2UI compose engine — framework-agnostic. Takes natural-language intents + a catalog and produces A2UI protocol messages. Pairs with `@adia-ai/a2ui-retrieval` (intent classification, catalog lookup) and `@adia-ai/a2ui-validator` (schema + semantic checks).",
5
5
  "type": "module",
6
6
  "exports": {
7
- ".": "./index.js",
8
- "./engine": "./engine/generator.js",
9
- "./engines/zettel": "./engines/zettel/generator-adapter.js",
10
- "./engines/registry": "./engines/registry.js",
11
- "./llm": "./llm/llm-bridge.js",
12
- "./llm/*": "./llm/*.js",
13
- "./transpiler": "./transpiler/transpiler.js",
14
- "./evals": "./evals/harness.mjs"
7
+ ".": "./index.js",
8
+ "./core": "./core/generator.js",
9
+ "./strategies/zettel": "./strategies/zettel/generator-adapter.js",
10
+ "./strategies/registry": "./strategies/registry.js",
11
+ "./llm": "./llm/llm-bridge.js",
12
+ "./llm/*": "./llm/*.js",
13
+ "./transpiler": "./transpiler/transpiler.js",
14
+ "./evals": "./evals/harness.mjs",
15
+ "./engine": "./core/generator.js",
16
+ "./engines/zettel": "./strategies/zettel/generator-adapter.js",
17
+ "./engines/registry": "./strategies/registry.js"
15
18
  },
16
19
  "files": [
17
- "engine/",
18
- "engines/",
20
+ "core/",
21
+ "strategies/",
19
22
  "llm/",
20
23
  "evals/",
21
24
  "transpiler/",
@@ -34,8 +37,8 @@
34
37
  "directory": "packages/a2ui/compose"
35
38
  },
36
39
  "dependencies": {
37
- "@adia-ai/a2ui-utils": "^0.0.2",
38
- "@adia-ai/a2ui-retrieval": "^0.0.1",
39
- "@adia-ai/a2ui-validator": "^0.0.1"
40
+ "@adia-ai/a2ui-utils": "^0.2.0",
41
+ "@adia-ai/a2ui-retrieval": "^0.2.0",
42
+ "@adia-ai/a2ui-validator": "^0.2.0"
40
43
  }
41
44
  }
@@ -1,16 +1,16 @@
1
1
  /**
2
2
  * @adia-ai/a2ui-compose — monolithic engine, shared helpers.
3
3
  *
4
- * Pure helpers extracted from engine/generator.js per spec §11 Phase 2:
4
+ * Pure helpers extracted from core/generator.js per spec §11 Phase 2:
5
5
  * prompt builders, JSON parsers, canvas diff utilities, suggestion
6
6
  * generators. All stateless (the only module-level state is the lazy
7
7
  * component-catalog cache inside getComponentCatalog).
8
8
  */
9
9
 
10
- import { listPatterns } from '../../engine/reference.js';
11
- import { store } from '../../engine/state.js';
12
- import { checkIntentAlignment } from '../../../retrieval/intent-alignment.js';
13
- import { composeSubtasks } from '../../../retrieval/decomposer.js';
10
+ import { listPatterns } from '../../core/reference.js';
11
+ import { store } from '../../core/state.js';
12
+ import { checkIntentAlignment } from '../../../retrieval/intent/intent-alignment.js';
13
+ import { composeSubtasks } from '../../../retrieval/intent/decomposer.js';
14
14
  import { getWiringCatalog } from '../../../retrieval/wiring-catalog.js';
15
15
  import { getComponentData } from '../../../retrieval/pattern-library.js';
16
16
 
@@ -412,10 +412,10 @@ STATE LAYER:
412
412
  Controllers: ${wc.controllers.map(c => c.type).join(', ')} (extensible via registerController)
413
413
  "bind" maps controller state ↔ model paths for two-way sync.
414
414
 
415
- ACTIONS LAYER — AdiaEvent → handler chains:
416
- Each action binds a AdiaEvent to a handler. The event is a typed object, not a string.
415
+ ACTIONS LAYER — UIEvent → handler chains:
416
+ Each action binds a UIEvent to a handler. The event is a typed object, not a string.
417
417
 
418
- AdiaEvent types: ${wc.adiaEvents.map(e => e.event).join(', ')}
418
+ UIEvent types: ${wc.adiaEvents.map(e => e.event).join(', ')}
419
419
  Each carries a typed payload:
420
420
  ${wc.adiaEvents.filter(e => e.payload).map(e => ` ${e.event} → ${e.payload}`).join('\n')}
421
421
 
@@ -434,7 +434,7 @@ ${wc.adiaEvents.filter(e => e.payload).map(e => ` ${e.event} → ${e.payload}
434
434
  }
435
435
  ]
436
436
 
437
- AdiaEvent shape: { "event": "<type>", "target": "<componentId>", "debounce"?: ms, "throttle"?: ms, "condition"?: { "path", "equals"|"notEquals"|"exists" } }
437
+ UIEvent shape: { "event": "<type>", "target": "<componentId>", "debounce"?: ms, "throttle"?: ms, "condition"?: { "path", "equals"|"notEquals"|"exists" } }
438
438
  "target" scopes to a component id. Omit for surface-level events (mount, unmount).
439
439
  "onSuccess" / "onError" are follow-up action arrays — NOT separate entries.
440
440
  Handlers: ${wc.handlers.map(h => h.name).join(', ')} (extensible via registerHandler)
@@ -1,17 +1,17 @@
1
1
  /**
2
2
  * @adia-ai/a2ui-compose — monolithic engine, instant mode.
3
3
  *
4
- * Extracted from engine/generator.js per spec §11 Phase 2. Shares state
5
- * (ArtifactStore, PipelineEngine) via engine/state.js. Pure helpers live
6
- * in engines/monolithic/_shared.js.
4
+ * Extracted from core/generator.js per spec §11 Phase 2. Shares state
5
+ * (ArtifactStore, PipelineEngine) via core/state.js. Pure helpers live
6
+ * in strategies/monolithic/_shared.js.
7
7
  */
8
8
 
9
9
  import { validateSchema } from '../../../validator/validator.js';
10
- import { getContext, searchBlocks, lookupDomain } from '../../engine/reference.js';
11
- import { assessClarity } from '../../../retrieval/clarity.js';
12
- import { feedbackStore } from '../../../retrieval/feedback-store.js';
13
- import { store, engine } from '../../engine/state.js';
14
- import { isRecording } from '../../../retrieval/dialog-recorder.js';
10
+ import { getContext, searchBlocks, lookupDomain } from '../../core/reference.js';
11
+ import { assessClarity } from '../../../retrieval/intent/clarity.js';
12
+ import { feedbackStore } from '../../../retrieval/feedback/feedback-store.js';
13
+ import { store, engine } from '../../core/state.js';
14
+ import { isRecording } from '../../../retrieval/feedback/dialog-recorder.js';
15
15
  import {
16
16
  generateSuggestions,
17
17
  } from './_shared.js';
@@ -1,19 +1,19 @@
1
1
  /**
2
2
  * @adia-ai/a2ui-compose — monolithic engine, pro mode.
3
3
  *
4
- * Extracted from engine/generator.js per spec §11 Phase 2. Shares state
5
- * (ArtifactStore, PipelineEngine) via engine/state.js. Pure helpers live
6
- * in engines/monolithic/_shared.js.
4
+ * Extracted from core/generator.js per spec §11 Phase 2. Shares state
5
+ * (ArtifactStore, PipelineEngine) via core/state.js. Pure helpers live
6
+ * in strategies/monolithic/_shared.js.
7
7
  */
8
8
 
9
9
  import { validateSchema } from '../../../validator/validator.js';
10
- import { getContext, searchBlocks, lookupDomain } from '../../engine/reference.js';
11
- import { decomposeIntent } from '../../../retrieval/decomposer.js';
12
- import { assessClarity } from '../../../retrieval/clarity.js';
13
- import { isConversational } from '../../../retrieval/intent-gate.js';
14
- import { feedbackStore } from '../../../retrieval/feedback-store.js';
15
- import { store, engine } from '../../engine/state.js';
16
- import { isRecording } from '../../../retrieval/dialog-recorder.js';
10
+ import { getContext, searchBlocks, lookupDomain } from '../../core/reference.js';
11
+ import { decomposeIntent } from '../../../retrieval/intent/decomposer.js';
12
+ import { assessClarity } from '../../../retrieval/intent/clarity.js';
13
+ import { isConversational } from '../../../retrieval/intent/intent-gate.js';
14
+ import { feedbackStore } from '../../../retrieval/feedback/feedback-store.js';
15
+ import { store, engine } from '../../core/state.js';
16
+ import { isRecording } from '../../../retrieval/feedback/dialog-recorder.js';
17
17
  import {
18
18
  buildSystemPrompt,
19
19
  buildChatMessages,
@@ -1,18 +1,18 @@
1
1
  /**
2
2
  * @adia-ai/a2ui-compose — monolithic engine, thinking mode.
3
3
  *
4
- * Extracted from engine/generator.js per spec §11 Phase 2. Shares state
5
- * (ArtifactStore, PipelineEngine) via engine/state.js. Pure helpers live
6
- * in engines/monolithic/_shared.js.
4
+ * Extracted from core/generator.js per spec §11 Phase 2. Shares state
5
+ * (ArtifactStore, PipelineEngine) via core/state.js. Pure helpers live
6
+ * in strategies/monolithic/_shared.js.
7
7
  */
8
8
 
9
9
  import { validateSchema } from '../../../validator/validator.js';
10
10
  import { validateMessages as validateCatalog } from '../../../validator/catalog-validator.js';
11
- import { getContext, searchBlocksSemantic, lookupDomain } from '../../engine/reference.js';
12
- import { assessClarity } from '../../../retrieval/clarity.js';
13
- import { feedbackStore } from '../../../retrieval/feedback-store.js';
14
- import { store, engine } from '../../engine/state.js';
15
- import { isRecording } from '../../../retrieval/dialog-recorder.js';
11
+ import { getContext, searchBlocksSemantic, lookupDomain } from '../../core/reference.js';
12
+ import { assessClarity } from '../../../retrieval/intent/clarity.js';
13
+ import { feedbackStore } from '../../../retrieval/feedback/feedback-store.js';
14
+ import { store, engine } from '../../core/state.js';
15
+ import { isRecording } from '../../../retrieval/feedback/dialog-recorder.js';
16
16
  import {
17
17
  buildSystemPrompt,
18
18
  buildChatMessages,
@@ -7,7 +7,7 @@
7
7
  * - zettel → fragment-graph composition (single mode: instant)
8
8
  *
9
9
  * The registry deliberately does NOT own intent-gating, clarity assessment, or
10
- * execution lifecycle — the public `generateUI()` in engine/generator.js does
10
+ * execution lifecycle — the public `generateUI()` in core/generator.js does
11
11
  * that, then delegates the actual work through `pick()`.
12
12
  *
13
13
  * Shape invariant: every engine function returns
@@ -15,8 +15,8 @@
15
15
  */
16
16
 
17
17
  // Zettel is lazy-loaded — it transitively imports node:fs / node:path / node:url
18
- // (engines/zettel/fragment-library.js), which Vite externalizes in the browser.
19
- // Static-importing it here would break browser loads of engine/generator.js.
18
+ // (strategies/zettel/fragment-library.js), which Vite externalizes in the browser.
19
+ // Static-importing it here would break browser loads of core/generator.js.
20
20
  // In-browser callers reach zettel via POST /api/generate (server-side), never
21
21
  // through this registry, so lazy-loading is safe and incurs only a one-time
22
22
  // import on first server-side invocation.
@@ -33,15 +33,15 @@ async function getGenerateZettel() {
33
33
  let _isRecording = null;
34
34
  async function getIsRecording() {
35
35
  if (!_isRecording) {
36
- const mod = await import('../../retrieval/dialog-recorder.js');
36
+ const mod = await import('../../retrieval/feedback/dialog-recorder.js');
37
37
  _isRecording = mod.isRecording;
38
38
  }
39
39
  return _isRecording;
40
40
  }
41
41
 
42
- // The three monolithic paths live inside engine/generator.js as internal
42
+ // The three monolithic paths live inside core/generator.js as internal
43
43
  // functions. They're injected at boot via `registerMonolithicEngines()` to
44
- // avoid a circular import (engine/generator.js owns the registry caller).
44
+ // avoid a circular import (core/generator.js owns the registry caller).
45
45
  let _monolithicInstant = null;
46
46
  let _monolithicPro = null;
47
47
  let _monolithicThinking = null;
@@ -31,6 +31,12 @@ import { composeFromPlan, validatePlan } from './chunk-composer.js';
31
31
  const STRONG_RETRIEVAL_SCORE = 8; // search-score threshold for fast path
32
32
  const PRE_SEARCH_LIMIT = 30; // chunks shown to the LLM in the prompt
33
33
  const DEFAULT_MAX_ATTEMPTS = 2;
34
+ // Scope-drift gate: composed HTML's component count vs. the sum of bound
35
+ // chunks' component counts. A multiplier > SCOPE_DRIFT_RATIO trips a warning
36
+ // + auto-fires a `scope-drift` issue. Floor prevents false positives on
37
+ // small UIs where slot-wrapper noise dominates.
38
+ const SCOPE_DRIFT_RATIO = 1.5;
39
+ const SCOPE_DRIFT_MIN_ACTUAL = 20;
34
40
 
35
41
  const SYSTEM_PROMPT = `You compose web-app pages by binding training chunks into named slots.
36
42
 
@@ -141,17 +147,30 @@ function extractJSON(raw) {
141
147
  export async function composeFromIntent({ intent, llmAdapter, maxAttempts = DEFAULT_MAX_ATTEMPTS }) {
142
148
  // Tier 1 — retrieval. Try semantic-blended hit first; fall back to keyword
143
149
  // when embeddings are unavailable (no chunk-embeddings.json or no API key).
144
- const hits = await searchChunksAsync(intent, { limit: 5 });
150
+ //
151
+ // Restricted to kind=block: page/panel chunks are SKELETONS that need
152
+ // slot-binding composition (Tier 2 handles them). Returning a skeleton
153
+ // directly from Tier-1 would emit a near-empty page; we only want the
154
+ // fast-path for atomic block patterns.
155
+ const hits = await searchChunksAsync(intent, { kind: 'block', limit: 5 });
145
156
  if (hits.length > 0 && hits[0].score >= STRONG_RETRIEVAL_SCORE) {
146
157
  const top = getChunk(hits[0].name);
147
158
  const html = top.html || top.instances?.[0]?.html || '';
159
+ // Tier-1 fast path: sole bound chunk is the retrieved block. The gate
160
+ // is mostly a no-op here (ratio ≈ 1) but stays for symmetry — and to
161
+ // catch a corner case where retrieval returns a block that, post-render,
162
+ // expands far beyond its source (shouldn't happen, but worth detecting).
163
+ const scopeDrift = computeScopeDrift(html, [top]);
148
164
  return {
149
165
  html,
150
166
  plan: null,
151
167
  source: 'retrieval',
152
168
  score: hits[0].score,
153
169
  cosineScore: hits[0].cosineScore,
154
- warnings: [],
170
+ warnings: scopeDrift.drift
171
+ ? [`scope drift: ${scopeDrift.actual} components in HTML vs ${scopeDrift.expected} in bound chunk (ratio ${scopeDrift.ratio.toFixed(2)}×)`]
172
+ : [],
173
+ scopeDrift,
155
174
  };
156
175
  }
157
176
 
@@ -189,6 +208,29 @@ export async function composeFromIntent({ intent, llmAdapter, maxAttempts = DEFA
189
208
  let lastError = null;
190
209
  const attempts = [];
191
210
 
211
+ // Trace: snapshot of the retrieval log for the issue-reporter to surface
212
+ // verbatim on bug tickets. Recorded once before the retry loop so it
213
+ // describes what the LLM actually saw.
214
+ const retrievalTrace = {
215
+ tier1Hits: hits.slice(0, 5).map((h) => ({
216
+ name: h.name,
217
+ score: Number(h.score.toFixed(3)),
218
+ kind: h.kind,
219
+ cosineScore: h.cosineScore != null ? Number(h.cosineScore.toFixed(3)) : null,
220
+ })),
221
+ tier1Threshold: STRONG_RETRIEVAL_SCORE,
222
+ tier1Pass: hits.length > 0 && hits[0].score >= STRONG_RETRIEVAL_SCORE,
223
+ catalogSize: filtered.length,
224
+ catalogPageNames: pageChunks.map((c) => c.name),
225
+ catalogPanelNames: panelChunks.map((c) => c.name),
226
+ catalogBlockTopN: blockHits.slice(0, 10).map((h) => ({
227
+ name: h.name,
228
+ score: Number(h.score.toFixed(3)),
229
+ })),
230
+ userPromptChars: userPrompt.length,
231
+ systemPromptChars: SYSTEM_PROMPT.length,
232
+ };
233
+
192
234
  for (let i = 0; i < maxAttempts; i++) {
193
235
  const retryNudge = lastError
194
236
  ? `\n\nPREVIOUS ATTEMPT FAILED: ${lastError}. Return ONLY a JSON object shaped as { "page": "...", "slot_bindings": { ... } }. No prose, no questions.`
@@ -216,12 +258,33 @@ export async function composeFromIntent({ intent, llmAdapter, maxAttempts = DEFA
216
258
  continue;
217
259
  }
218
260
 
261
+ // Compute scope-drift signal: composed envelope vs sum of bound chunks.
262
+ // Bound chunks = page chunk + every block/panel resolved into a slot.
263
+ const boundNames = new Set([plan.page]);
264
+ for (const v of Object.values(plan.slot_bindings || {})) {
265
+ const arr = Array.isArray(v) ? v : [v];
266
+ for (const n of arr) boundNames.add(n);
267
+ }
268
+ const boundChunks = [...boundNames].map((n) => getChunk(n)).filter(Boolean);
269
+ const scopeDrift = computeScopeDrift(composed.html, boundChunks);
270
+ const driftWarnings = scopeDrift.drift
271
+ ? [`scope drift: ${scopeDrift.actual} components in composed HTML vs ${scopeDrift.expected} in bound chunks (ratio ${scopeDrift.ratio.toFixed(2)}× exceeds gate ${SCOPE_DRIFT_RATIO}×)`]
272
+ : [];
273
+
219
274
  return {
220
275
  html: composed.html,
221
276
  plan,
222
277
  source: 'synthesis',
223
- warnings: composed.warnings,
224
- synthesis: { attempts: i + 1, attemptsLog: attempts, validation },
278
+ warnings: [...composed.warnings, ...driftWarnings],
279
+ scopeDrift,
280
+ synthesis: {
281
+ attempts: i + 1,
282
+ attemptsLog: attempts,
283
+ validation,
284
+ retrievalTrace,
285
+ userPrompt,
286
+ systemPromptHash: hashString(SYSTEM_PROMPT),
287
+ },
225
288
  };
226
289
  }
227
290
 
@@ -230,6 +293,59 @@ export async function composeFromIntent({ intent, llmAdapter, maxAttempts = DEFA
230
293
  plan: null,
231
294
  source: 'synthesis',
232
295
  warnings: [`synthesis failed after ${maxAttempts} attempts: ${lastError}`],
233
- synthesis: { attempts: maxAttempts, attemptsLog: attempts },
296
+ synthesis: {
297
+ attempts: maxAttempts,
298
+ attemptsLog: attempts,
299
+ retrievalTrace,
300
+ userPrompt,
301
+ systemPromptHash: hashString(SYSTEM_PROMPT),
302
+ },
234
303
  };
235
304
  }
305
+
306
+ // Cheap non-crypto hash for prompt-version fingerprinting in tickets.
307
+ function hashString(s) {
308
+ let h = 5381;
309
+ for (let i = 0; i < s.length; i++) h = ((h * 33) ^ s.charCodeAt(i)) >>> 0;
310
+ return 'h' + h.toString(36);
311
+ }
312
+
313
+ /**
314
+ * Heuristic component count — number of opening tags in the HTML.
315
+ * Loose but stable proxy for A2UI envelope size; used by the scope-drift
316
+ * gate (above) and by issue-reporter's ticket-rendering counter.
317
+ */
318
+ function countComponents(html) {
319
+ if (typeof html !== 'string') return 0;
320
+ return (html.match(/<[a-z][a-z0-9-]*/g) || []).length;
321
+ }
322
+
323
+ /**
324
+ * Compute the scope-drift signal for a composed result.
325
+ *
326
+ * Inputs: the final HTML string + the chunk records that fed into it
327
+ * (page chunk for Tier-2; the single block for Tier-1). Sums the bound
328
+ * chunks' component counts to get an "expected envelope," compares it to
329
+ * the actual composed count, and reports the ratio.
330
+ *
331
+ * Returns { actual, expected, ratio, drift } where `drift` is true when
332
+ * actual exceeds SCOPE_DRIFT_RATIO × expected AND actual ≥ SCOPE_DRIFT_MIN_ACTUAL.
333
+ */
334
+ function computeScopeDrift(html, boundChunks) {
335
+ const actual = countComponents(html);
336
+ let expected = 0;
337
+ for (const c of boundChunks) {
338
+ if (!c) continue;
339
+ const chunkHtml = c.html || c.instances?.[0]?.html;
340
+ expected += countComponents(chunkHtml);
341
+ }
342
+ // Guard against zero-expected: when bound chunks have no HTML accounted for
343
+ // (rare; should only happen in malformed corpora), skip the gate rather
344
+ // than report infinite drift.
345
+ if (expected === 0) {
346
+ return { actual, expected, ratio: null, drift: false };
347
+ }
348
+ const ratio = actual / expected;
349
+ const drift = actual >= SCOPE_DRIFT_MIN_ACTUAL && ratio > SCOPE_DRIFT_RATIO;
350
+ return { actual, expected, ratio, drift };
351
+ }
@@ -9,7 +9,7 @@
9
9
  * existing canvas instead of regenerating
10
10
  *
11
11
  * The actual implementation is generator-adapter.js; this file is the
12
- * spec-compliant entry point at engines/zettel/generate.js per §11.
12
+ * spec-compliant entry point at strategies/zettel/generate.js per §11.
13
13
  */
14
14
 
15
15
  export { generateZettel as generate } from './generator-adapter.js';
@@ -75,6 +75,16 @@ export const AUTO_FIRE_POLICY = {
75
75
  suggested_owner: 'validator',
76
76
  titleFor: () => `refine_composition ops_failed list non-empty after apply`,
77
77
  },
78
+ 'scope-drift': {
79
+ type: 'bug',
80
+ severity: 'drift',
81
+ suggested_owner: 'synthesis',
82
+ titleFor: (ctx) => {
83
+ const ratio = ctx?.scopeDrift?.ratio != null ? `${ctx.scopeDrift.ratio.toFixed(1)}×` : '';
84
+ const intent = ctx?.intent ? ` for "${truncate(ctx.intent, 40)}"` : '';
85
+ return `Scope drift${ratio ? ' ' + ratio : ''}: composed HTML exceeds bound-chunk envelope${intent}`;
86
+ },
87
+ },
78
88
  };
79
89
 
80
90
  function truncate(s, n = 60) {
@@ -167,9 +177,20 @@ export async function attachTrace(state_id, depth, cache) {
167
177
  };
168
178
  }
169
179
 
170
- // 'full'
180
+ // 'full' — high-resolution session trace. Includes everything from
181
+ // summary plus the synthesis breadcrumbs (retrieval log, LLM prompts,
182
+ // raw responses per attempt, validation, plan, composed HTML preview)
183
+ // so a human reviewer or future agent can replay the full session.
171
184
  return {
172
185
  ...baseTrace,
186
+ intent: entry.intent ?? null,
187
+ source: entry.source ?? null,
188
+ score: entry.score ?? null,
189
+ plan: entry.plan ?? null,
190
+ synthesis: entry.synthesis ?? null, // retrievalTrace, attemptsLog, prompts
191
+ htmlPreview: entry.html ? truncateHtmlForTrace(entry.html) : null,
192
+ componentCount: entry.html ? countComponents(entry.html) : null,
193
+ scopeDrift: entry.scopeDrift ?? null,
173
194
  internal: entry.internal ?? null,
174
195
  output: entry.output ?? {
175
196
  ops: entry.ops_history || [],
@@ -177,9 +198,27 @@ export async function attachTrace(state_id, depth, cache) {
177
198
  },
178
199
  warnings: entry.warnings || [],
179
200
  duration_ms: entry.duration_ms ?? null,
201
+ parent_state_id: entry.parent_state_id ?? null,
202
+ created_at: entry.created_at ?? null,
180
203
  };
181
204
  }
182
205
 
206
+ const HTML_PREVIEW_MAX_BYTES = 8 * 1024;
207
+
208
+ function truncateHtmlForTrace(html) {
209
+ if (typeof html !== 'string') return null;
210
+ if (html.length <= HTML_PREVIEW_MAX_BYTES) return html;
211
+ return html.slice(0, HTML_PREVIEW_MAX_BYTES) + `\n<!-- ... ${html.length - HTML_PREVIEW_MAX_BYTES} bytes truncated -->`;
212
+ }
213
+
214
+ function countComponents(html) {
215
+ if (typeof html !== 'string') return 0;
216
+ // Count opening tags (excluding void/standalone HTML elements that
217
+ // don't contribute to A2UI component count). Loose heuristic; signals
218
+ // scope drift even if it's not a perfect protocol-level count.
219
+ return (html.match(/<[a-z][a-z0-9-]*/g) || []).length;
220
+ }
221
+
183
222
  /**
184
223
  * Write an issue to disk.
185
224
  *
@@ -198,7 +237,11 @@ export async function reportIssue(input, ctx = {}) {
198
237
  ? ctx.reporter
199
238
  : 'user';
200
239
 
201
- const traceDepth = input.trace ?? (input.state_id ? 'summary' : 'none');
240
+ // Default to 'full' when a state_id is provided high-resolution session
241
+ // tickets are the design intent (the previous 'summary' default produced
242
+ // tickets too thin to debug from). Caller can opt down to 'summary' or
243
+ // 'none' explicitly.
244
+ const traceDepth = input.trace ?? (input.state_id ? 'full' : 'none');
202
245
  let trace = null;
203
246
  if (input.state_id && traceDepth !== 'none') {
204
247
  trace = await attachTrace(input.state_id, traceDepth, ctx.cache);
@@ -237,7 +280,181 @@ export async function reportIssue(input, ctx = {}) {
237
280
  const path = join(storageRoot, `${issue_id}.json`);
238
281
  await writeFile(path, JSON.stringify(issue, null, 2));
239
282
 
240
- return { issue_id, path, ack: 'logged' };
283
+ // Write a sibling Markdown report for human review. The JSON ticket
284
+ // is the machine-readable source of truth; the .md is the readable
285
+ // surface a maintainer scans first to triage. Always written when
286
+ // we have a non-trivial trace (full or summary), so reviewers can
287
+ // see retrieval log, LLM prompts, attempts, plan, and composed HTML
288
+ // without reading the raw JSON.
289
+ let markdown_path = null;
290
+ if (trace) {
291
+ markdown_path = join(storageRoot, `${issue_id}.md`);
292
+ await writeFile(markdown_path, renderTicketMarkdown(issue, trace));
293
+ }
294
+
295
+ return {
296
+ issue_id,
297
+ path, // .json (machine-readable)
298
+ markdown_path, // .md (human-readable; null when trace='none')
299
+ ack: 'logged',
300
+ severity: issue.severity,
301
+ suggested_owner: issue.suggested_owner,
302
+ };
303
+ }
304
+
305
+ /**
306
+ * Render the high-resolution session ticket as Markdown.
307
+ * Sections (each appears only if relevant data exists):
308
+ * 1. Header (id, severity, type, title, owner, tags)
309
+ * 2. Description (body)
310
+ * 3. Reproduction (intent, state_id, source, score)
311
+ * 4. Component-count drift (heuristic; flagged when > 50 components)
312
+ * 5. Retrieval log (Tier 1 hits + Tier 2 catalog summary)
313
+ * 6. LLM trace (each attempt's raw response)
314
+ * 7. Composer plan (slot bindings)
315
+ * 8. HTML preview (first 8KB of generated output)
316
+ * 9. Warnings + ops history
317
+ * 10. Environment (mcp/engine/model)
318
+ */
319
+ function renderTicketMarkdown(issue, trace) {
320
+ const lines = [];
321
+ lines.push(`# ${issue.title}`);
322
+ lines.push('');
323
+ lines.push(`> **${issue.severity.toUpperCase()}** · ${issue.type} · owner: \`${issue.suggested_owner}\` · ${issue.created_at}`);
324
+ lines.push('> ');
325
+ lines.push(`> Issue ID: \`${issue.issue_id}\`${issue.tags?.length ? ' · tags: ' + issue.tags.map((t) => '`' + t + '`').join(' ') : ''}`);
326
+ lines.push('');
327
+
328
+ if (issue.body) {
329
+ lines.push('## Description');
330
+ lines.push('');
331
+ lines.push(issue.body);
332
+ lines.push('');
333
+ }
334
+
335
+ if (trace.intent || trace.state_id) {
336
+ lines.push('## Reproduction');
337
+ lines.push('');
338
+ if (trace.intent) lines.push(`- **Intent**: \`${trace.intent}\``);
339
+ if (trace.state_id) lines.push(`- **State ID**: \`${trace.state_id}\``);
340
+ if (trace.source) lines.push(`- **Source**: ${trace.source}${trace.score != null ? ` (score: ${trace.score.toFixed(3)})` : ''}`);
341
+ if (trace.parent_state_id) lines.push(`- **Parent state**: \`${trace.parent_state_id}\``);
342
+ if (trace.duration_ms != null) lines.push(`- **Duration**: ${trace.duration_ms}ms`);
343
+ lines.push('');
344
+ }
345
+
346
+ if (trace.componentCount != null) {
347
+ lines.push('## Component count');
348
+ lines.push('');
349
+ lines.push(`- **${trace.componentCount}** components in generated HTML`);
350
+ if (trace.scopeDrift) {
351
+ const sd = trace.scopeDrift;
352
+ const flag = sd.drift ? ' ⚠ **scope drift**' : '';
353
+ const ratio = sd.ratio != null ? `${sd.ratio.toFixed(2)}×` : 'n/a';
354
+ lines.push(`- Bound-chunk envelope: ${sd.expected} components`);
355
+ lines.push(`- Ratio (actual / expected): **${ratio}**${flag}`);
356
+ if (sd.drift) {
357
+ lines.push('');
358
+ lines.push(`> The composed HTML's component count exceeds the bound chunks' envelope by more than the drift gate. This is the canvas-drift regression class — the synthesizer materialized markup beyond what the retrieved chunks justify.`);
359
+ }
360
+ } else if (trace.componentCount > 50) {
361
+ lines.push('- ⚠ over-generation candidate (>50 components, no scope-drift signal available)');
362
+ }
363
+ lines.push('');
364
+ }
365
+
366
+ if (trace.synthesis?.retrievalTrace) {
367
+ const r = trace.synthesis.retrievalTrace;
368
+ lines.push('## Retrieval log');
369
+ lines.push('');
370
+ lines.push(`**Tier 1** (semantic-blended, threshold ${r.tier1Threshold}, ${r.tier1Pass ? '✓ pass' : '✗ fall through to Tier 2'})`);
371
+ lines.push('');
372
+ lines.push('| rank | score | kind | chunk |');
373
+ lines.push('|------|------:|------|-------|');
374
+ r.tier1Hits.forEach((h, i) => lines.push(`| ${i + 1} | ${h.score} | ${h.kind} | \`${h.name}\` |`));
375
+ lines.push('');
376
+ if (r.catalogSize) {
377
+ lines.push(`**Tier 2 catalog**: ${r.catalogSize} chunks (${r.catalogPageNames.length} page · ${r.catalogPanelNames.length} panel · ${r.catalogBlockTopN.length} top-block)`);
378
+ lines.push('');
379
+ if (r.catalogBlockTopN.length) {
380
+ lines.push('Top block candidates:');
381
+ for (const h of r.catalogBlockTopN) lines.push(` - ${h.score} \`${h.name}\``);
382
+ lines.push('');
383
+ }
384
+ }
385
+ }
386
+
387
+ if (trace.synthesis?.attemptsLog?.length) {
388
+ lines.push('## LLM attempts');
389
+ lines.push('');
390
+ trace.synthesis.attemptsLog.forEach((att, i) => {
391
+ lines.push(`### Attempt ${att.attempt ?? i + 1}`);
392
+ lines.push('');
393
+ lines.push('```json');
394
+ lines.push(typeof att.raw === 'string' ? att.raw.slice(0, 2000) : JSON.stringify(att.raw, null, 2).slice(0, 2000));
395
+ lines.push('```');
396
+ lines.push('');
397
+ });
398
+ if (trace.synthesis.userPrompt) {
399
+ lines.push('### User prompt sent to LLM');
400
+ lines.push('');
401
+ lines.push('```');
402
+ lines.push(trace.synthesis.userPrompt.slice(0, 3000));
403
+ if (trace.synthesis.userPrompt.length > 3000) lines.push(`... (${trace.synthesis.userPrompt.length - 3000} more chars)`);
404
+ lines.push('```');
405
+ lines.push('');
406
+ if (trace.synthesis.systemPromptHash) lines.push(`System prompt hash: \`${trace.synthesis.systemPromptHash}\` (matches across attempts)`);
407
+ lines.push('');
408
+ }
409
+ }
410
+
411
+ if (trace.plan) {
412
+ lines.push('## Composer plan');
413
+ lines.push('');
414
+ lines.push('```json');
415
+ lines.push(JSON.stringify(trace.plan, null, 2));
416
+ lines.push('```');
417
+ lines.push('');
418
+ }
419
+
420
+ if (trace.htmlPreview) {
421
+ lines.push('## Generated HTML (preview)');
422
+ lines.push('');
423
+ lines.push('```html');
424
+ lines.push(trace.htmlPreview);
425
+ lines.push('```');
426
+ lines.push('');
427
+ }
428
+
429
+ if (trace.warnings?.length) {
430
+ lines.push('## Warnings');
431
+ lines.push('');
432
+ for (const w of trace.warnings) lines.push(`- ${w}`);
433
+ lines.push('');
434
+ }
435
+
436
+ if (trace.output?.ops?.length) {
437
+ lines.push('## Ops history (refinement chain)');
438
+ lines.push('');
439
+ lines.push(`${trace.output.ops.length} ops applied. ${trace.output.delta_summary ? 'Last delta: ' + trace.output.delta_summary : ''}`);
440
+ lines.push('');
441
+ }
442
+
443
+ if (issue.environment) {
444
+ lines.push('## Environment');
445
+ lines.push('');
446
+ for (const [k, v] of Object.entries(issue.environment)) lines.push(`- **${k}**: ${v}`);
447
+ lines.push('');
448
+ }
449
+
450
+ if (issue.related_issue_ids?.length) {
451
+ lines.push('## Related issues');
452
+ lines.push('');
453
+ for (const id of issue.related_issue_ids) lines.push(`- \`${id}\``);
454
+ lines.push('');
455
+ }
456
+
457
+ return lines.join('\n');
241
458
  }
242
459
 
243
460
  /**
@@ -17,7 +17,7 @@
17
17
  * restart. Good enough for a dev server; a persistent store is a later phase.
18
18
  */
19
19
 
20
- import { ArtifactStore } from '../../engine/artifacts.js';
20
+ import { ArtifactStore } from '../../core/artifacts.js';
21
21
 
22
22
  const store = new ArtifactStore();
23
23
 
@@ -15,7 +15,7 @@ import {
15
15
  SKIP_TAGS, extractProps, inferGap,
16
16
  } from './transpiler-maps.js';
17
17
  import { validateSchema } from '../../validator/validator.js';
18
- import { getContext } from '../engine/reference.js';
18
+ import { getContext } from '../core/reference.js';
19
19
 
20
20
  // ═══════════════════════════════════════════════════════════════
21
21
  // PUBLIC API
@@ -1,78 +0,0 @@
1
- # AdiaUI Generation Constitution
2
-
3
- Rules the generator MUST satisfy. Used by the RLAIF critique-revise stage
4
- to evaluate and improve generated output. Each rule maps to a validator check.
5
-
6
- ---
7
-
8
- ## Card Content Model
9
-
10
- - Every `Card` MUST have at least one of: `Header`, `Section`, `Footer`.
11
- - `Section` content MUST be wrapped in a `Column` component, not placed directly.
12
- - Headings (`h1`-`h6`) belong in `Header`, not `Section`.
13
- - Action buttons belong in `Footer`, not `Section` or `Header` (unless using `slot="action"`).
14
- - `Header` supports 4 slots: `slot="icon"` (leading), `slot="heading"` (title), `slot="description"` (subtitle), `slot="action"` (trailing badge/button).
15
-
16
- ## Typography & Variant System
17
-
18
- - Use semantic HTML elements with `variant` attribute: `<h1 variant="title">`, `<h3 variant="section">`, `<p variant="body">`.
19
- - Never use `data-heading` — use `variant` instead.
20
- - Text hierarchy: display > title > heading > section > subsection > body > deck > caption > kicker > label.
21
- - Use `color="subtle"` or `color="muted"` for secondary text, not inline styles.
22
-
23
- ## Layout Primitives
24
-
25
- - `Column` (col-ui): vertical stack. Use numeric `gap` values: `gap="2"`, `gap="4"`, `gap="6"`.
26
- - `Row` (row-ui): horizontal. Use `gap`, `justify`, `align` attributes.
27
- - `Grid` (grid-ui): CSS grid. Use `columns="2|3|4"`.
28
- - Never use named gaps (`gap="sm"`, `gap="md"`, `gap="lg"`) — always numeric.
29
- - Never use inline `style` attributes for layout.
30
- - Never use CSS class names.
31
-
32
- ## Component Types
33
-
34
- - Use `Input` for text inputs (not `TextField` or `TextInput`).
35
- - Use `CheckBox` for checkboxes (not `Checkbox`).
36
- - Use `Select` for dropdowns (not `ChoicePicker`).
37
- - Use `Toggle` for switch controls.
38
- - Use `Button` with `variant="primary"` for main CTA, `variant="outline"` for secondary.
39
- - Button text via `text` prop, icon via `icon` prop (Phosphor icon name).
40
-
41
- ## Flat Adjacency Protocol
42
-
43
- - Every component has a unique `id`.
44
- - Root component MUST have `id: "root"`.
45
- - Children referenced by ID array: `children: ["child-1", "child-2"]`.
46
- - IDs should be short and descriptive: `"hdr"`, `"email-field"`, `"submit-btn"`.
47
- - No circular references.
48
- - No orphaned components (every non-root must be referenced by a parent).
49
-
50
- ## Anti-Patterns (DO NOT)
51
-
52
- 1. **No bare divs** — never use `<div>` or HTML-only elements. Use AdiaUI components.
53
- 2. **No flat adjacency violations** — every Text inside Section must be inside a Column wrapper.
54
- 3. **No heading hierarchy skips** — don't jump from h1 to h3.
55
- 4. **No duplicate IDs** — every component must have a unique ID.
56
- 5. **No content directly in Tab** — Tab is a button strip item, not a content container.
57
- 6. **No invented components** — only use types registered in the A2UI registry.
58
- 7. **No slot on containers** — `slot="heading"` goes on the heading element, not on Header.
59
- 8. **No inline styles** — use component props and tokens, not `style="..."`.
60
-
61
- ## Prose & Content Context
62
-
63
- - For marketing/content pages, wrap in `<section prose>` to shift to content-optimized typography.
64
- - Centered page headers use: `<header align="center" size="lg">` → `<col-ui gap="4">` → kicker/display/deck.
65
- - Use `nomargin` on typography elements inside cards to prevent double-spacing.
66
-
67
- ## Icon Names
68
-
69
- - Use Phosphor icon names: `arrow-right`, `check-circle`, `warning-circle`, etc.
70
- - Brand icons need `-logo` suffix: `google-logo`, `github-logo`, `twitter-logo`.
71
- - Common aliases are resolved automatically: `send`→`paper-plane-right`, `settings`→`gear`, `mail`→`envelope`.
72
-
73
- ## Accessibility
74
-
75
- - Interactive elements must have `aria-label` if no visible text.
76
- - Icon-only buttons must have `aria-label`.
77
- - Images must have `alt` text.
78
- - Form inputs must have `label` prop.
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes