@adia-ai/a2ui-compose 0.0.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.
- package/CHANGELOG.md +86 -0
- package/README.md +181 -0
- package/engine/artifacts.js +262 -0
- package/engine/constitution.md +78 -0
- package/engine/context-store.js +218 -0
- package/engine/generator.js +500 -0
- package/engine/pattern-export.js +149 -0
- package/engine/pipeline/engine.js +289 -0
- package/engine/pipeline/types.js +91 -0
- package/engine/reference.js +115 -0
- package/engine/state.js +15 -0
- package/engines/monolithic/_shared.js +1320 -0
- package/engines/monolithic/generate-instant.js +229 -0
- package/engines/monolithic/generate-pro.js +367 -0
- package/engines/monolithic/generate-thinking.js +211 -0
- package/engines/registry.js +195 -0
- package/engines/zettel/_smoke.js +37 -0
- package/engines/zettel/composer.js +146 -0
- package/engines/zettel/fragment-library.js +209 -0
- package/engines/zettel/generate.js +15 -0
- package/engines/zettel/generator-adapter.js +202 -0
- package/engines/zettel/session-store.js +121 -0
- package/engines/zettel/synthesizer.js +343 -0
- package/evals/harness.mjs +193 -0
- package/index.js +16 -0
- package/llm/adapters/anthropic.js +106 -0
- package/llm/adapters/gemini.js +99 -0
- package/llm/adapters/index.js +138 -0
- package/llm/adapters/openai.js +85 -0
- package/llm/adapters/sse.js +50 -0
- package/llm/llm-bridge.js +214 -0
- package/llm/llm-stub.js +69 -0
- package/package.json +41 -0
- package/transpiler/transpiler-maps.js +277 -0
- package/transpiler/transpiler.js +820 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Changelog — @adia-ai/a2ui-compose
|
|
2
|
+
|
|
3
|
+
Follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
4
|
+
|
|
5
|
+
Scope: generation engines (monolithic + zettel), LLM adapters,
|
|
6
|
+
transpiler, evals. Retrieval + validation now ship as sibling
|
|
7
|
+
packages (`@adia-ai/a2ui-retrieval`, `@adia-ai/a2ui-validator`) so
|
|
8
|
+
non-compose consumers can depend on them without pulling the
|
|
9
|
+
generator graph.
|
|
10
|
+
|
|
11
|
+
## [Unreleased]
|
|
12
|
+
|
|
13
|
+
_Nothing yet._
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## [0.0.1] - 2026-04-24
|
|
18
|
+
|
|
19
|
+
First public release. Framework-agnostic compose engine for the
|
|
20
|
+
A2UI protocol: takes natural-language intents + a catalog and
|
|
21
|
+
produces A2UI protocol messages. Two generation strategies ship
|
|
22
|
+
behind a plug-in engine registry.
|
|
23
|
+
|
|
24
|
+
### Included
|
|
25
|
+
|
|
26
|
+
- **Monolithic engine** (`engine/generator.js`) — single-call
|
|
27
|
+
generator with `instant` / `pro` / `thinking` modes. Anthropic
|
|
28
|
+
and OpenAI LLM adapters included. Pattern-match-only `instant`
|
|
29
|
+
mode for deterministic output; `pro` + `thinking` call the LLM.
|
|
30
|
+
- **Zettel engine** (`engines/zettel/`) — fragment-graph composition
|
|
31
|
+
strategy that assembles UI from reusable fragments + compositions
|
|
32
|
+
via a backlink graph. Framework-agnostic; driven by the same
|
|
33
|
+
intent-gate + domain-router as monolithic.
|
|
34
|
+
- **Engine registry** (`engines/registry.js`) — plug-in API.
|
|
35
|
+
`registerEngine(name, fn)` + `pick(name)` dispatcher;
|
|
36
|
+
reserved names for built-ins.
|
|
37
|
+
- **LLM bridge** (`llm/llm-bridge.js`) — unified interface over
|
|
38
|
+
Anthropic, OpenAI, and Gemini adapters. Streaming + non-streaming
|
|
39
|
+
calls; prompt-cache support for Anthropic.
|
|
40
|
+
- **Transpiler** (`transpiler/transpiler.js`) — A2UI JSON → HTML
|
|
41
|
+
rendering; used by MCP tools and the visual-validate pipeline.
|
|
42
|
+
- **Evals harness** (`evals/harness.mjs`) — V2 cross-engine eval
|
|
43
|
+
runner. Consumed by `eval-diff.mjs` in `@adia-ai/a2ui-mcp`.
|
|
44
|
+
- **Index barrel** (`index.js`) — re-exports `generateUI`,
|
|
45
|
+
`generateUIStream`, `pick`, `listEngines`, `registerEngine`,
|
|
46
|
+
`unregisterEngine`, `ENGINES` for one-import consumer surface.
|
|
47
|
+
|
|
48
|
+
### Dependencies
|
|
49
|
+
|
|
50
|
+
- `@adia-ai/a2ui-utils` ^0.0.2 — A2UI runtime primitives.
|
|
51
|
+
- `@adia-ai/a2ui-retrieval` ^0.0.1 — retrieval layer.
|
|
52
|
+
- `@adia-ai/a2ui-validator` ^0.0.1 — validation layer.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Pre-0.0.1 history
|
|
57
|
+
|
|
58
|
+
### Changed
|
|
59
|
+
- **A2UI runtime dependency relocated.** `retrieval/catalog.js`,
|
|
60
|
+
`transpiler/transpiler-maps.js`, and `validation/validator.js` now
|
|
61
|
+
import `registry` and `wiringRegistry` from the new published package
|
|
62
|
+
`@adia-ai/a2ui-utils` instead of `../../web-components/a2ui/`
|
|
63
|
+
(which no longer exists — the runtime moved out of web-components in
|
|
64
|
+
`@adia-ai/web-components@0.0.4`). The scoped import is resolved via
|
|
65
|
+
the workspace symlink in dev and the transitive dependency in
|
|
66
|
+
published installs.
|
|
67
|
+
- `retrieval/anti-patterns.js` — `noInventedComponents` regex broadened from `/<([a-z]+-n)\b/` to `/<([a-z]+-(?:n|ui))\b/` so the check actually fires against the current `-ui` codebase. Allowlist aligned with canonical registry.
|
|
68
|
+
- `transpiler/transpiler.js` and `transpiler/transpiler-maps.js` — stale `-n` tag references updated to `-ui` forms consistent with `packages/web-components/a2ui/registry.js`.
|
|
69
|
+
- `retrieval/catalog.js`, `retrieval/synthetic-data.js`, `engines/monolithic/_shared.js` — internal tag references audited and aligned with the canonical registry.
|
|
70
|
+
|
|
71
|
+
### Fixed
|
|
72
|
+
- Stale internal-identifier references in comments + doc strings swept to the current naming.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## [0.1.0] — internal baseline (unreleased)
|
|
77
|
+
|
|
78
|
+
Initial version at the time the monorepo was established. Contains:
|
|
79
|
+
|
|
80
|
+
- **Monolithic engine** — single-call generator with instant / pro / thinking modes (Anthropic, OpenAI adapters).
|
|
81
|
+
- **Zettel engine** — fragment-graph composition engine using leverage-ranked fragments from the training corpus.
|
|
82
|
+
- **Retrieval layer** — keyword + concept-tag + component-signature scoring. Dialog recorder writes per-turn JSON to `logs/dialogs/`.
|
|
83
|
+
- **Validator** — 15-check weighted schema validator; rejects `_fallback: true` surfaces as score 0.
|
|
84
|
+
- **Prompt caching** — enabled on Anthropic calls as of 2026-04-19.
|
|
85
|
+
|
|
86
|
+
Package name still uses the legacy `@adia-ai/a2ui-compose` scope pending rename (see root CHANGELOG for context).
|
package/README.md
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# @adia-ai/a2ui-compose
|
|
2
|
+
|
|
3
|
+
Framework-agnostic UI generation engine. Takes a natural-language intent +
|
|
4
|
+
an A2UI component catalog and produces a tree of A2UI protocol messages
|
|
5
|
+
ready for a renderer.
|
|
6
|
+
|
|
7
|
+
> This package is pipeline runtime only. UI components live in
|
|
8
|
+
> [`@adia-ai/web-components`](../web-components); the A2UI runtime (renderer,
|
|
9
|
+
> registry, streams, wiring) in [`@adia-ai/a2ui-utils`](../a2ui/utils);
|
|
10
|
+
> the pattern corpus in [`@adia-ai/a2ui-corpus`](../corpus);
|
|
11
|
+
> the MCP server in [`@adia-ai/a2ui-mcp`](../mcp).
|
|
12
|
+
>
|
|
13
|
+
> Note: this package still lives under the legacy `@adiahealth` scope in its
|
|
14
|
+
> own `package.json` name. The `@adia-ai` scope is the public npm scope
|
|
15
|
+
> (only `@adia-ai/web-components` and `@adia-ai/a2ui-utils` are currently
|
|
16
|
+
> published).
|
|
17
|
+
|
|
18
|
+
## What it does
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
intent ─▶ classify ─▶ retrieve ─▶ compose / adapt ─▶ validate ─▶ A2UI
|
|
22
|
+
(concepts) (patterns) (engine-specific) (score ≥70) JSON
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
One entry point, two generation strategies, pluggable LLM back-end.
|
|
26
|
+
|
|
27
|
+
```javascript
|
|
28
|
+
import { generateUI } from '@adia-ai/a2ui-compose/engine';
|
|
29
|
+
|
|
30
|
+
const result = await generateUI({
|
|
31
|
+
intent: 'login form with email, password, and remember-me',
|
|
32
|
+
engine: 'zettel', // 'monolithic' | 'zettel'
|
|
33
|
+
mode: 'pro', // monolithic only: instant | pro | thinking
|
|
34
|
+
model: 'claude-sonnet-4-7',
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// result.components — A2UI message array
|
|
38
|
+
// result.validation — { score, checks, warnings }
|
|
39
|
+
// result.debug — pattern matches, LLM prompt, token usage, …
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Layout
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
gen-ui/
|
|
46
|
+
├── engine/
|
|
47
|
+
│ └── generator.js generateUI() — the one public entry point
|
|
48
|
+
│
|
|
49
|
+
├── engines/ pluggable strategies via registerEngine()
|
|
50
|
+
│ ├── registry.js engine selector + reserved-name guard
|
|
51
|
+
│ ├── monolithic/ pattern-match + LLM-adapt (3 modes)
|
|
52
|
+
│ │ ├── generate-instant.js no LLM — pattern-match only
|
|
53
|
+
│ │ ├── generate-pro.js pattern + LLM adaptation, non-streaming
|
|
54
|
+
│ │ └── generate-thinking.js streaming LLM + repair loop
|
|
55
|
+
│ └── zettel/ fragment-graph composition
|
|
56
|
+
│ ├── generator-adapter.js entry point
|
|
57
|
+
│ ├── composer.js assembles fragments → compositions
|
|
58
|
+
│ └── session.js multi-turn state
|
|
59
|
+
│
|
|
60
|
+
├── retrieval/
|
|
61
|
+
│ ├── catalog.js loads component schemas from sibling .a2ui.json
|
|
62
|
+
│ ├── pattern-library.js keyword-ranked pattern search (corpus + embeddings)
|
|
63
|
+
│ ├── fragments.js atomic-shape lookup for zettel
|
|
64
|
+
│ ├── anti-patterns.js catalog of canonical anti-patterns
|
|
65
|
+
│ └── feedback-store.js accumulates user feedback → disk
|
|
66
|
+
│
|
|
67
|
+
├── llm/
|
|
68
|
+
│ ├── llm-bridge.js unified adapter (Anthropic / OpenAI / Gemini)
|
|
69
|
+
│ ├── env.js Vite + Node env-var routing
|
|
70
|
+
│ └── prompts/ system prompts per engine mode
|
|
71
|
+
│
|
|
72
|
+
├── validation/
|
|
73
|
+
│ └── validator.js 15-check A2UI validator, weighted 0–100 score
|
|
74
|
+
│
|
|
75
|
+
├── intelligence/ intent classification + concept extraction
|
|
76
|
+
│ ├── classifier.js
|
|
77
|
+
│ ├── concepts.js
|
|
78
|
+
│ └── steelman.js
|
|
79
|
+
│
|
|
80
|
+
└── evals/
|
|
81
|
+
└── harness.mjs held-out intent benchmark runner
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Engines
|
|
85
|
+
|
|
86
|
+
**Monolithic** — pattern-match against full-canvas templates, optionally
|
|
87
|
+
adapt via LLM. Three modes:
|
|
88
|
+
|
|
89
|
+
| Mode | LLM? | Speed | Use for |
|
|
90
|
+
|------------|------|--------|-------------------------------------------------|
|
|
91
|
+
| `instant` | no | <50ms | High-confidence intents with exact pattern hit |
|
|
92
|
+
| `pro` | yes | ~2s | Most requests — adapt a template to the intent |
|
|
93
|
+
| `thinking` | yes | ~5s | Complex requests; streams + runs a repair loop |
|
|
94
|
+
|
|
95
|
+
**Zettel** — fragment-graph composition. Retrieves atomic fragments
|
|
96
|
+
(form-field, card-header, action-row, …) by keyword + concept-tag overlap
|
|
97
|
+
and assembles them into compositions. Verbatim retrieval above a threshold
|
|
98
|
+
(score ≥ 40); LLM synthesis from fragments below. Preserves session state
|
|
99
|
+
across multi-turn iterations.
|
|
100
|
+
|
|
101
|
+
```javascript
|
|
102
|
+
import { registerEngine } from '@adia-ai/a2ui-compose/engines/registry';
|
|
103
|
+
|
|
104
|
+
registerEngine('my-engine', async (ctx) => {
|
|
105
|
+
// ctx: intent, catalog, patterns, concepts, session, llm, …
|
|
106
|
+
return { components: [...], validation: {...}, debug: {...} };
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Reserved names: `monolithic`, `monolithic-*`, `zettel`, `mcp`.
|
|
111
|
+
|
|
112
|
+
## Validation
|
|
113
|
+
|
|
114
|
+
Every generated output runs through `validation/validator.js` — 15 weighted
|
|
115
|
+
checks covering structural validity, card/grid conventions, intent
|
|
116
|
+
alignment (F1), and anti-patterns. Result:
|
|
117
|
+
|
|
118
|
+
```javascript
|
|
119
|
+
{
|
|
120
|
+
score: 92, // 0-100
|
|
121
|
+
passed: true, // score ≥ 70
|
|
122
|
+
checks: [
|
|
123
|
+
{ id: 'structure', ok: true, weight: 10 },
|
|
124
|
+
{ id: 'intent-f1', ok: true, weight: 8, value: 0.84 },
|
|
125
|
+
{ id: 'card-grid', ok: true, weight: 6 },
|
|
126
|
+
{ id: 'anti-pattern', ok: false, weight: 4, hit: 'chart-legend' },
|
|
127
|
+
…
|
|
128
|
+
]
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`_fallback` surfaces score 0 by design — ensure the engine returns real
|
|
133
|
+
output, not a safety net.
|
|
134
|
+
|
|
135
|
+
## LLM bridge
|
|
136
|
+
|
|
137
|
+
Multi-provider adapter with a common interface:
|
|
138
|
+
|
|
139
|
+
```javascript
|
|
140
|
+
import { getAdapter } from '@adia-ai/a2ui-compose/llm';
|
|
141
|
+
|
|
142
|
+
const adapter = getAdapter('anthropic'); // or 'openai', 'gemini'
|
|
143
|
+
const stream = await adapter.streamChat({ model, messages, tools });
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Env-var routing via `llm/env.js` — works under Node (`process.env`) and
|
|
147
|
+
Vite (`import.meta.env`). Browser calls proxy through `server.js` at the
|
|
148
|
+
repo root (holds API keys); Node calls go direct.
|
|
149
|
+
|
|
150
|
+
## Evals
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
npm run evals # held-out intent benchmark
|
|
154
|
+
npm run eval:diff -- --engine zettel # diff against baseline
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The held-out fixture lives in `gen-ui-training/evals/held-out.jsonl`.
|
|
158
|
+
Regression thresholds the pipeline must hold:
|
|
159
|
+
|
|
160
|
+
- Zettel coverage **100%**, avgScore **≥ 88**, MRR **≥ 0.94**
|
|
161
|
+
- Monolithic coverage **100%**, avgScore **≥ 95**
|
|
162
|
+
- Fragment reuse ratio **≥ 29.9%** (167 refs / 559 nodes)
|
|
163
|
+
|
|
164
|
+
Full gate sweep: see `AGENTS.md` at repo root, or run `/verification-sweep`.
|
|
165
|
+
|
|
166
|
+
## Gotchas
|
|
167
|
+
|
|
168
|
+
- **Component catalog is read-only.** `.a2ui.json` sidecars in
|
|
169
|
+
`web-components/components/*/` are build outputs; edit the sibling YAML
|
|
170
|
+
instead.
|
|
171
|
+
- **Zettel loading is lazy.** The corpus is only parsed on first zettel
|
|
172
|
+
call — avoids Node `fs`/`path` imports reaching the browser bundle.
|
|
173
|
+
- **Validator score ≥ 70 is required** for downstream consumers to trust
|
|
174
|
+
the output. Below that, callers should treat the tree as advisory.
|
|
175
|
+
- **Engine name reservations** are enforced at registration time —
|
|
176
|
+
`registerEngine('zettel-v2', …)` passes; `registerEngine('zettel', …)`
|
|
177
|
+
throws.
|
|
178
|
+
|
|
179
|
+
## License
|
|
180
|
+
|
|
181
|
+
MIT
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ArtifactStore — In-memory artifact storage for pipeline executions.
|
|
3
|
+
*
|
|
4
|
+
* Stores A2UI message sequences keyed by executionId, with multi-turn
|
|
5
|
+
* history (iterations). Each turn records the messages, optional HTML
|
|
6
|
+
* render, a human-readable summary, and the intent that produced it.
|
|
7
|
+
*
|
|
8
|
+
* Drift detection: measures structural divergence between turns to flag
|
|
9
|
+
* scope creep in iterative generation sessions.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export class ArtifactStore {
|
|
13
|
+
#artifacts = new Map(); // executionId → { turns: [], metadata }
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Record a turn (iteration) for an execution.
|
|
17
|
+
*
|
|
18
|
+
* @param {string} executionId
|
|
19
|
+
* @param {object} opts
|
|
20
|
+
* @param {object[]} opts.messages — A2UI message sequence
|
|
21
|
+
* @param {string} [opts.html] — Optional rendered HTML snapshot
|
|
22
|
+
* @param {string} [opts.summary] — Human-readable description
|
|
23
|
+
* @param {object} [opts.validation] — Validation result from validateSchema
|
|
24
|
+
* @param {string} [opts.intent] — The user intent for this turn
|
|
25
|
+
*/
|
|
26
|
+
record(executionId, { messages, html = null, summary = '', validation = null, intent = '', analysis = null }) {
|
|
27
|
+
let artifact = this.#artifacts.get(executionId);
|
|
28
|
+
if (!artifact) {
|
|
29
|
+
artifact = {
|
|
30
|
+
executionId,
|
|
31
|
+
turns: [],
|
|
32
|
+
metadata: {
|
|
33
|
+
created: Date.now(),
|
|
34
|
+
updated: Date.now(),
|
|
35
|
+
originalIntent: intent || summary || '',
|
|
36
|
+
// Stash the first turn's analyzer output so iteration turns can
|
|
37
|
+
// recover the original brief's concepts/entities/components for
|
|
38
|
+
// canvas-aware downstream decisions (e.g. suggestion generation,
|
|
39
|
+
// drift detection). Iteration turns skip the analyzer, so this is
|
|
40
|
+
// the only durable handle on what the user originally asked for.
|
|
41
|
+
originalAnalysis: analysis || null,
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
this.#artifacts.set(executionId, artifact);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Extract structural snapshot for drift tracking
|
|
48
|
+
const components = messages?.flatMap(m => m.components || []) || [];
|
|
49
|
+
const snapshot = _buildStructuralSnapshot(components);
|
|
50
|
+
|
|
51
|
+
artifact.turns.push({
|
|
52
|
+
turn: artifact.turns.length,
|
|
53
|
+
messages,
|
|
54
|
+
html,
|
|
55
|
+
summary,
|
|
56
|
+
validation,
|
|
57
|
+
intent: intent || summary || '',
|
|
58
|
+
snapshot,
|
|
59
|
+
timestamp: Date.now(),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
artifact.metadata.updated = Date.now();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get a specific turn for an execution.
|
|
67
|
+
*
|
|
68
|
+
* @param {string} executionId
|
|
69
|
+
* @param {number} [turn=-1] — Turn index (negative counts from end; -1 = latest)
|
|
70
|
+
* @returns {object|null}
|
|
71
|
+
*/
|
|
72
|
+
get(executionId, turn = -1) {
|
|
73
|
+
const artifact = this.#artifacts.get(executionId);
|
|
74
|
+
if (!artifact || artifact.turns.length === 0) return null;
|
|
75
|
+
|
|
76
|
+
const idx = turn < 0 ? artifact.turns.length + turn : turn;
|
|
77
|
+
return artifact.turns[idx] ?? null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get all turns for an execution.
|
|
82
|
+
*
|
|
83
|
+
* @param {string} executionId
|
|
84
|
+
* @returns {object[]|null}
|
|
85
|
+
*/
|
|
86
|
+
getAll(executionId) {
|
|
87
|
+
const artifact = this.#artifacts.get(executionId);
|
|
88
|
+
if (!artifact) return null;
|
|
89
|
+
return [...artifact.turns];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get the original (first turn) prompt analyzer output for an execution.
|
|
94
|
+
* Used by iteration turns that need to recover the original concepts /
|
|
95
|
+
* implied components / steelman to drive canvas-aware decisions.
|
|
96
|
+
* @param {string} executionId
|
|
97
|
+
* @returns {object|null}
|
|
98
|
+
*/
|
|
99
|
+
getOriginalAnalysis(executionId) {
|
|
100
|
+
const artifact = this.#artifacts.get(executionId);
|
|
101
|
+
return artifact?.metadata?.originalAnalysis || null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get the original (first turn) intent for an execution.
|
|
106
|
+
* @param {string} executionId
|
|
107
|
+
* @returns {string|null}
|
|
108
|
+
*/
|
|
109
|
+
getOriginalIntent(executionId) {
|
|
110
|
+
const artifact = this.#artifacts.get(executionId);
|
|
111
|
+
return artifact?.metadata?.originalIntent || null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Compute drift metrics between the first turn and the latest turn.
|
|
116
|
+
*
|
|
117
|
+
* Returns null if there are fewer than 2 turns.
|
|
118
|
+
*
|
|
119
|
+
* @param {string} executionId
|
|
120
|
+
* @returns {{ componentGrowth: number, sectionGrowth: number, newSections: string[], driftScore: number, turnCount: number, warning: string|null }|null}
|
|
121
|
+
*/
|
|
122
|
+
getDriftMetrics(executionId) {
|
|
123
|
+
const artifact = this.#artifacts.get(executionId);
|
|
124
|
+
if (!artifact || artifact.turns.length < 2) return null;
|
|
125
|
+
|
|
126
|
+
const first = artifact.turns[0].snapshot;
|
|
127
|
+
const latest = artifact.turns[artifact.turns.length - 1].snapshot;
|
|
128
|
+
if (!first || !latest) return null;
|
|
129
|
+
|
|
130
|
+
// Component count growth ratio
|
|
131
|
+
const componentGrowth = first.componentCount > 0
|
|
132
|
+
? latest.componentCount / first.componentCount
|
|
133
|
+
: latest.componentCount;
|
|
134
|
+
|
|
135
|
+
// Section count growth ratio
|
|
136
|
+
const sectionGrowth = first.sectionCount > 0
|
|
137
|
+
? latest.sectionCount / first.sectionCount
|
|
138
|
+
: latest.sectionCount;
|
|
139
|
+
|
|
140
|
+
// New sections added since turn 0
|
|
141
|
+
const originalSections = new Set(first.sectionNames);
|
|
142
|
+
const newSections = latest.sectionNames.filter(s => !originalSections.has(s));
|
|
143
|
+
|
|
144
|
+
// Component type diversity change
|
|
145
|
+
const originalTypes = new Set(first.componentTypes);
|
|
146
|
+
const newTypes = latest.componentTypes.filter(t => !originalTypes.has(t));
|
|
147
|
+
|
|
148
|
+
// Drift score: 0 = no drift, 1 = extreme drift
|
|
149
|
+
// Weighted: 40% component growth, 30% section growth, 30% new sections
|
|
150
|
+
const growthFactor = Math.min(componentGrowth / 3, 1); // 3x = max
|
|
151
|
+
const sectionFactor = Math.min(sectionGrowth / 3, 1);
|
|
152
|
+
const newSectionFactor = Math.min(newSections.length / 4, 1); // 4+ new = max
|
|
153
|
+
const driftScore = Math.round((growthFactor * 0.4 + sectionFactor * 0.3 + newSectionFactor * 0.3) * 100) / 100;
|
|
154
|
+
|
|
155
|
+
// Warning thresholds
|
|
156
|
+
let warning = null;
|
|
157
|
+
if (driftScore >= 0.7) {
|
|
158
|
+
warning = `High canvas drift detected (score: ${driftScore}). Canvas grew from ${first.componentCount} to ${latest.componentCount} components across ${artifact.turns.length} turns. ${newSections.length} new sections added: ${newSections.join(', ')}. Consider confirming with user that this matches original intent: "${artifact.metadata.originalIntent}"`;
|
|
159
|
+
} else if (driftScore >= 0.4) {
|
|
160
|
+
warning = `Moderate canvas drift (score: ${driftScore}). Canvas: ${first.componentCount} → ${latest.componentCount} components. ${newSections.length} new sections added.`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
componentGrowth: Math.round(componentGrowth * 100) / 100,
|
|
165
|
+
sectionGrowth: Math.round(sectionGrowth * 100) / 100,
|
|
166
|
+
newSections,
|
|
167
|
+
newTypes,
|
|
168
|
+
driftScore,
|
|
169
|
+
turnCount: artifact.turns.length,
|
|
170
|
+
originalIntent: artifact.metadata.originalIntent,
|
|
171
|
+
warning,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* List all execution summaries.
|
|
177
|
+
*
|
|
178
|
+
* @returns {object[]}
|
|
179
|
+
*/
|
|
180
|
+
list() {
|
|
181
|
+
const results = [];
|
|
182
|
+
for (const [id, artifact] of this.#artifacts) {
|
|
183
|
+
const latest = artifact.turns[artifact.turns.length - 1];
|
|
184
|
+
results.push({
|
|
185
|
+
executionId: id,
|
|
186
|
+
turnCount: artifact.turns.length,
|
|
187
|
+
latestSummary: latest?.summary ?? '',
|
|
188
|
+
latestScore: latest?.validation?.score ?? null,
|
|
189
|
+
originalIntent: artifact.metadata.originalIntent ?? '',
|
|
190
|
+
created: artifact.metadata.created,
|
|
191
|
+
updated: artifact.metadata.updated,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
return results;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Delete an execution's artifacts.
|
|
199
|
+
*
|
|
200
|
+
* @param {string} executionId
|
|
201
|
+
* @returns {boolean} — true if deleted
|
|
202
|
+
*/
|
|
203
|
+
delete(executionId) {
|
|
204
|
+
return this.#artifacts.delete(executionId);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Clear all artifacts.
|
|
209
|
+
*/
|
|
210
|
+
clear() {
|
|
211
|
+
this.#artifacts.clear();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Number of stored executions */
|
|
215
|
+
get size() {
|
|
216
|
+
return this.#artifacts.size;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Build a structural snapshot of the canvas for drift comparison.
|
|
222
|
+
* Extracts component count, section names, component types —
|
|
223
|
+
* the metrics that indicate structural changes.
|
|
224
|
+
*
|
|
225
|
+
* @param {object[]} components — flat A2UI component array
|
|
226
|
+
* @returns {{ componentCount: number, sectionCount: number, sectionNames: string[], componentTypes: string[] }}
|
|
227
|
+
*/
|
|
228
|
+
function _buildStructuralSnapshot(components) {
|
|
229
|
+
if (!components || components.length === 0) {
|
|
230
|
+
return { componentCount: 0, sectionCount: 0, sectionNames: [], componentTypes: [] };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const sectionNames = [];
|
|
234
|
+
for (const c of components) {
|
|
235
|
+
if (c.component === 'Card') {
|
|
236
|
+
// Find the title of this Card section
|
|
237
|
+
const headerChild = components.find(
|
|
238
|
+
h => h.parentId === c.id && (h.component === 'Header' || h.component === 'CardHeader')
|
|
239
|
+
);
|
|
240
|
+
if (headerChild) {
|
|
241
|
+
const titleChild = components.find(
|
|
242
|
+
t => t.parentId === headerChild.id && t.component === 'Text'
|
|
243
|
+
);
|
|
244
|
+
sectionNames.push(titleChild?.text || titleChild?.children || headerChild.title || c.id);
|
|
245
|
+
} else {
|
|
246
|
+
sectionNames.push(c.id);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const typeSet = new Set();
|
|
252
|
+
for (const c of components) {
|
|
253
|
+
if (c.component) typeSet.add(c.component);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
componentCount: components.length,
|
|
258
|
+
sectionCount: sectionNames.length,
|
|
259
|
+
sectionNames,
|
|
260
|
+
componentTypes: [...typeSet].sort(),
|
|
261
|
+
};
|
|
262
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
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.
|