OPER-181
Frontmatter-Not-Persisted-To-Task-Row: hydrate size/module/writeSets/phi/migration from body frontmatter on POST/PATCH
Band
SMALL
Suggested
Justin Cooke
Body
--- feature_id: FEAT-studio-planning-validation band: SMALL size: S module: '@operant/web' target_repo: operant-health/operant-studio verify_cmd: pnpm --filter @operant/web test body-frontmatter-extractor task-body-hydration --- ## Context Failure class: **Frontmatter-Not-Persisted-To-Task-Row**. Observed on OPER-173 and OPER-174 (2026.09.10). Both tickets were filed with correct YAML frontmatter blocks at the top of the body declaring size, module, writeSets, verify_cmd. Both landed BACKLOG at POST, were picked up by the auto-planner, and bounced to NEEDS_RESCOPE with empty admissionResult / classifierResult / planningValidation. Manual PATCH of `size`, `module`, and `verificationCmd` (the same values the frontmatter already declared) plus a `status=READY_FOR_PLANNING` reset was required to unstick them. Ground-truth citations (operant-health/operant-studio, CLAUDE.md SHA 3e93088, OPERANT-BOILERPLATE.md SHA 95212ce, verified 2026.09.10): - **Extractor recognizes 9 keys.** apps/web/src/lib/body-frontmatter-extractor.ts:200-227 writes result.verificationCmd, result.writeSets, result.size, result.phiInScope, result.hasMigration, result.module, result.featureId, result.targetBranch, result.ticketId. - **POST handler hydrates 1.** apps/web/src/app/api/studio/[team]/tasks/route.ts:155-158 calls extractBodyFrontmatter(cleanBody) and uses only frontmatter.verificationCmd. The task.create() call at lines 270-309 reads body.size, body.module, body.writeSets from the JSON payload only. - **PATCH handler hydrates 1.** apps/web/src/app/api/studio/[team]/tasks/[id]/route.ts:91-93 re-runs extractBodyFrontmatter on body edits and pulls only verificationCmd. - **Auto-planner requires all three.** apps/web/src/lib/planning-validator.ts:54-63 defines PLANNING_REJECTION_REASONS including missing_size, missing_writesets, missing_module. A ticket with any of those null bounces. - **NEEDS_RESCOPE is deliberately unrecoverable.** apps/web/src/app/api/cron/auto-planner/route.ts:290 comment: "NEEDS_RESCOPE is absent from the status filter above by design (OPER-60)". So the bounce cost is a human PATCH round, not an auto-retry. Result today: any human or agent that follows the extractor documented contract (YAML block at top of body) and does NOT redundantly repeat every attribute in the JSON payload gets their ticket rejected. The verify_cmd fallback (OPER-82) works because it was wired. Parallel wiring for size, module, writeSets, phiInScope, hasMigration was never added. The extractor advertises 9 fields and delivers 1. Distinct from Phantom-writeSet (OPER-174, admission-time path validation) and Prose-Depends-Trips-Admission (OPER-155, pre-YAML scan). Those catch wrong declared values. This class catches correct declared values that never reach the row. ## Acceptance Criteria 1. New helper apps/web/src/lib/task-body-hydration.ts exports `hydrateTaskFieldsFromBody(body, existing)` that calls extractBodyFrontmatter once and, for each of size, module, writeSets, phiInScope, hasMigration, verificationCmd, fills the field from frontmatter only when the caller did not supply it (undefined). Explicit caller value always wins, per the OPER-82 rule. 2. apps/web/src/app/api/studio/[team]/tasks/route.ts POST calls the helper right after PHI redaction and before admission + task.create(). The data block reads hydrated values for the six fields. 3. apps/web/src/app/api/studio/[team]/tasks/[id]/route.ts PATCH applies the same hydration when the patch includes a body edit, using the current patch as `existing` so an explicit field in the PATCH still wins. 4. Existing verificationCmd fallback at POST (route.ts:155-158) and PATCH ([id]/route.ts:91-93) is refactored to go through the new helper. No double-extraction, no duplicated fallback logic. 5. body-frontmatter-extractor.test.ts adds a case asserting a body with size, module, write_sets, phi_in_scope, has_migration, verify_cmd returns all six fields in one call. 6. New task-body-hydration.test.ts asserts explicit-wins-over-frontmatter for each of the six fields. 7. New route.test.ts cases (POST + PATCH) assert end-to-end that a body with a full frontmatter block and no explicit fields in the JSON payload writes all six columns on the Task row, verified via follow-up GET. 8. Every writeSet path declared on this ticket resolves to a real file or a `new:` path in a real directory. No phantom paths (OPER-174 rule). 9. The audit-log payload written by createAuditLog for task.created includes a new `hydratedFromFrontmatter: string[]` key so a later gap audit can grep for silent hydration. ## Non-goals - featureId and targetBranch hydration onto the Task row. Those are not TaskAttrs columns today; they live on the AuditLog admission blob. Adding them is a schema change and out of scope. - ticketId hydration is reserved for the Linear-sync path (OPER-57). - Changing the auto-planner rejection behavior. It stays strict; this ticket removes the reason it fires spuriously.
Attachments
Loading attachments…
Comments
Loading comments…