@adia-ai/a2ui-mcp 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -11,6 +11,37 @@ zettel strategies.
11
11
 
12
12
  _No pending changes._
13
13
 
14
+ ## [0.5.2] - 2026-05-13
15
+
16
+ ### Added — `eval:diff --report-substitutions` flag (§107a infra, v0.5.2)
17
+
18
+
19
+ `packages/a2ui/mcp/scripts/eval-diff.mjs` learns a new `--report-substitutions` flag. When set, captures per-intent substitution data from free-form plans (available substitutable nodes across resolved ingredients vs substitutions the LLM actually emitted) + emits a `## Substitution coverage (§107a)` section in `diff.md` with overall ratio + 3-bucket histogram (<30% / 30-50% / ≥50%) + top-20 under-substitution table.
20
+
21
+ Drives the F1-plateau question for v0.5.2: ratio <30% → §125 catalog-text sweep + §126 prompt iteration are high-leverage. Ratio >50% → F1 plateau is structural (catalog content or scorer artifact), revise the F1-lift plan.
22
+
23
+ Pre-baseline measurement (run `2026-05-13T22-29-12-085Z`, `--limit 30`): **17.2% overall substitution ratio**, 17/30 intents in the <30% bucket. Decision-rule outcome: §125 + §126 are high-leverage. Companion to `@adia-ai/a2ui-compose`'s `plan` first-class graduation.
24
+
25
+ Eval tooling only; no runtime behavior change.
26
+
27
+ ### Changed — drop dead `result._debug?.plan` fallback in `eval-diff.mjs` (§131, v0.5.2)
28
+
29
+ `eval-diff.mjs` line 147 previously read `const plan = result.plan || result._debug?.plan || null;` — a defensive fallback to the pre-§107a soft-API path. Since `@adia-ai/a2ui-compose@0.5.2` no longer populates `_debug.plan` (§107a graduated it to first-class; §131 documents the volatility contract), the fallback is dead code. Removed.
30
+
31
+ Eval tooling only; no runtime behavior change.
32
+
33
+ ### Added — `eval:diff --model <id>` flag for Haiku-vs-Opus A/B harness (§127 infra, v0.5.2)
34
+
35
+ `packages/a2ui/mcp/scripts/eval-diff.mjs` adds a `--model <model-id>` flag. When set, exports `FREE_FORM_MODEL_OVERRIDE` env var before any dynamic strategy imports — the override propagates to `@adia-ai/a2ui-compose@strategies/registry.js generateFreeFormAdapter` which reads the env var at call-time (post-§127-companion change). Lets the §127 A/B harness run Opus + Haiku full-100 evals without env-var setup or process restart.
36
+
37
+ Usage: `npm run eval:diff -- --engine free-form --model claude-opus-4-7 --report-substitutions`.
38
+
39
+ Default unchanged: Haiku 4.5 pin (`claude-haiku-4-5-20251001`) holds when `--model` is unset. Eval tooling only; no runtime behavior change.
40
+
41
+ ## [0.5.1] - 2026-05-13
42
+
43
+ _Lockstep ride-along (no source change)._
44
+
14
45
  ## [0.5.0] - 2026-05-13
15
46
 
16
47
  _Lockstep ride-along (no source change)._
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adia-ai/a2ui-mcp",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "AdiaUI A2UI MCP server. Exposes the compose engine over MCP with an engine selector for monolithic + zettel strategies.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -46,9 +46,24 @@ const opt = (k) => {
46
46
  const engine = opt('engine') || 'all';
47
47
  const limit = opt('limit') ? Number(opt('limit')) : undefined;
48
48
  const domain = opt('domain');
49
+ // §127 (v0.5.2): --model <id> override for free-form Haiku-vs-Opus A/B.
50
+ // Sets FREE_FORM_MODEL_OVERRIDE env var BEFORE generateUI is imported so
51
+ // the registry's module-time read picks it up. Without --model, the
52
+ // v0.5.1 §108 Haiku pin holds (claude-haiku-4-5-20251001).
53
+ const modelOverride = opt('model');
54
+ if (modelOverride) {
55
+ process.env.FREE_FORM_MODEL_OVERRIDE = modelOverride;
56
+ }
49
57
  // Shadow-mode semantic validator (Phase 1). Opt-in; zero effect on gating
50
58
  // when --gate-mode=structural (default).
51
59
  const semanticEnabled = args.includes('--semantic');
60
+ // §107a (v0.5.1): substitution-coverage report. When set, capture per-intent
61
+ // substitution data from free-form plans → emit a "Substitution coverage"
62
+ // section in diff.md. Targets the F1-plateau question: are we under-using
63
+ // the substitution surface (lift via §107b/c worth the cost), or is F1
64
+ // structural (catalog text quality or scorer artifact)?
65
+ const reportSubstitutions = args.includes('--report-substitutions');
66
+ const substitutionStats = []; // per-intent: { intent, ingredientCount, available, applied, ratio }
52
67
  // Phase 2 (gating mode). Default depends on --semantic:
53
68
  // without --semantic → 'structural' (no semantic work; Phase 1 baseline).
54
69
  // with --semantic → 'combined' since v0.1.2 (Phase 2 promotion); was
@@ -129,6 +144,39 @@ async function generateFreeFormCapture({ intent }) {
129
144
  if (semanticEnabled && Array.isArray(result.messages) && result.messages.length > 0) {
130
145
  capturedMessages.set(`free-form:${intent}`, result.messages);
131
146
  }
147
+
148
+ // §107a: capture substitution-coverage data when requested. Counts
149
+ // available substitutable nodes (Text/Button/Badge/Tag/Icon/Image/Link/Kbd)
150
+ // across the resolved ingredients vs substitutions the LLM emitted.
151
+ if (reportSubstitutions && result.strategy === 'free-form-composed') {
152
+ const SUBSTITUTABLE = new Set(['Text', 'Button', 'Badge', 'Tag', 'Kbd', 'Icon', 'Image', 'Link']);
153
+ let available = 0;
154
+ let applied = 0;
155
+ const plan = result.plan || null;
156
+ if (Array.isArray(result.messages) && result.messages.length > 0) {
157
+ const components = result.messages[0]?.components || [];
158
+ // Count substitutable nodes in the emitted tree (excludes the root).
159
+ for (const c of components) {
160
+ if (c.id === 'free-form-root') continue;
161
+ if (SUBSTITUTABLE.has(c.component)) available += 1;
162
+ }
163
+ }
164
+ if (plan && Array.isArray(plan.ingredients)) {
165
+ for (const ing of plan.ingredients) {
166
+ if (ing?.substitutions && typeof ing.substitutions === 'object') {
167
+ applied += Object.keys(ing.substitutions).length;
168
+ }
169
+ }
170
+ }
171
+ substitutionStats.push({
172
+ intent,
173
+ ingredientCount: result.usedIngredients?.length || 0,
174
+ available,
175
+ applied,
176
+ ratio: available > 0 ? applied / available : 0,
177
+ });
178
+ }
179
+
132
180
  return result;
133
181
  }
134
182
 
@@ -389,6 +437,47 @@ if (mcp && zettel) {
389
437
  const intent = (r.intent || '').slice(0, 48).replace(/\|/g, '\\|');
390
438
  md += `| ${r.id} | ${fmt(r.domain)} | ${intent} | ${fmt(r.validationScore)} | ${fmt(r.componentF1)} | ${fmt(r.strategy)} |\n`;
391
439
  }
440
+
441
+ // §107a (v0.5.1): Substitution coverage section. Surfaces the
442
+ // ratio of LLM-applied substitutions to available substitutable
443
+ // nodes. Drives the F1-plateau question: <30% = §107b/c high-leverage;
444
+ // >50% = F1 plateau is structural.
445
+ if (reportSubstitutions && substitutionStats.length > 0) {
446
+ const total = substitutionStats.length;
447
+ let totalAvailable = 0;
448
+ let totalApplied = 0;
449
+ let bucketLow = 0; // ratio < 0.3
450
+ let bucketMid = 0; // 0.3 ≤ ratio < 0.5
451
+ let bucketHigh = 0; // ≥ 0.5
452
+ for (const s of substitutionStats) {
453
+ totalAvailable += s.available;
454
+ totalApplied += s.applied;
455
+ if (s.available === 0) continue;
456
+ if (s.ratio < 0.3) bucketLow += 1;
457
+ else if (s.ratio < 0.5) bucketMid += 1;
458
+ else bucketHigh += 1;
459
+ }
460
+ const overallRatio = totalAvailable > 0 ? (totalApplied / totalAvailable * 100).toFixed(1) : 'n/a';
461
+ md += `\n## Substitution coverage (§107a)\n\n`;
462
+ md += `| metric | value |\n|---|---:|\n`;
463
+ md += `| intents measured | ${total} |\n`;
464
+ md += `| total substitutable nodes (across ingredients) | ${totalAvailable} |\n`;
465
+ md += `| substitutions applied by LLM | ${totalApplied} |\n`;
466
+ md += `| **overall ratio** | **${overallRatio}%** |\n`;
467
+ md += `| intents with ratio < 30% | ${bucketLow} |\n`;
468
+ md += `| intents with 30% ≤ ratio < 50% | ${bucketMid} |\n`;
469
+ md += `| intents with ratio ≥ 50% | ${bucketHigh} |\n\n`;
470
+
471
+ md += `### Per-intent substitution detail (top 20 by under-substitution)\n\n`;
472
+ md += `| intent | ingredients | available | applied | ratio |\n|---|---:|---:|---:|---:|\n`;
473
+ const sorted = substitutionStats.slice().sort((a, b) => a.ratio - b.ratio).slice(0, 20);
474
+ for (const s of sorted) {
475
+ const intent = s.intent.slice(0, 48).replace(/\|/g, '\\|');
476
+ md += `| ${intent} | ${s.ingredientCount} | ${s.available} | ${s.applied} | ${(s.ratio * 100).toFixed(0)}% |\n`;
477
+ }
478
+ md += `\n**Decision rule**: ratio < 30% → §107b/c high-leverage (catalog-text quality + system-prompt push). ratio > 50% → F1 plateau is structural; revise the F1 lift plan.\n`;
479
+ }
480
+
392
481
  await writeFile(join(outDir, 'diff.md'), md);
393
482
  console.error(`\n[eval-diff] wrote ${outDir}`);
394
483
  }