refactor(groomer): schema-driven enum validation with structured alias resolution#476
Conversation
…s resolution Replace hand-rolled validateGroomerOutput with a zod-based validator that: - Uses a generic EnumConfig registry (src/lib/groomer/enum-configs.ts) for all enum fields. Adding a new enum field = appending to the registry. - Surfaces alias resolution as structured ResolutionEvent[] in the result, not silent mutation. Each event records rawValue -> resolvedValue + source. - Mutates parsed output to canonical values when aliases fire (same behavior as before, but now observable). - Wires resolution events into contextWarnings in run.ts as grep-friendly 'enum:<field>: resolved '<raw>' -> '<canonical>' via alias' entries. - Keeps isAllowedLabel prefix-validation for status/priority/type labels (not enums) as a separate concern. New files: - src/lib/groomer/enum-config.ts: EnumConfig interface + resolveEnumConfig() - src/lib/groomer/enum-configs.ts: GROOMER_ENUM_CONFIGS registry Test delta: 34 -> 44 tests (10 new alias-resolution tests) Fixes the root cause behind PR #475 (lane id 'normal' rejected) by making the validator generalize across all enum fields rather than special-casing lane validation.
There was a problem hiding this comment.
AI Automated Review
Full PR review.
Analysis engine: MiniMax-M2.7@https://litellm.jory.dev/v1 (anthropic) — routed smart (risk match: db_or_migration_changes)
Review: PR PR 476 — Schema-Driven Enum Validation with Structured Alias Resolution
Recommendation: Approve
This is a pure TypeScript refactor of the groomer's validation layer. It does not modify the Prisma schema, add migrations, or alter database operations in any way. The db_or_migration_changes classification appears to be a false positive for this PR.
Change-by-Change Findings
New files — src/lib/groomer/enum-config.ts and enum-configs.ts
enum-config.tsintroducesEnumConfig<T>,ResolvedEnumConfig<T>,ResolutionEvent, andresolveEnumConfig(). Clean generic interface; aliases support both static objects and lazy getters.enum-configs.tsregisters configs for all groomer enum fields (lane.id,lane.confidence,confidence,actionability,nextGroomingAction). Status/priority/type labels are correctly excluded since they use prefix-basedisAllowedLabelvalidation.lane.iduses lazy resolution (resolveValidValues+aliasesas functions) so it reads fromgetConfiguredLanes()/getLaneAliases()at validation time, making it lane-config-aware.
Modified — src/lib/groomer/schema.ts
- Replaces the hand-rolled
Array.includesvalidation with avalidateEnumValue()helper that checks validValues first, then aliases, then errors. This fixes the root cause identified in PR PR 475. - Uses a minimal Zod
baseSchemafor structural validation (required fields, types) while keeping enum logic in custom code — a reasonable split. - Returns
ValidationResultwith newresolutions?: ResolutionEvent[]array;resolutionsis always present on the return object (empty array on success, non-array only on early-return for null/non-object input — correctly tested). - All existing behavior preserved: null tolerance on optional string fields,
isAllowedLabelprefix logic, error message shapes, public API surface.
Modified — src/lib/groomer/run.ts
- After validation, iterates
validation.resolutionsand appends structured warning strings (enum:lane.id: resolved 'normal' -> 'local' via alias) tocontextWarnings. Grep-friendly, machine-parseable, no type changes needed.
Modified — src/lib/groomer/schema.test.ts
- +10 new tests covering: alias resolution (
normal→local,escalated→frontier), canonical lane (no spurious resolution event), unknown lane (validation error, no resolution event), multiple enum fields, empty aliases map,resolutionsarray presence on all return paths.
Required Checks — Explicitly Addressed
-
Review migration for data loss risk — Does not apply. This PR contains zero database schema changes, migrations, or data mutations. It is a pure TypeScript refactor of in-memory validation logic. No
prisma/schema.prismamodifications, no.sqlmigration files, no Prisma model changes. -
Test migration on a copy of production schema — Does not apply. Same rationale as above: no migration exists to test. The validation runs entirely in application code (
validateGroomerOutput) with no persistent state changes.
Standards Compliance
- No new dependencies: Uses existing
zod— confirmed in PR checklist and not added topackage.json. - Code standards: Error handling uses
instanceof-compatible patterns; input validation occurs before database operations (though no DB operations are involved here); no secrets committed. - Label conventions:
status//priority//type/prefix validation is preserved exactly viaisAllowedLabel;agent/*labels remain blocked. - Null tolerance: Optional string fields (
summary,githubComment,needsInfoReason,blockedReason) continue to treatnullas absent. - API shape:
validateGroomerOutputreturns{ valid, parsed?, errors?, resolutions? }— backward-compatible extension with new optionalresolutionsfield.
Linked Issue Fit
No linked issues. The PR body references PR PR 475 as context for motivation, but no issue acceptance criteria are defined to verify against.
Evidence Provider Findings
No evidence providers configured.
Tool Harness Findings
No tool calls were issued; reviewing the diff directly.
CI Results
All checks passed:
- Tests: success
- Lint: success
- Typecheck: success
- Build: success
- Docker Build: success
Unknowns / Needs Verification
None. The diff is self-contained, tests are comprehensive, and all CI checks pass.
Why
PR #475 was closed because it fixed the immediate symptom (lane id
normalrejected) but didn't address why it happened. The hand-rolledvalidateGroomerOutputused brittleArray.includeschecks that only worked for specific enum fields and silently swallowed alias resolution.This PR replaces it with a schema-driven validator that generalizes across all enum fields.
What changed
New files
src/lib/groomer/enum-config.ts— GenericEnumConfig<T>interface +resolveEnumConfig()helper. Defines the contract for declaring enum fields: valid values, aliases (static or lazy), and dynamic resolvers.src/lib/groomer/enum-configs.ts—GROOMER_ENUM_CONFIGSregistry covering all groomer output enum fields (lane.id,lane.confidence,confidence,actionability,nextGroomingAction). Status/priority/type labels excluded (prefix-validated, not closed enums).Modified files
src/lib/groomer/schema.ts— RewrotevalidateGroomerOutputto:ValidationResultwith newresolutions?: ResolutionEvent[]arrayisAllowedLabelprefix logicsrc/lib/groomer/run.ts— After validation, appends structured warnings tocontextWarnings:Grep-friendly and machine-parseable without changing the
contextWarnings: string[]type.src/lib/groomer/schema.test.ts— 34 → 44 tests (+10):normal→local,escalated→frontier)resolutionsarray presence on valid/invalid/early-return pathsDesign decisions
EnumConfig registry pattern — Anyone adding a new enum field just appends to
GROOMER_ENUM_CONFIGS. No validator code changes needed. Aliases can be static or lazy (functions), supporting env-driven configs like lane topology.resolutionsis informational, not an error — Alias resolution succeeds; it's just worth observing. TheResolutionEventstruct recordsfield,rawValue,resolvedValue, andsource: "alias"for downstream tooling.Zod for structure, custom logic for enums — Zod validates the shape (required fields, types). Enum validation with alias resolution uses a targeted helper (
validateEnumValue) that returns{ error?, resolvedValue? }. This keeps zod clean while getting structured alias handling.Preserved all existing behavior — Null tolerance,
isAllowedLabelprefix validation, error messages, and the public API shape ({ valid, parsed?, errors? }) are maintained. Only grew withresolutions?.Checklist
npm run typecheckpassesnpm run lintpassesllm.tsuntouched