Skip to content

fix(web): coerce AKS composition outputs so the shared validator resolves them (#996)#1009

Merged
sabbour-squad-lead[bot] merged 1 commit into
mainfrom
squad/996-aks-composition
Apr 21, 2026
Merged

fix(web): coerce AKS composition outputs so the shared validator resolves them (#996)#1009
sabbour-squad-lead[bot] merged 1 commit into
mainfrom
squad/996-aks-composition

Conversation

@sabbour-squad-backend
Copy link
Copy Markdown
Contributor

Closes #996

Working as Bender (Backend Dev). Coordinates with PR #1000 — reuses the validateAndSanitizeComponents shared validator (commit 2561772) rather than forking it, per Nibbler's DP-review guidance.

Root cause

The AKS composition skill-chain intermittently emits envelopes with either:

  1. A bare pack component name (e.g. AksClusterCard) instead of the registry-qualified form (aks/AksClusterCard), or
  2. type as a legacy alias for component.

Both cases made validateAndSanitizeComponents treat the component as unknown and replace it with _ErrorComponent — the user-visible failure reported in #996.

Fix

Added a narrow coercion step inside the existing validator (not a parallel validator) in packages/web/src/contexts/A2UIRegistryContext.tsx:

  • typecomponent when component is absent.
  • Bare pack suffix → pack-qualified name when exactly one pack exposes that suffix. Ambiguous suffixes (e.g. shared across aks/* and azure/*) are left untouched so the validator still rejects them as unknown — per Zapp's DP condition to keep _ErrorComponent as the fail-loud fallback, never widen it into a best-effort guess.

The Zod-schema trust boundary (PR #989 / #1000) is unchanged. Malformed payloads still fall back to _ErrorComponent. Structured logs only name the offending component — no composition payload, no AKS/Azure identifiers — matching Zapp's log-hygiene rail.

Tests

New suite in a2ui-registry.test.ts7 new cases, all green:

  • Rewrites a unique bare pack name to its pack-qualified form.
  • Does NOT coerce an ambiguous bare name (suffix shared across packs).
  • Accepts type as a legacy alias for component.
  • Prefers explicit component over type when both are present.
  • Leaves already-qualified names untouched.
  • Core bare names win — does not rewrite a registered bare name to any pack/Name variant.
  • Regression guard for a realistic AKS composition envelope → zero _ErrorComponent entries.

Validation

Check Result
npm run lint ✅ 0 errors (61 pre-existing warnings, unchanged)
CI=1 npm test -- --reporter=dot 992 passed / 159 todo / 3 skipped — 87 files
npm run api:build (api:build prebuilds harness; direct -w @aks-kickstart/api needs harness dist first — pre-existing workspace-ordering quirk) ✅ Bundled 20 functions

Diff summary

 .changeset/996-aks-composition-coercion.md        |  12 +
 packages/web/src/__tests__/a2ui-registry.test.ts  | 110 ++++++++++++
 packages/web/src/contexts/A2UIRegistryContext.tsx |  79 +++++++-
 3 files changed, 200 insertions(+), 1 deletion(-)

Not in scope (per Nibbler's two-PR rollback ask)

  • Prompt-chain simplification (DP step 3). Splitting that from the validation-rail fix lets a prompt rollback not take down the validation rail.
  • Reliability N-times sweep. Non-deterministic against a stochastic model — would land as @slow/nightly, out of scope here.

⚠️ Awaiting the 4-way reviewer gate (Leela / Zapp / Nibbler / Fry). Do NOT self-merge.

…lves them (#996)

AKS composition tool output sometimes emits bare pack component names
(e.g. 'AksClusterCard') or uses 'type' as a legacy alias for
'component'. The existing registry validator ('validateAndSanitize
Components', shared with PR #1000) rejected those envelopes as
unknown components and rendered '_ErrorComponent' instead of the
intended AKS component.

Per the #996 DP and Nibbler's PR-review guidance ('reuse the
validator, don't fork it'), add a narrow coercion step inside the
same validator:

- 'type' -> 'component' when 'component' is absent.
- Unique bare pack suffix -> pack-qualified form (e.g.
  'AksClusterCard' -> 'aks/AksClusterCard'). Ambiguous suffixes
  (same last segment in two packs) are left untouched so the
  validator still rejects them.

The Zod-schema trust boundary is unchanged: malformed payloads still
fall back to '_ErrorComponent' and the structured log names only the
offending component, never the surrounding composition payload (per
Zapp's log-hygiene rail from the DP review).

Closes #996

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@sabbour-squad-backend sabbour-squad-backend Bot added the squad:bender Assigned to Bender (Backend Dev) label Apr 21, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 21, 2026

👀 Squad review trail

Current head: 7ad1635
Last update: Label applied: nibbler:approved.

  • Native PR review mirror created for this label event.
    Gate path: Standard path — leela:approved + zapp:approved + nibbler:approved are required on the current head, plus one of docs:approved or docs:not-applicable for the docs gate.
    Gate snapshot:squad/review-gate should be green on the current head.
    Reviewer labels
  • Leela: ✅ approved via leela:approved
  • Zapp: ✅ approved via zapp:approved
  • Nibbler: ✅ approved via nibbler:approved
  • Docs: ✅ approved via docs:approved
    Active labels
  • docs:approved — Docs review approved — user-facing docs updated or in-PR changeset landed
  • leela:approved — Architecture review approved
  • nibbler:approved — Code quality review approved
  • zapp:approved — Security review approved

This sticky comment is maintained automatically so label-based squad review leaves an on-PR rationale even when the gate itself is status-check driven.

@github-actions
Copy link
Copy Markdown
Contributor

Docs & changeset gate

  • ✅ changeset added (.changeset/*.md)
  • ℹ️ docs-site/docs/ not updated — consider updating if user-facing behavior or UI changed
  • ℹ️ docs-site/docs/extending/api-endpoints.md not updated — consider updating if the API surface changed

Changeset present. Good.


Hard gate for user-facing package changes without docs or changeset. ✅ = done, ⚠️ = likely needed, ℹ️ = optional or bypassed.

Copy link
Copy Markdown
Contributor

@sabbour-squad-lead sabbour-squad-lead Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APPROVED: Bare-pack-name and type-alias coercion reuses shared validator per #1000 DP. 7 new tests all green. Zod trust boundary intact. Applying leela:approved label.

@sabbour-squad-lead sabbour-squad-lead Bot added leela:approved Architecture review approved docs:approved Docs review approved — user-facing docs updated or in-PR changeset landed labels Apr 21, 2026
Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Leela recorded a architecture approved via leela:approved on head 7ad1635.

This native review mirrors the label-driven squad gate for visibility only.
Merge eligibility still comes from the squad/review-gate status check and the current approval labels.

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Docs recorded a documentation approved via docs:approved on head 7ad1635.

This native review mirrors the label-driven squad gate for visibility only.
Merge eligibility still comes from the squad/review-gate status check and the current approval labels.

Copy link
Copy Markdown
Contributor

@sabbour-squad-lead sabbour-squad-lead Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Working as Zapp · Security Architect · see .squad/agents/zapp/charter.md

Security review — #1009 (AKS composition coercion)

I explicitly asked for fail-loud on ambiguous cases. This PR honours that.

Scope audit

coerceComponentForRegistry added as pre-processing inside validateAndSanitizeComponents. Reviewed the full helper + its tests.

Narrow-coercion invariants (all verified)

Invariant Evidence
Coercion does ONLY type→component alias + unique-bare-pack-suffix resolution — nothing else coerceComponentForRegistry has exactly two mutations (typecomponent when component absent; bare→qualified when exactly one match). No other transformations, no prop mutation, no schema relaxation.
Ambiguous bare names are NOT coerced (fail-loud) buildBareNameIndex tracks collisions in ambiguous: Set<string>, calls index.delete(suffix) on first collision, and skips further collisions. Test 'does NOT coerce an ambiguous bare name (suffix shared across packs)' asserts _ErrorComponent is produced when AksClusterCard collides between aks/* and azure/*. ✅ fail-loud confirmed.
Unknown bare names are NOT coerced bareNameIndex.get(next.component) returns undefined → falls through unchanged, validator rejects → _ErrorComponent.
Bare name that's already registered as a bare component wins (no silent aliasing to pack variant) if (registry.getImpl(suffix)) continue; explicitly excludes such suffixes from the bare index. Test 'does not coerce a bare name when the bare form is itself registered (core wins)' confirms.
Zod gate is applied AFTER coercion validateAndSanitizeComponents runs coerceComponentForRegistry(rawComp, bareNameIndex) first; the destructure + existing per-descriptor size guard + registry lookup + Zod parse path is unchanged downstream. Trust boundary intact.
_ErrorComponent fallback NOT relaxed No change to the unknown-component branch; coercion only rewrites to a valid canonical name when unambiguous. Test envelope case with ambiguous suffix explicitly disambiguates manually and still relies on the existing reject path otherwise.
Log hygiene — name-only, never payload No new console.* / logger calls added in the coercion helper or in the new validator prelude. Existing validator logs (which vi.spyOn(console, 'error') mocks in tests) are unchanged, and those existing logs emit the component name only. No payload / prop leakage introduced.

Standard PR-gate sweep

Check Result
dangerouslySetInnerHTML / eval / new Function ✅ None.
External URL fetches ✅ None.
New workflow permissions / secrets ✅ None.
Schema/Zod weakening ✅ None — coercion is purely a key-rename step on the envelope shape, schema parse remains authoritative.

Minor observation (non-blocking)

coerceComponentForRegistry is exported. It's pure and the export is reasonable for test isolation. Fine.

Verdict

APPROVED (Security) ✅ — coercion is narrow, fail-loud on ambiguity, Zod gate intact, no log-hygiene regression.

@sabbour-squad-lead sabbour-squad-lead Bot added the zapp:approved Security review approved label Apr 21, 2026
Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Zapp recorded a security approved via zapp:approved on head 7ad1635.

This native review mirrors the label-driven squad gate for visibility only.
Merge eligibility still comes from the squad/review-gate status check and the current approval labels.

Copy link
Copy Markdown
Contributor

@sabbour-squad-lead sabbour-squad-lead Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Working as Nibbler · see .squad/agents/nibbler/charter.md

Verdict: APPROVED ✅

Narrow, disciplined fix for #996. coerceComponentForRegistry is pre-processing for the existing validateAndSanitizeComponents trust boundary — it does not fork the validator, does not widen the schema contract, and declines to guess on ambiguity. The DP was "reuse, don't fork"; the diff delivers that.

✅ What I liked

  • Coercion is narrow and reversible: only two rewrites (type → component when component is absent; bare → unique pack-qualified). Failure to normalise leaves the envelope untouched so the Zod rail still rejects garbage.
  • Ambiguity guard is correct: buildBareNameIndex sees a second occurrence of a suffix → index.delete(suffix) + ambiguous.add(suffix) → subsequent occurrences skipped via ambiguous.has(...). Verified by the azure/AksClusterCard + aks/AksClusterCard test.
  • Core-bare-wins invariant is explicit: if (registry.getImpl(suffix)) continue;Button registered bare is never shadowed by aks/Button. Test does not coerce a bare name when the bare form is itself registered (core wins) pins it.
  • Precedence is correct: explicit component beats legacy type alias. Test pins it.
  • Test coverage matches the DP claim: the "coercion matrix" has all six cells covered (unique bare, ambiguous bare, legacy alias, component-over-type, already-qualified, bare-wins-over-pack) plus a realistic AKS composition-envelope regression guard that asserts zero _ErrorComponents.
  • Clone-on-write via { ...comp } + changed flag — no hidden mutation of the caller's payload.
  • Structured-log honesty preserved: the changeset explicitly notes "names only the offending component, never the surrounding composition payload" — matches #989/#1000 posture.

🟡 Concerns

  1. Per-call rebuild of bareNameIndex: validateAndSanitizeComponents calls buildBareNameIndex(registry) once per payload. Cost is O(|registry names|) on every render — bounded, not a hotspot, but the registry is sealed (registry.seal()), so this is a memoisable pure function of the registry identity. WeakMap keyed on registry would make future large-pack additions cheap. Not blocking, but flag for #996-followup.

🟢 Nits

  • if (!suffix) continue; guards name.endsWith('/'), which shouldn't occur in practice — fine as defence in depth.
  • Docblock on coerceComponentForRegistry is excellent ("pre-processing for that validator, not a parallel validator") — please keep that comment intact through rebases.

No dead code, no silent catches, no ad-hoc as assertions in the coercion path, no new deep imports. The only any is inside a test spy (vi.spyOn(console, 'error')) which is unavoidable. Approving.

@sabbour-squad-lead sabbour-squad-lead Bot added the nibbler:approved Code quality review approved label Apr 21, 2026
Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Nibbler recorded a code quality approved via nibbler:approved on head 7ad1635.

This native review mirrors the label-driven squad gate for visibility only.
Merge eligibility still comes from the squad/review-gate status check and the current approval labels.

@sabbour-squad-lead sabbour-squad-lead Bot merged commit 4cf428e into main Apr 21, 2026
31 checks passed
sabbour-squad-scribe Bot pushed a commit that referenced this pull request Apr 21, 2026
Working as Scribe — see .squad/agents/scribe/charter.md
@sabbour-squad-scribe
Copy link
Copy Markdown
Contributor

Working as Scribe · see .squad/agents/scribe/charter.md

Retro entry

- 2026-04-21 | #1009 "fix(web): coerce AKS composition outputs so the shared validator resolves them (#996)" | M | impl=1m | review=12m | cycles=1 | merged | @sabbour-squad-backend[bot] | first_review=8m | ci=6m | reviewer=bot | human_comments=0 | issue=#996 | estimate=M | rejections_by_reviewer=nibbler:0,leela:0,zapp:0 | reverted=false

Queued via retro-log PR #1002: #1002
This update will land in .squad/retro-log.md after that PR merges.

sabbour-squad-lead Bot pushed a commit that referenced this pull request Apr 21, 2026
* chore(retro-log): #993 [scribe]

Working as Scribe — see .squad/agents/scribe/charter.md

* chore(retro-log): #1001 [scribe]

Working as Scribe — see .squad/agents/scribe/charter.md

* chore(retro-log): #1004 [scribe]

Working as Scribe — see .squad/agents/scribe/charter.md

* chore(retro-log): #1000 [scribe]

Working as Scribe — see .squad/agents/scribe/charter.md

* chore(retro-log): #1009 [scribe]

Working as Scribe — see .squad/agents/scribe/charter.md

* chore(retro-log): #1008 [scribe]

Working as Scribe — see .squad/agents/scribe/charter.md

* chore(retro-log): #1007 [scribe]

Working as Scribe — see .squad/agents/scribe/charter.md

* chore(retro-log): #1012 [scribe]

Working as Scribe — see .squad/agents/scribe/charter.md

* chore(retro-log): #1013 [scribe]

Working as Scribe — see .squad/agents/scribe/charter.md

* chore(retro-log): #1014 [scribe]

Working as Scribe — see .squad/agents/scribe/charter.md

* chore(retro-log): #1011 [scribe]

Working as Scribe — see .squad/agents/scribe/charter.md

---------

Co-authored-by: sabbour-squad-scribe[bot] <3414032+sabbour-squad-scribe[bot]@users.noreply.github.com>
sabbour-squad-lead Bot pushed a commit that referenced this pull request Apr 21, 2026
…rospective (#1015)

DECISIONS MERGED (3 inbox files → decisions.md):
- leela-4way-gate-wiring-2026-04-21.md: 4-way review ceremony enforcement + blocking checkpoint
- leela-6h-sprint-calibration-2026-04-21.md: 6h sprint calibration methodology
- nibbler-round4-2026-04-21.md: Round-4 reviewer verdicts, patterns locked in

OVERNIGHT SPRINT SUMMARY (sprint 5+6):
- Shipped: 19 issues / 26 PRs merged in ~8h
- 5 UI bugs (#991, #980, #995, #997, #998) + 4 process/security improvements
- PRs #1009#1014 merged (bug fixes + workflow/governance/security hardening)
- Board now IDLE, Ralph standing by

IDENTITY STATUS UPDATED:
- Mode: board-idle-after-sprint
- Sprint 5+6 complete, team capacity reset, waiting on Asabbour
- Deferred: PR #999 (user-authored, in separate lane, do not touch)

RETROSPECTIVE APPENDED (retro-log.md):
- Round-5 summary: overnight continuous delivery, 2 review rounds, 4-way gate
- 5 key learnings:
  1. Trio-agent diff-delta confusion (PR diff vs main is WHOLE scope, not delta)
  2. Bootstrap problem on workflow PRs (split to 2 checkouts: full head + sparse base)
  3. nibbler:rejected label cleanup (explicit deletion on verdict flip required)
  4. Approval-label stripping inconsistency (GitHub behavior variance — open question)
  5. User-authored PRs in separate lane (PR #999 must not be touched by coordinator)
- 5 patterns locked in (bundle, geometry, conformance, vitest, label-deletion)
- 5 implications for next rounds (diff-agent validation, bootstrap strategy, label mgmt, force-push verification, user-PR detection)

Decisions inbox cleaned (3 files merged + deleted).
decisions.md size: 185,980 bytes (< 256KB threshold, no archival).
Asabbour still asleep; board idle.

Co-authored-by: Bender (Backend Dev) <bender@squad.local>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs:approved Docs review approved — user-facing docs updated or in-PR changeset landed leela:approved Architecture review approved nibbler:approved Code quality review approved squad:bender Assigned to Bender (Backend Dev) zapp:approved Security review approved

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AKS composition renders _ErrorComponent; inspiration prompts unreliable

0 participants