Operant Studio
OPER-94

sync-product-checksums workflow + drift-check + admission metric (PDT 2026-08-20)

Band
SMALL
Suggested
Justin Cooke

Body

## Context

Portico's product manifest (`products/portico/{feature-map,dependency-map}.md`) lives in `operant-studio` and is consumed by `apps/web/src/lib/ticket-admission.ts` via `sha256(fileContent)` compared to `Product.featureMapChecksum` + `Product.dependencyMapChecksum`. Every manifest edit currently requires a second PR containing a hand-rolled SQL migration to UPDATE those checksums. Without it, `assertAdmissible()` returns `code: map_drift` on every well-formed Portico ticket and stalls the dispatcher.

This exact stall has happened three times in eleven days:
- [REDACTED-DOB] POR-132 (FEAT-pay-rcm-onboarding declaration, PR #213)
- [REDACTED-DOB] OPER-585 Console module (PR #244)
- [REDACTED-DOB] OPER-586 Command module (PR #247, merged today)

PDT decided [REDACTED-DOB] (memo `pdt_2026-08-20_portico_manifest_ownership`, Option A) to keep manifests in operant-studio and eliminate the second-PR friction with an automated on-merge sync workflow. This ticket implements that workflow plus the observability + drift-check extension its dissenting seats required.

## Acceptance Criteria

- [ ] New workflow file `.github/workflows/sync-product-checksums.yml` — triggers on `push` to `main` when `products/**/{feature-map,dependency-map}.md` changes.
- [ ] Workflow computes `sha256sum` of each changed manifest file, runs the corresponding one-row `UPDATE "Product" SET featureMapChecksum=?, dependencyMapChecksum=? WHERE slug=?` via `pnpm --filter @operant/database exec prisma db execute`.
- [ ] Workflow bound to the `Production` GitHub environment (same secret scope as `migrate-prod`). Uses `DATABASE_URL_PROD` + `DIRECT_URL` (both set to the Neon unpooled URL, per migrate-prod pattern).
- [ ] Workflow scope is **immutable**: only one operation (`UPDATE "Product"`) on one table, keyed only by `slug`. Any future expansion must convene DOC — noted in the workflow header comment.
- [ ] Emits AuditLog row after each successful UPDATE: `action=UPDATE`, `resource=Product`, `after.workflow="sync-product-checksums"`, `after.slug=<slug>`, `after.featureMapChecksum=<sha>`, `after.dependencyMapChecksum=<sha>`.
- [ ] Extend the existing `migrate-prod` 6-hourly `drift-check` cron (or add sibling cron in same file) to include Product-row checksum verification against the live file sha; Slack `#dev-alerts` on mismatch.
- [ ] Add `admission_rejections_total{code="map_drift"}` label to the admission metric emitter in `apps/web/src/lib/ticket-admission.ts`. Existing metric name and cardinality shape unchanged.
- [ ] Add alert rule: `admission_rejections_total{code="map_drift"} > 0` for 15 minutes outside a `products/**` PR merge window (10-minute grace after each merge). Route to `#dev-alerts`.
- [ ] Update `docs/policies/dependency-map.md` — remove any tribal-knowledge reference to a manual checksum migration; replace with the sync workflow's name and its trigger conditions.
- [ ] PR description includes a note flagging that OPER-585 + OPER-586 are the last two by-hand checksum migrations — future manifest changes must NOT include a checksum migration file (workflow handles it).

## Marisol (HIPAA Compliance Officer) review requirement

Non-negotiable per PDT memo: Marisol reviews before merge. The workflow mutates prod DB outside the Prisma migration graph and this pattern must not creep in scope. Explicit review gate.

## References

- PDT memo: `/pdt-memos/[REDACTED-DOB]-portico-manifest-ownership.md` (Approved [REDACTED-DOB])
- Prior incidents: OPER-585 (PR #244), OPER-586 (PR #247), POR-132 (PR #213)
- Related workflow: `.github/workflows/migrate-prod.yml` (OPER-D13, OPER-83)
- Admission gate: `apps/web/src/lib/ticket-admission.ts`


---
<!-- admission-rejected -->
**Admission rejected (missing_feature_id):** ticket body is missing `feature_id: FEAT-...` frontmatter — declare which feature this ticket implements

Attachments

Loading attachments…

Comments

Loading comments…