# Strategy engines, zettel file map, strategy labels, issue telemetry

## File map (`packages/gen-ui/a2ui/compose/strategies/zettel/`, verified 2026-07)

```text
generator-adapter.js   ← zettel entry: retrieval → strong-match / chunk-synthesis bridge / atoms
generate.js            ← thin wrapper for direct invocation
_smoke.js              ← in-tree smoke (npm run zettel:smoke)
composition-library.js ← loads corpus/chunks/*.json, normalizes to composition shape, searchAll()
composer.js            ← resolveComposition(): defensive copy + strip pass (see fragment note)
session-store.js       ← multi-turn artifact tracking (in-memory analytics; no iteration branch)
chunk-synthesizer.js   ← page-shell + slot-binding synthesis (chunk-corpus, 2-tier)
chunk-composer.js      ← resolves a chunk plan into A2UI messages; slot/kind validation
chunk-refiner.js       ← multi-turn refinement (locator → modifier two-pass)
state-cache.js         ← bounded LRU keyed by state_id
issue-reporter.js      ← runtime telemetry (see below)
```

**Fragments are retired.** `fragment-library.js` / `synthesizer.js` no longer
exist; `composition-library.js` is the renamed successor and the harvested
chunk corpus is the only retrieval substrate. `composer.js` is a passthrough:
any lingering `$fragment` ref renders as a visible placeholder to surface
corpus drift. Never author new `$fragment` refs.

## Two engines under one directory

- **`zettel`** (`generator-adapter.js`): `searchAll()` over compositions →
  strong match emits verbatim; weak/no match bridges to chunk-synthesis when an
  LLM adapter is present; no LLM → `fragment-candidates` (atoms for downstream
  assembly).
- **`chunk-zettel`**: chunk-corpus synthesis + `chunk-refiner.js` for
  history-aware iteration. The zettel session-iteration branch was retired, turn≥2 on `zettel` takes the same path as turn 1 (fresh retrieval). True
  modify-an-existing-canvas work goes through `chunk-zettel` + `state_id`.

Both register independently in `strategies/registry.js`.

## Strategy labels (public contract, emitted to the eval harness)

| Label | Trigger | LLM call? |
| --- | --- | --- |
| `composition-match` | Fresh retrieval, `searchAll` score ≥ `STRONG_MATCH_THRESHOLD` (40) | No |
| `composition-synthesized` | Weak retrieval, chunk-synthesis bridge succeeded | Yes |
| `synthesis-failed` | LLM tried + failed validation | Yes (failed) |
| `fragment-candidates` | Weak retrieval + no LLM available → atoms only | No |

The eval harness scores per-label distribution; a calibration tweak that shifts
the distribution shifts the score. Don't rename labels without a coordinated
migration, eval, MCP tools, and dialog-recorder pattern-match on the strings.

## Issue reporter, three call paths (`issue-reporter.js`)

| Path | Trigger | `reporter` | Suppression |
| --- | --- | --- | --- |
| LLM self-fire | `report_issue` MCP tool | `llm` | none |
| Consumer-fire | Human requests a record | `user` | none |
| Engine auto-fire | Internal failure (scope-drift, synthesis-failed) | `auto` | when `ctx.evalMode` is true |

Records land in an engine-internal store (`DEFAULT_STORAGE_ROOT` in
`issue-reporter.js`); traces >200KB spill to a sidecar `.trace.json`. The store
is scratch telemetry, durable tracking of recurring patterns belongs in GitHub
issues / PR descriptions. Type taxonomy: `bug` / `training-gap` /
`protocol-gap` / `ux-feedback`; severity `nit` < `drift` < `blocker`; owner
`synthesis | retrieval | validator | chunk-corpus | mcp-protocol | unknown`.

## Closed-loop validation, chunk-zettel is deliberately NOT wired in

Every LLM-calling engine outside this directory (`generate-pro`,
`generate-thinking`) routes its final candidate through the shared
`packages/gen-ui/a2ui/compose/shared/validate-and-repair.js`, full schema +
Ajv catalog + anti-pattern conformance, orthogonal to whatever narrower
check the engine already runs. `chunk-zettel` (`chunk-synthesizer.js`'s
`composeFromIntent`) does NOT get this: its result is a raw HTML
**string**, wrapped by the caller (`registry.js`, `generator-adapter.js`)
into a single node with an unregistered `component: 'article'` type, running full schema validation against that would report `invalid` on
every single output, unconditionally, regardless of actual quality. See
TKT-0009 for the three-direction decision this is waiting on before any
change lands. `chunk-zettel`'s own `validatePlan` (chunk-existence/slot
checks) remains its complete validation contract for now.

Separately, `harvest-chunks.mjs` (corpus admission) now runs every
templated chunk through the same shared module (validate-only) at
harvest time, report-only by default (`npm run harvest:chunks:dry`
shows the results); `--strict` enforcement is deferred to TKT-0010
(27% of the corpus currently fails, mostly a schema-generation gap
around `data-*`/`span` attributes rather than corpus-content defects).

## Provider thinking opt-in kill switch (gh#3516, LLD-3516 slice 1)

`monolithic-thinking` and `free-form-composer` opt into provider thinking at
a fixed strategy-level budget (`THINKING_BUDGET_MONOLITHIC_THINKING`,
`THINKING_BUDGET_FREE_FORM`, both 4000 as of gh#3516 slice 3) via the shared
`compose/shared/thinking-opt-in.js` helper. `ADIA_THINKING_OPT_IN=0` drops
the `thinking`/`thinkingBudget` fields at both call sites entirely (not
`thinking: false`, the adapter never sees the keys), giving the
thinking-off baseline a runtime lever instead of a code revert. Unset (or
`"1"`) keeps current behavior. This is an operations knob, not a caller
option: no consumer threads it through `generateUI()`.

## Pitfalls

- **`STRONG_MATCH_THRESHOLD` was raised 22 → 40 post-incident.** Lowering it
  reverts to repetitive verbatim output. If retrieval feels cold, profile the
  score distribution via `searchAll()` debugging first, don't lower the gate.
- **`state-cache` is per-process**, multi-turn breaks across MCP server
  restarts; the client must re-run `compose_from_chunks` for a fresh
  `state_id`. Nothing durable belongs in state-cache.
- **`issueAccumulator` must be passed via `opts`** to refinement engines, a
  newly wired engine that skips it silently drops auto-fired issues.
- **`PRE_SEARCH_LIMIT = 30` is kind-aware, not linear**, see
  [zettel-calibration](zettel-calibration.md) before judging it over-permissive.

## Verification

```bash
npm run zettel:smoke                       # fast, no LLM
npm run smoke:engines
npm run eval:diff -- --engine zettel       # slow, real LLM; floors in SKILL.md
npm run eval:diff -- --engine chunk-zettel
```
