Skip to content

Quality & Governance — Technical Reference

Last verified: Session 220 (02 May 2026) — §5.3 publication-bulk-action endpoint + bulk-approve UX surface (PublicationBulkActionBar / PublicationBulkResultDialog) shipped + spec archived. Pending updates: None

Quality & Governance — Technical Reference

Section titled “Quality & Governance — Technical Reference”

Quality & Governance covers four overlapping but distinct subsystems on the content lifecycle:

  1. Publication lifecycle (publication_status) — whether an item is ready for use: draft → in_review → published → archived. §5.2 Phases 1+2+2.5+1f shipped (S201–S202).
  2. Quality score (quality_score) — composite 0–100 health metric recalculated weekly. §5.5 Phase 1 schema, Phase 2 cron, Phase 3 quality fields, Phase 4 MCP filter widening, Phase 5 cadence-compliance modifier shipped (S200–S208).
  3. Governance review (governance_review_status) — change-management workflow that asks “has this recent edit been reviewed?” with approve/request_changes/revert actions. Bridged from quality drops and freshness transitions via per-domain governance_config presets.
  4. Content review (verified_at) — verify/flag model for assessing individual content items at /review. Distinct from governance review (separate UI, separate API, separate action vocabulary).

In addition, Change Reports (/change-reports route, change_reports table) summarise KB activity over a period. Auto-generation for users >24h old is gated by an opt-out preference and a 150-item cost guard.

MethodRouteAuthPurposeFile
GET/api/governanceAllList per-domain governance config rowsapp/api/governance/route.ts
POST/api/governanceAdminUpsert governance config via { domain, preset } (S177 P0-16)app/api/governance/route.ts

The POST handler maps preset (light_touch or strict) to concrete column values via PRESET_VALUES in lib/governance/presets.ts. The seven underlying columns (posture, timeout_days, quality_score_threshold, auto_flag_on_quality_drop, auto_flag_on_freshness_transition, auto_flag_cooldown_days, reviewer_id) are persisted directly so consumers read columns, not preset.

MethodRouteAuthPurposeFile
GET/api/governance/reviewAllList items pending governance review; ?count_only=true returns countapp/api/governance/review/route.ts
POST/api/governance/reviewEditor+Process governance review action (approve / request_changes / revert)app/api/governance/review/route.ts

The POST handler enforces governance_review_status ∈ ALLOWED_REVIEW_INPUT_STATUSES (pending, review_overdue) before processing — see lib/governance/review-input-statuses.ts. The approve branch also calls computeNextReviewDate(...) from lib/governance/cadence-renewal.ts to auto-renew next_review_date when the item has a configured cadence.

MethodRouteAuthPurposeFile
GET/api/review/queueEditor+Fetch review queue items with sort/filter/paginationapp/api/review/queue/route.ts
POST/api/review/actionEditor+Apply review action (verify/flag/skip/unverify/unflag)app/api/review/action/route.ts
GET/POST/PATCH/api/review/assignmentsPOST: Admin; GET/PATCH: Editor+Filter-based reviewer assignmentsapp/api/review/assignments/route.ts
GET/api/review/cadenceEditor+Aggregate review-health stats (overdue / due-soon / domain breakdown)app/api/review/cadence/route.ts
GET/api/review/historyEditor+Verification history audit trailapp/api/review/history/route.ts
GET/api/review/statsEditor+Summary stats (totals, overdue, by_domain, by_content_type)app/api/review/stats/route.ts

Query parameters validated by ReviewQueueParamsSchema:

ParamTypeRequiredDescription
statusenumNounverified (default), verified, flagged, draft, or all
sortenumNocreated_at (default desc), confidence_asc, quality_score_asc
limit / offsetintNoPagination — limit clamped to 1..100
domainstringNoRepeatable or comma-separated; filters primary_domain
content_typestringNoRepeatable or comma-separated
source_filestringNoFilter by source file
source_document_idstringNoFilter by source document
assigned_to_mestringNoLiteral 'true' opts in; intersects with active review_assignments rows
include_overduestringNoLiteral 'true'; widens unverified filter to OR-include governance_review_status='review_overdue' (S205 WP-E T2)

The route reads publication_status (NOT governance_review_status) for draft filtering post-§5.2 Phase 2.5 (T8b).

The publication-lifecycle transition is wired into the generic content PATCH route, not a dedicated endpoint:

MethodRouteAuthFieldPurposeFile
PATCH/api/items/[id]Editor+publication_statusTransition between draft/in_review/published/archivedapp/api/items/[id]/route.ts
PATCH/api/items/[id]Editor+next_review_dateSet ISO date when next review is dueapp/api/items/[id]/route.ts
PATCH/api/items/[id]Editor+review_cadence_daysSet recurring review cadence (1..1095, NULL clears)app/api/items/[id]/route.ts

The publication_status branch:

  1. Fetches current state (publication_status, archived_at, archived_by, archive_reason, plus snapshot fields for content_history).
  2. Validates the transition against computeAllowedTransitions(fromStatus, role) per §3.2 + §3.4 — 403 if role has no transitions out of the current state, 409 if the requested target is not in the allowed set.
  3. Applies applyTransitionSideEffects(...) to stamp archived_at/ archived_by/archive_reason on archive paths and clear archived_at (preserving archived_by/archive_reason) on un-archive paths.
  4. Persists with optimistic-concurrency guard (.eq('publication_status', fromStatus)) — concurrent state change returns 409 with Concurrent state change detected; please retry..
  5. Writes a content_history row with change_type='publication_state' and change_reason='Transition from {from} to {to}' (+ optional (reason: {archive_reason}) suffix). The auto_version_content_history trigger fills version.

The bulk-approve UX layer on the publication-review queue (tab 6 of /review) calls a dedicated server-side endpoint that mirrors the per-row PATCH semantics above for each id, so multi-select approvals retain a distinct audit literal.

MethodRouteAuthPurposeFile
POST/api/review/publication-bulk-actionEditor+Bulk transition 'in_review' → 'published' (action=‘approve’) or 'in_review' → 'draft' (action=‘return_to_draft’); cap=50app/api/review/publication-bulk-action/route.ts

Contract (PublicationBulkActionBodySchema at lib/validation/schemas.ts):

  • ids: 1..50 UUIDs (D-3 ratified S217 close-out — tightened from 100).
  • action: 'approve' | 'return_to_draft'.
  • Rate limit: 20 req/min per user (D-8 ratified S217).

Per-item iteration (sequential, NOT Promise.all) mirrors the per-row PATCH at app/api/items/[id]/route.ts:199-365 exactly, with two changes:

  1. Pre-loop fromStatus guard (D-10): any row whose fromStatus !== 'in_review' returns { status: 'conflict' } regardless of role/action. Defence-in-depth on top of the .eq('publication_status', 'in_review') UPDATE filter — prevents silent 'published' → 'draft' regressions on stale-queue selections.
  2. Audit literal: change_reason='bulk_approve' (action=‘approve’) or 'bulk_return_to_draft' (action=‘return_to_draft’) — distinct from the per-row PATCH phrasing so audit queries can filter bulk-vs-singleton approvals (see data-entry-points.md Appendix D).

Response is ALWAYS HTTP 200 with { action, totalRequested, successCount, failureCount, results }. Five per-item statuses: 'success', 'conflict', 'forbidden', 'not_found', 'error'. Outer try/catch returns 500 on route-level crashes only.

Spec: .planning/.archive/.specs/publication-approval-gate-spec.md §4 + §6 + §7 (archived S220 W4).

MethodRouteAuthPurposeFile
GET/api/change-reports/latestAllFetch most recent stored digestapp/api/change-reports/latest/route.ts
GET/api/change-reports/listAllList historical digests (paginated)app/api/change-reports/list/route.ts
GET/api/change-reports/[id]AllFetch a single digestapp/api/change-reports/[id]/route.ts
POST/api/change-reports/generateEditor+Generate a new Change Report (rate limit 5/min/user)app/api/change-reports/generate/route.ts

POST /api/change-reports/generate enforces a cost guard at lib/ai/change-reports.ts:241–254: if typedItems.length >= CHANGE_REPORT_AUTO_GEN_MAX_ITEMS (150), it throws an AIServiceError with HTTP 413 + code='CHANGE_REPORT_TOO_MANY_ITEMS'

  • data: { item_count, max }. The route propagates the structured payload to the client unchanged.
ComponentFilePurpose
ReviewQueuePanelcomponents/review/review-queue-panel.tsxMain tabular queue UI with item selection + L-key toggle
ReviewFilterscomponents/review/review-filters.tsxFilter popover — status, domain, content_type, source file/doc, assigned-to-me, overdue reviews toggle (S205 WP-E T2)
ReviewActionBarcomponents/review/review-action-bar.tsxverify/flag/skip/unverify/unflag buttons
ReviewCardcomponents/review/review-card.tsxIndividual item card
ReviewProgressBarcomponents/review/review-progress-bar.tsxSticky session progress indicator
ReviewSessionSummarycomponents/review/review-session-summary.tsxSession counter + summary
ReviewCadenceCardcomponents/review/review-cadence-card.tsxAggregate review-health stats card on /review
ReviewHistorySectioncomponents/review/review-history-section.tsxVerification audit trail per item
AssignmentManagercomponents/review/assignment-manager.tsxFilter-based reviewer assignment UI
PublicationReviewQueuecomponents/review/PublicationReviewQueue.tsxTab 6 of /review — items in publication_status='in_review'. Multi-select checkboxes + bulk action bar (§5.3 S220). Per-row <PublicationReviewActionBar> remains alongside selection state.
PublicationReviewActionBarcomponents/review/publication-review-action-bar.tsxPer-row Approve & publish / Return to draft / Open in editor. Independent of bulk-selection state.
PublicationBulkActionBarcomponents/review/publication-bulk-action-bar.tsxSticky bulk action bar — counter, “Select all on page”, Clear, Approve selected, Return selected to draft. Mounts when ≥1 row selected; cap=50 enforced client-side with disabled-with-tooltip past the cap (S220 §5.3 D-3).
PublicationBulkResultDialogcomponents/review/publication-bulk-result-dialog.tsxPer-row outcome dialog opened on partial-failure response (failureCount > 0). Falls back to UUID + “(item no longer in queue)” when title lookup misses (S220 §5.3 §7.7).
ComponentFilePurpose
VerificationBadgecomponents/shared/verification-badge.tsxBinary trust badge — Unverified (amber ShieldAlert) / Verified (green ShieldCheck). WCAG: text label always present.
QualityBadgecomponents/shared/quality-badge.tsxComposite 0–100 score with simplified or component-breakdown tooltip
QualityScoreBreakdowncomponents/shared/quality-score-breakdown.tsxFive-component breakdown — used by metadata sidebar, content card
QualityScorecomponents/shared/quality-score.tsxDetailed form-metadata quality breakdown (separate domain — form drafting)
GovernanceBadgecomponents/shared/governance-badge.tsxgovernance_review_status icon+label badge — pending/approved/changes_requested/reverted/draft
ReviewCadenceBadgecomponents/shared/review-cadence-badge.tsxFour-band: overdue (red) / due ≤14d (amber) / due ≤30d (muted) / >30d (no badge). Pure helper calculateReviewBand().
ReviewCadenceEditorcomponents/content/review-cadence-editor.tsxAdmin/editor cadence editor — <input type="date"> + 5-preset Select (None / 90 / 182 / 365 / Custom 1..1095)
GovernanceSectioncomponents/settings/governance-section.tsxSettings UI for per-domain preset configuration (Light-touch / Strict)
ComponentFilePurpose
ChangeReportViewcomponents/change-reports/change-report-view.tsxRenders narrative summary + by-domain sections + Current KB Health card

Current KB Health (OPS-19, S191) is the freshness state-of-the-world snapshot (fresh/aging/stale/expired counts) — a current-state breakdown, not a period-specific delta. Lives alongside the period-scoped delta counters in the same view.

ModuleFilePurpose
calculateQualityScore / cadenceCompliancePenaltylib/quality/quality-score.tsComposite 0–100 score across 5 components (freshness 30%, confidence 20%, completeness 20%, summary 15%, citations 15%). §5.5 Phase 5 cadence modifier subtracts 0–40 from raw freshness when next_review_date is non-null.
qaDetection / qaQualityActionslib/quality/qa-detection.ts, quality-actions.tsQuality-action repair suggestions surfaced via the MCP where_are_we_exposed tool (which absorbed the former get_quality_actions)
PRESET_VALUES / inferPresetlib/governance/presets.tsLight-touch / Strict preset → concrete column values; inferPreset(posture) for legacy rows
computeAllowedTransitions / applyTransitionSideEffects / VALID_PUBLICATION_STATUSESlib/governance/publication-transitions.tsPure helper consumed by PATCH route + MCP update_publication_status tool. §3.2 transition matrix × §3.4 role-gate matrix. Drift-pinned by publication-transitions.test.ts.
computeNextReviewDatelib/governance/cadence-renewal.tsPure helper — auto-renewal arithmetic for the approve branch. GREATEST(currentNextReviewDate, today) + cadenceDays. Returns null when reviewCadenceDays is null.
ALLOWED_REVIEW_INPUT_STATUSESlib/governance/review-input-statuses.tsAllow-list (pending, review_overdue) for governance review action handlers (route + MCP tool)
changeReportFrequencyLabellib/change-reports/change-reports-helpers.tsMaps frequency to user-facing label (e.g. “Weekly Change Report”)
generateDigest / CHANGE_REPORT_AUTO_GEN_MAX_ITEMSlib/ai/change-reports.tsAnthropic-backed digest generator with 150-item cost guard
recordPipelineRunlib/pipeline/record-run.tsCanonical write to pipeline_runs for all cron jobs (status: 'completed' | 'completed_with_errors' | 'failed')
TablePurposeKey ColumnsRLS
governance_configPer-domain governance rules (preset-driven)domain, preset (light_touch/strict), posture, timeout_days, quality_score_threshold, auto_flag_on_quality_drop, auto_flag_on_freshness_transition, auto_flag_cooldown_days, reviewer_idAdmin: write
review_assignmentsFilter-based reviewer assignments (not per-item)reviewer_id, assigned_by, assignment_type, filter_domains, filter_content_types, filter_freshness, filter_date_from, filter_date_to, due_date, statusEditor+: read/write
verification_historyAppend-only audit trail of verify/flag/unverify/unflag eventscontent_item_id, performed_by, action_type, note, performed_atInsert-only
ingestion_quality_logQuality flag log (review_needed, classification, dedup, etc.)content_item_id, flag_type, severity, details, resolved, resolved_at, resolved_byEditor+: write
content_historyVersioned snapshots of content_itemscontent_item_id, version, title, content, brief, detail, reference, change_summary, change_reason (mandatory per S153), change_type, created_by, created_atEditor+: insert
change_reportsChange Reportsfrequency, period_start, period_end, item_count, domain_summaries, narrative_summary, generated_at, generated_by, tokens_usedRead: All
user_notification_prefsPer-user email + auto-gen preferencesuser_id (PK), email_weekly_change_report, email_review_assignments, email_owned_content_flags, auto_generate_change_reportsUser-owns-row
pipeline_runsCron + ingest run audit logpipeline_name, status, started_at, completed_at, items_processed, error_message, result (jsonb), git_shaEditor+: read
content_items (cols)Carries lifecycle state on the canonical rowpublication_status (NOT NULL DEFAULT 'published'), governance_review_status, next_review_date, review_cadence_days (CHECK 1..1095), quality_score, previous_quality_score, quality_score_updated_at, verified_at, verified_by, archived_at, archived_by, archive_reason, content_owner_idRole-based
  • content_items.publication_status{'draft','in_review','published','archived'} (NOT NULL, DEFAULT 'published' — migration 20260427141626…).
  • content_items.governance_review_status{'pending','approved','reverted','changes_requested','review_overdue'} (NULL allowed). 'draft' was removed S202 §5.2 Phase 2.5 — migration 20260427180854_tighten_governance_review_status_check_drop_draft.sql.
  • content_items.review_cadence_days[1, 1095] (NULL allowed). Migration 20260427103256_add_review_cadence_columns.sql.
  • content_history.change_type enum extended with 'publication_state' for publication transitions (commit eeb8ae25).
  • enforce_archive_state_consistency — bidirectional PL/pgSQL trigger on content_items enforcing publication_status='archived' ↔ archived_at IS NOT NULL across 4 directions. Migration 20260427141627_publication_status_indexes_and_trigger.sql.
  • auto_version_content_history — BEFORE INSERT trigger sets NEW.version = COALESCE(MAX(version), 0) + 1. Routes omit version from payloads and rely on this trigger.
  • auto_v1_on_insert — DB trigger ensures every content_items insert pairs with a v1 content_history row; change_reason is mapped from ingest_source.
  • get_review_breakdown_stats() — aggregate stats RPC consumed by the review queue and ReviewCadenceCard. Extended S204 T0 with overdue field used by the Overdue-reviews filter pill.
  • The find tool’s chunk branch (backed by the search_content_chunks RPC) — extended S207 with overdue_review: boolean + review_due_within_days: integer (1..365) filters (migration 20260428212936).
  • whats_in_my_queue (facet: governance — formerly get_governance_queue) — extended S207 with include_overdue: boolean + status_filter: enum('pending'|'review_overdue'|'all') filters; existing 4-arg callers unchanged.
  • idx_content_items_next_review_date — partial on next_review_date IS NOT NULL, excluding superseded + archived.
  • idx_content_items_publication_status_published — partial on publication_status='published'.
  • idx_content_items_published_recent — partial on publication_status='published' ordered by created_at DESC.
  • idx_content_items_archived — partial on publication_status='archived'.
ToolPurposeRead-onlyAnnotations
delete_content_itemArchive (editor+) or hard-delete (admin only) a content itemNoDESTRUCTIVE_WRITE_ANNOTATIONS
update_governance_statusBulk update governance_review_status (change-management state, not lifecycle)NoNON_IDEMPOTENT_WRITE_ANNOTATIONS
update_publication_statusTransition publication_status (draft/in_review/published/archived); same role gate + transition matrix as PATCH routeNoSAFE_WRITE_ANNOTATIONS
whats_in_my_queue (facet: governance)One faceted queue (formerly get_governance_queue / get_review_queue / get_assignments_for_user / get_dashboard_summary); supports include_overdue + status_filter (S207)YesREAD_ONLY_ANNOTATIONS
review_governance_itemProcess governance review action (approve / request_changes / revert) — same allow-list as the routeNoSAFE_WRITE_ANNOTATIONS
where_are_we_exposedFive-layer exposure report — absorbed the former get_quality_briefing, get_quality_actions, get_quality_summary, audit_content, freshness, coverage-gap and certification readsYesREAD_ONLY_ANNOTATIONS
find_duplicates (scope: all)Cross-corpus duplicate detection (consolidated find_duplicate_candidates + find_all_duplicates)YesREAD_ONLY_ANNOTATIONS
suggest_content_creationCoverage-gap-driven content suggestionsYesREAD_ONLY_ANNOTATIONS
find (granularity: "chunk")Heading-bounded chunk search; widened S207 with overdue_review + review_due_within_days (backed by the search_content_chunks RPC)YesREAD_ONLY_ANNOTATIONS

update_publication_status and update_governance_status are intentionally distinct: the former handles publication lifecycle (whether the item is ready for use); the latter handles change-management workflow (whether a recent edit has been reviewed). Both write content_history with the appropriate change_reason.

Schedules from vercel.json (UTC):

CronScheduleRoutePurpose
freshness-transitionsDaily 03:15/api/cron/freshness-transitionsDetect freshness transitions, notify, auto-flag stale/expired items for governance review (Phase 2 bridge)
review-cadenceDaily 03:45/api/cron/review-cadenceFlag next_review_date < CURRENT_DATE items as 'review_overdue' (§5.5 Phase 2 T1)
classification-qualitySundays 04:00/api/cron/classification-qualityFlag low-confidence anomalies
quality-scoreSundays 05:00/api/cron/quality-scoreRecalculate composite scores; notify on threshold drops; auto-flag for governance review (Phase 1 bridge)
coverage-alertsMondays 05:00/api/cron/coverage-alertsCoverage gap alerts
content-gapsMondays 05:30/api/cron/content-gapsContent gap suggestions
intelligence-poll (SI)Every 15 min/api/cron/intelligence-pollSector intelligence feed polling
intelligence-cleanup (SI)Sundays 03:00/api/cron/intelligence-cleanup90-day SI article retention

All cron handlers verify verifyCronAuth(request) (Vercel cron secret) and record their run via recordPipelineRun() from @/lib/pipeline/record-run.

  • quality-score: BATCH_SIZE=100, TIMEOUT_BUFFER_MS=40_000 (maxDuration=50 → 10s buffer for writes), DEFAULT_THRESHOLD=40, BATCH_SUMMARY_THRESHOLD=20.
  • freshness-transitions: BATCH_THRESHOLD=10 for transition notifications, GOVERNANCE_BATCH_SUMMARY_THRESHOLD=20 for governance auto-flag bridge. Also runs checkDateExpiryReminders() (entity + content-item expiry within 30 days) and cleanupExpiredNotifications() (delete expired+dismissed >30d).
  • review-cadence: REVIEW_CADENCE_BATCH_SUMMARY_THRESHOLD=20, maxDuration=30. Notification idempotency uses getExistingNotificationIds() keyed on today_utc_midnight.

lib/quality/quality-score.tscalculateQualityScore({...}) returns { score, components, label }.

ComponentWeightRaw 0–100 derivation
Freshness30%fresh→100, ageing/aging→60, stale→30, expired→0, null→100. Modulated by cadenceCompliancePenalty(next_review_date) when non-null.
Confidence20%classification_confidence clamped to [0, 1] × 100; null → 0
Completeness20%Count of brief/detail/reference populated ÷ 3 × 100
Summary15%summary populated → 100, else 0
Citations15%min(citation_count × 20, 100)

Final integer score = sum of weighted components, label band: 80+ Excellent | 60–79 Good | 40–59 Fair | 20–39 Needs Work | <20 Poor.

cadenceCompliancePenalty(nextReviewDate, now) returns a non-negative penalty subtracted from the raw freshness score (clamped at 0):

Days until duePenalty
> 300
1..300–10 linear (graduated warning)
≤ 0 and overdue ≤ 1415
Overdue 15..3025
Overdue > 3040

Items with next_review_date IS NULL produce identical scores to the pre-Phase 5 model — the null guard in freshnessRaw() preserves backwards compatibility. Wired into the cron at app/api/cron/quality-score/route.ts and into UI surfaces (content-card.tsx, metadata-sidebar.tsx, QualityScoreBreakdown).

§3.2 transition table × §3.4 role-gate matrix encoded in lib/governance/publication-transitions.ts:

Current stateAdmin allowed →Editor allowed →Viewer
draftin_review, publishedin_review
in_reviewpublished, draftpublished, draft
publishedarchived, draft
archivedpublished, draft

Disallowed transitions (always 409 Conflict):

  • draft → archived (drafts are deleted, not archived)
  • in_review → archived (must return to draft or publish first)
  • archived → in_review (must restore to draft, then resubmit)
  • published → in_review (no “send back to review without revision”)

The role gate publishes 403 when the role has zero transitions out of the current state, 409 when the role can transition but not to the requested target.

SettingLocationDefaultPurpose
CHANGE_REPORT_AUTO_GEN_MAX_ITEMSlib/ai/change-reports.ts150Cost guard — rejects auto-gen on KBs with too many items in window
BATCH_SIZE (quality-score)app/api/cron/quality-score/route.ts100Items processed per DB fetch
DEFAULT_THRESHOLD (quality)app/api/cron/quality-score/route.ts40Per-domain threshold fallback when governance_config row missing
BATCH_SUMMARY_THRESHOLDapp/api/cron/quality-score/route.ts20Switch to summary notifications above this count
BATCH_THRESHOLD (freshness)app/api/cron/freshness-transitions/route.ts10Switch to summary notifications for freshness transitions
GOVERNANCE_BATCH_SUMMARY_THRESHOLDapp/api/cron/freshness-transitions/route.ts20Switch governance auto-flag notifications to summary
REVIEW_CADENCE_BATCH_SUMMARY_THRESHOLDapp/api/cron/review-cadence/route.ts20Switch review-cadence notifications to summary
Light-touch presetlib/governance/presets.tsopen / 40 / no auto-flagPermissive default
Strict presetlib/governance/presets.tsreview_on_change / 60 / 7d / 14d cooldownQuality drops + freshness transitions auto-flag
Test FileTestsCovers
__tests__/api/governance.test.ts~60/api/governance GET/POST — preset-driven config upsert
__tests__/api/governance-integration.test.ts~12Full GET→POST→GET round-trip + RLS
__tests__/api/items-patch-publication-status.test.ts~30publication_status PATCH route — transition matrix, side effects, history
__tests__/api/items-patch-publication-status-roles.test.ts~18Role gate (403 vs 409 split per §3.4)
__tests__/integration/items-patch-publication-status.integration.test.tsReal DB integration — optimistic concurrency guard (PGRST116)
__tests__/integration/publication-status-trigger.integration.test.tsenforce_archive_state_consistency trigger — bidirectional invariant
__tests__/integration/publication-status-migration.integration.test.tsMigration fixture — seed → run → assert state
__tests__/api/review/queue.test.ts~50/api/review/queue filter + sort + draft-vs-published path + include_overdue
__tests__/api/review/cadence.test.ts~30/api/review/cadence aggregation
__tests__/api/review/assignments.test.ts~28Assignment CRUD
__tests__/api/review/history.test.ts~14History query
__tests__/api/review/stats.test.ts~7Stats aggregation
__tests__/api/cron/quality-score.test.ts~50Cron — score calc, threshold cross, governance bridge, batch summary
__tests__/api/cron/freshness-transitions.test.ts~50Cron — freshness transitions, governance bridge, expiry reminders
__tests__/api/cron/review-cadence.test.ts~25Cron — overdue flag, batch summary
__tests__/lib/governance/publication-transitions.test.ts~25Pure helper — transition matrix + side effects + drift guard
__tests__/lib/governance/cadence-renewal.test.ts~10Pure helper — computeNextReviewDate(...)
__tests__/lib/governance/draft-writer-rewire-guard.test.ts~15Guard — every draft writer uses publication_status, not legacy column
__tests__/lib/quality-score.test.ts~40Quality score calculation + cadence-compliance penalty schedule
__tests__/mcp/update-publication-status.test.ts~20MCP tool parity with PATCH route
__tests__/mcp/update-governance-status.test.ts~15MCP bulk update
__tests__/mcp/governance-queue-tools.test.ts~20whats_in_my_queue (facet: governance) + review_governance_item (S207 widening)
__tests__/components/governance-section.test.tsx~25Settings UI — preset selection
__tests__/components/quality-score-breakdown.test.tsx~15Component breakdown rendering
__tests__/components/content/review-cadence-editor.test.tsx~15Cadence editor — preset/custom/range validation
__tests__/validation/content-history-change-reason.test.ts~25S153 guard — every content_history insert supplies change_reason
__tests__/lib/governance-validation.test.ts~10Schema validation
__tests__/lib/validation/schemas-publication-status.test.ts~15Zod superRefine for publication_status field
  • Visibility is not gated. All publication_status values are visible to authenticated users — the publication-lifecycle column controls workflow, not access. RPC visibility flip is §5.2 Phase 3 (roadmap).
  • No publication-review queue tab. UI surfaces for in_review items are bundled with the §5.2 Phase 4 EP2 build (deferred).
  • No supersession + cron exclusion. §5.2 Phase 5 (roadmap).
  • Email delivery not yet wired. user_notification_prefs.email_* toggles exist; the downstream cron jobs read the table but no SMTP/SES integration yet (in-app notifications only).
  • auto_flag_cooldown_days flapping. The cooldown is keyed on verified_at; rapid recovery+regression cycles within the window are suppressed but the logic has not been validated against pathological inputs.
  • Cadence-compliance modifier is one-way. It can only subtract from freshness; it never adds. An item that’s overdue and stale gets the lower of the two but no compounding bonus for being on-time.
DecisionRationaleAlternative Considered
Two separate review systems (verify/flag vs approve/request_changes)Different question — content quality vs change-management. Unifying would force workflow into one shape that fits neither well.Unified review queue with mode toggle (rejected — confused users, harder to reason about state).
Preset-driven governance_config (S177 P0-16)7 columns × N domains was a configuration nightmare; admins picked obvious presets anyway.Per-column UI (replaced); machine-learned presets (deferred — too little signal).
Auto-version trigger for content_history.versionRemoves a roundtrip per insert; race-free under SERIALIZABLE.App-side MAX(version)+1 SELECT (replaced — race condition under concurrent writes).
Optimistic-concurrency guard (.eq('publication_status', fromStatus))State-machine transitions are race-sensitive; a stale UPDATE returns PGRST116 → 409, letting the client re-fetch + retry.Pessimistic SELECT FOR UPDATE (rejected — adds round-trip, doesn’t compose with PostgREST).
Cadence-compliance penalty subtracts only (never adds)Score model is asymmetric — overdue is a quality signal; “very on-time” is the default state. Bonus would inflate scores arbitrarily.Symmetric reward/penalty (rejected — distorts existing baselines and quality-score backfill).
Mandatory change_reason on every content_history insert (S153 guard test)Provenance is the entire point of the table; silent regressions silently destroy audit value.Free-text-only without enforcement (replaced — drift detected within weeks).
Internal “digest” code, “Change Reports” UI labelRenaming hundreds of identifiers across TS+SQL was disproportionate to user-facing benefit; the label change is at the rendering boundary.Full rename (deferred to a future workstream).
'draft' removed from governance_review_status post-§5.2publication_status='draft' is now the canonical draft signal; storing it twice invited drift and confused readers.Keep both columns (rejected — would have required dual-write logic).