Module Build Status

Build-pipeline status per module. Updated by the module runbook lock gate after each module pass. The Issues column is a count only — detail lives in docs/open-items/OPEN_ITEMS.md.

Legend: ✅ done · 🚧 started (partial, see footnote) · ⬜ not started · N/A not applicable · deferred (see OPEN_ITEMS)

'Schema locked' = schema has completed the AI Capability Plane pass (AI-needed fields/flags/tables added) AND passed the Section 4 audit. The original pre-plane schemas are NOT locked under this definition.

² PLATFORMSERVICE COMPLETE 2026-06-29 (both phases, 56/56 tests green). Phase 1: tenant management cluster — tenant core (provisionTenant, updateTenant, updateTenantProfile), lifecycle state machine (transitionTenantState — validates §8 legal transitions, rejects illegal, writes lifecycle_event, sets timestamps), data lifecycle/GDPR, onboarding (createSetupTasks 19 codes, completeSetupTask → identity.actor FK, getOnboardingStatus), legal (recordAgreementAcceptance → identity.identity_user FK, getAcceptanceStatus, listAgreementVersions, getContract), tenant contacts (addTenantContact → identity.identity_user FK, updateTenantContact). Phase 2: billing/entitlements/audit — createBillingAccount/update, createSubscription (one-active partial unique honored), dunning (recordPaymentFailure→past_due→writes-lifecycle-event, processDunning, recoverDunning→recovered), resolveEntitlements (real: active-window + override>contract>beta>addon+tier stacking per spec §9), consumeAiCredit (atomic balance + spend_limit + ledger), createPromoCode/validatePromoCode/applyPromoCode, invoices/payments/usage, writeOperatorAudit/writeInternalActivity (both → identity.identity_user FK). Past_due schema gap resolved: 'past_due'+'recovered' added to event_type CHECK (migration 20260629030000). HTTP controllers remain (step 11 / pages build).

¹ IDENTITY SCHEMA DESIGN COMPLETE — all batches A–D locked 2026-06-28. Batch A LOCKED 2026-06-28 — Pass 1 (actor model) + Pass 2 (groups, role inheritance, permission_group bundles) — 18 tables, 202 cols. Batch B Pass 1 LOCKED 2026-06-28 — role management core (role_assignment, role_permission_group, role_template, role_template_permission_group; tenant_user.role_id removed) — 22 tables, 233 cols. Batch B Pass 2 LOCKED 2026-06-28 — governance, session, access management (sod_rule, sod_rule_permission, sod_violation, identity_session, access_request; +identity_access_event.session_id + event_type 14→23) — 27 tables, 295 cols. ALL OF BATCH B COMPLETE. Batch C LOCKED 2026-06-28 — machine + AI-agent identity (agent_type_catalog, service_account, api_key, agent_identity, agent_skill, agent_skill_assignment + trg_tenant_user_actor_type_check, the schema's first trigger) — 33 tables, 357 cols. Batch D LOCKED 2026-06-28 — tenant_security_policy (per-tenant session/MFA/concurrent-session/api-key-rotation config); consent_record NOT BUILT (no v1 gap — DR-29) — 34 tables, 365 cols. IDENTITY SCHEMA DESIGN COMPLETE (A–D). MIGRATION COMPLETE 2026-06-29 — Drizzle schema authored (packages/db/src/schema/identity/), migration 0001_true_loners.sql applied (34 tables / 365 cols verified in Supabase local), trigger trg_tenant_user_actor_type_check deployed, all 8 platform→identity FK constraints wired (.references() in platform Drizzle files + FK constraints in DB). POST-BUILD RECONCILIATION 2026-06-29 — full doc-vs-DB diff; migration 20260629020000_identity_drift_reconcile.sql applied (+4 cols, +6 indexes/constraints); schema + docs realigned at 34 tables / 369 cols. IDENTITYSERVICE COMPLETE 2026-06-29 — all 5 build phases done: Phase 1 Permission Resolution Engine, Phase 2 Core RBAC CRUD, Phase 3 Sessions + Auth, Phase 4 Machine + Agent Identity, Phase 5 Governance + SoD + Support Access. 50/50 integration tests green. On-approval seam (DR-22), detect-and-flag (DR-19), time-window double-check (DR-10), ALLOW-ONLY SoD (S1) all live. Pages (Phase 6) remain.

Backend foundation milestone 2026-06-29: IdentityService (50/50 tests) + PlatformService (56/56 tests) complete. Admin HTTP layer started: AdminAuthGuard (Supabase JWT + operator gate) + AdminTenantsController (7 read endpoints, 18 HTTP integration tests). Both services fully integration-tested against local Supabase; HTTP layer pattern established.

³ ADMIN CONSOLE (admin.vrida.app) SHELL BUILT 2026-06-30, RECONCILED + VERIFIED END-TO-END 2026-06-30. Repurposed the untouched create-next-app scaffold at apps/web into a multi-app structure — apps/web/admin (this build), with apps/web/tenant and apps/web/storefront planned but not yet created; workspace globs and architecture/README.md updated accordingly. Full nav shell (5 groups / 18 pages, matching the Vrida Admin Console mockup minus a redundant Feature Flags page — see PROJECT_DECISIONS #14, removed as duplicating Plans & Entitlements) + design system (General Sans, brand palette, shared Table/Badge/Chart/Drawer/Toast primitives) + one isolated mockData.ts module, all verified with a clean next build + tsc + eslint pass. Tier taxonomy reconciled: Seed/Grow/Bloom are now the official display names for the real starter/pro/enterprise keys (PROJECT_DECISIONS #14) — one tier palette used everywhere, no separate marketing ladder. Tenants list + Tenant detail Overview tab verified against a real, running apps/api + local Supabase — minted a real operator JWT, confirmed real tenant rows render correctly (no mock banner, no error boundary) — the first true end-to-end validation of the real path; the other 6 detail tabs (Lifecycle/Contacts/Onboarding/Usage/Data & Retention/Internal Notes) show as disabled with a "future slice" toast, matching the source mockup's own scope. Update 2026-06-30: Announcements and Settings later moved from 🔴 PROPOSED to real skeleton tables + read endpoints (see PROJECT_DECISIONS #15) — the current split is 3 real page groups (Tenants, Announcements, Settings) and 15 fully 🟡 sample-flagged pages with no endpoint at all (dashboard, work-queue, reports, audit, onboarding, subscriptions, invoices, payments, entitlements, plans, contracts-promotions, legal-agreements, usage, operators, support-access — see OPEN_ITEMS row 67). apps/api boot gap fixed: packages/db now has a real build step (tsc → CJS dist/) instead of shipping raw ESM TypeScript as mainapps/api now boots and serves real traffic outside Jest for the first time (125/125 tests still green). No write endpoints and no operator login flow were built this pass (see OPEN_ITEMS). Update 2026-07-08 (PROJECT_DECISIONS #41) — real-data cutover, most of the console is now real. 18 new GET routes (16 on PlatformService/7 pre-existing reused, 2 on IdentityService via its first-ever controller — see footnote ⁵, now stale). All 7 remaining tenant-detail tabs (Contracts, Lifecycle, Contacts, Usage, Data & Retention, Internal Notes, Support Access) and 8 more cross-tenant pages (Onboarding, Subscriptions, Invoices, Payments, Promotions, Agreement Versions, Audit log, Support Access) cut over from mockData.ts to real reads. Of the original 15 fully-sample page groups listed above, only 4 remain unwired by explicit in-session decision — Plans & pricing, Entitlements catalog, Operators (role field — a real schema gap, platform-level roles aren't representable today), Reports — plus the top-level Usage page's daily-chart portion (its per-tenant Usage tab is real); work-queue folded into the Dashboard tile, contracts-promotions/legal-agreements folded into the Contracts tab + Promotions/Agreement Versions pages. Two real bugs found and fixed during this pass (a bigint-returned-as-string MRR sum bug; a DATE-column UTC-timezone display bug) — see PROJECT_DECISIONS #41 for both. Dashboard aggregates scoped to 5 flagship tenants (not the full, largely schema-test-fixture-polluted platform.tenant table) — also #41. Seed data for the 10 previously-empty tables these routes read from added via packages/db/seeds/admin-console-seed.ts (additive-only). See OPEN_ITEMS rows 55/66/67 (updated) for the reconciled remaining-gap count.

TENANT ADMIN CONSOLE (tenant.vrida.app) MOCKUP BUILT 2026-06-30. apps/web/tenant created as its own Next.js app (mirrors apps/web/admin's scaffold — package.json/tsconfig/eslint/next.config, globals.css/design tokens copied verbatim). Covers all 9 tenant admin UI page areas from identity.md §17 (Members, Groups, Roles & Permissions, Access Requests, SoD Compliance, Service Accounts, AI Agents, Active Sessions, Security Policy), grouped into 4 nav sections (People/Access/Identities/Security). Every page reads from one isolated, fully-typed lib/mockData.ts module (~25 identity tables, field-for-field matching docs/database/schema_docs/identity.md) — no invented fields, verified by an independent per-page review pass. Shared primitives copied/adapted from apps/web/admin (Table, Badge, DataFlagBanner, palette, Drawer, Toast, ToggleSwitch) plus two NEW extracted-for-reuse components (DetailCard — Card/Grid/Pill, previously duplicated inline in admin's TenantSkeletonPanels.tsx; TabSwitcher — generic tab strip). All 9 pages are 🟡 SAMPLE DATA — zero API calls, zero auth, zero tenant-context wiring, since IdentityService (86 methods, tested) has no HTTP endpoints at all yet (see OPEN_ITEMS). Verified with a clean next build + eslint pass and a full browser click-through of all 9 pages + the /members/[id] detail route. One real bug caught and fixed during the build: a Server Component passing a live event handler into the ToggleSwitch Client Component on /security-policy (Next.js RSC boundary violation) — fixed via a small ReadOnlyToggleRow client wrapper. Enterprise-heaviness scoping flag for a small single-location nursery (human judgment call, not yet decided): Roles & Permissions' 6 sub-tabs, Service Accounts, AI Agents, Access Requests, and SoD Compliance all read as heavier than day-to-day small-nursery needs — Members, Groups, and Active Sessions read as appropriately scoped.

IDENTITY HAD ZERO HTTP CONTROLLERS (confirmed 2026-06-30; STALE as of 2026-07-08 — see update below). IdentityService is fully built (86 methods, 50/50 integration tests across its own 5 phases, 2026-06-29) but had no REST surface at all — find apps/api/src -iname "*.controller.ts" returned only app.controller.ts and 3 platform controllers (AdminTenantsController, AdminAnnouncementsController, AdminSettingsController); none referenced IdentityService except AdminAuthGuard, which calls one method (resolveActorFromAuthUser) purely to resolve the caller's identity, not to expose IdentityService's own capabilities. Marked ⬜ (not started) rather than a caveated ✅ — unlike Platform, which had a genuine partial HTTP layer (3 controllers, 9 real endpoints), Identity had none. See OPEN_ITEMS row 63 (now closed). Update 2026-07-08 (PROJECT_DECISIONS #41): no longer true. IdentityController now exists — Identity's first-ever HTTP-reachable route, 2 endpoints (GET /admin/tenants/:id/support-access, GET /admin/support-access), both wrapping listSupportAccessGrants(tenantId?). Every other one of IdentityService's 86 methods remains service-layer-only until its own controller is built — this closes the "zero controllers" gap specifically, not the broader HTTP-surface gap.

SHARED MODULE LOCKED 2026-07-05 (module #3) — 10 tables, 101 cols, fully seeded. Natural-key PKs on 9 of 10 tables (currency/country/language/locale/administrative_region/unit_of_measure/climate_zone use their ISO/short code as PK; plant uses UUID — the one deliberate exception, since taxonomic names aren't permanently stable). A Section 4 audit FAIL on administrative_region.region_type's CHECK list (originally 6 values, insufficient for real-world ISO 3166-2 type diversity) was fixed before lock by expanding to 21 values. Seed scope was honestly scaled down from originally-discussed targets in two places: administrative_region (186 rows — comprehensive for US/Canada/Mexico/UK/Australia, representative for 5 more countries, not the full ~3-4K global ISO 3166-2 set) and plant (114 rows, not "a few hundred") — both to avoid generating volume that couldn't be verified accurately from memory. 26 new schema/seed-integrity tests added (151/151 total suite still green, no regressions). No SharedService built this pass — schema + seed data only; downstream modules query shared.* directly via Drizzle until a consuming module needs an abstraction. See PROJECT_DECISIONS #17.

MULTI-LOCATION SCHEMA LOCKED 2026-07-05 (module #4) — 1 table (site), 29 cols (up from v1's 21, net +8 from the global-address redesign). Global from day 1: natural-key FKs into shared (country/administrative_region/climate_zone) replace v1's US-shaped address JSONB; currency/locale resolve via country_code join rather than being stored; measurement_system and climate_zone_code's system-appropriateness are service/UI-enforced (no shared lookup exists for either) — see PROJECT_DECISIONS #18. New DB-enforceable chk_site_region_country_match CHECK. 4 pre-existing deferred FKs (platform.tenant.primary_site_id, identity.tenant_user.default_site_id, identity.user_site_assignment.site_id, identity.user_permission_override.scope_id) are now unblocked but deliberately not wired this pass — see OPEN_ITEMS. Tests: 1 test suite, 6 tests, all passed (0 failed). Sections: A(1) table exists, B(1) RLS enabled, C(1) valid insert incl. DE/DE-BW/rhs:h4, D(1) chk_site_region_country_match rejects DE+US-CA, E(1) is_primary partial-unique rejects second primary for same tenant, F(1) climate_zone_code FK resolves via LEFT JOIN with zero unresolved rows. All inserted rows cleaned up by slug (verified 0 stray test-multi-loc-% rows remain in DB post-run). Full suite: 11 test suites passed, 157 tests passed, 0 failed (run via npx jest --forceExit in apps/api). No regressions introduced by the new file. No MultiLocService yet.

AUTONOMY-FIRST BACKFILL applied 2026-07-06 across platform (+6 cols), identity (+2 cols), shared (+7 cols), multi_loc (+8 cols) — see PROJECT_DECISIONS #19 for the canonical pattern and full rationale. 33 new tests added (platform+10, shared+19, multi_loc+5, identity+4), full suite 197/197 passing.

AGENT-DUTY-GRANT BUILT 2026-07-06 — the A5 agent-authority passport (AI_CAPABILITY_PLANE.md A5). +identity.agent_duty_grant (+1 table, +25 cols) — per (agent, permission) authority grant recording draft/execute/needs-approval authority and spend/quantity ceilings; complements (does not conflict with) role.requires_approval_for_agents's grant-time gate (PROJECT_DECISIONS #21). identity_access_event.event_type CHECK expanded 39→42 (additive). 34→35 tables, 371→396 cols. Two bugs caught by independent adversarial verification during design, before any migration was written, and fixed pre-build: (1) the unique index originally included scope_id/scope_code, which — since Postgres treats NULL as distinct in unique indexes — would have silently let two active tenant-scoped grants coexist for the same (agent, permission); fixed to (agent_identity_id, permission_id, scope_type) WHERE status='active', matching user_permission_override_unique's real shape. (2) scope_id's FK to multi_loc.site was originally proposed as deferred, incorrectly citing a precedent (user_permission_override.scope_id) whose justification (multi_loc unbuilt) no longer applies now that multi_loc is locked; fixed to a real, enforced FK. See PROJECT_DECISIONS #22, schema_docs/identity.md DR-31/DR-32. 10 new schema/seed-integrity tests added (identity-agent-duty.spec.ts) — including the one-active-grant regression test (D1) proving the bug fix actually holds. Full suite: 15 test suites, 210 tests, all green (2 consecutive runs). No IdentityService methods built this pass (schema-only) — grantAgentDuty()/revokeAgentDuty()/checkAgentDutyAuthority() remain open (see OPEN_ITEMS).

¹⁰ CRM SCHEMA LOCKED 2026-07-06 (module #5) — the first actually-built v2 PRODUCT module (modules #1-4 — platform/identity/shared/multi_loc — were all foundation/service-layer). 13 tables, 189 cols: customer (core entity, individual/business discriminator, full autonomy treatment), contact, address (global shape, mirrors multi_loc.site), customer_group, customer_note (append-only, no review seam — additive/zero-risk), customer_task, customer_merge_candidate (the review gateway for dedup), customer_merge (append-only post-execution audit), customer_consent (append-only, never AI-initiated), customer_tax_certificate, customer_segment_definition (NEW — mirrors identity.role's mixed-scope built-in/tenant-custom pattern), customer_segment_membership (NEW — agent-computed VIP/at-risk/seasonal/wholesale-like classification, the primary home for D6/D7 autonomy this pass), customer_tag_assignment (NEW — human free-form labels, deliberately kept separate from segments). Pure consumer of identity.agent_duty_grant — introduces no new authority mechanism (illustrative crm:* permission codes, module_code='crm' requires zero identity-module changes). Two issues caught and fixed before/during the migration, not after: (1) customer_tax_certificate's two jurisdiction-scoped partial unique indexes were designed up front to split on issuing_region_code IS NULL vs IS NOT NULL, avoiding the same NULL-in-multi-column-unique trap agent_duty_grant had originally hit (PROJECT_DECISIONS #22) — caught by applying that precedent proactively during design, not by a live failure this time. (2) customer_segment_membership's active/expired state was originally drafted as a partial index predicate on WHERE expires_at > now(); confirmed empirically pre-migration (ERROR: functions in index predicate must be marked IMMUTABLEnow() is STABLE, not IMMUTABLE) that Postgres rejects this, so it was fixed to a stored status column before the migration was ever run, matching the on-read/sweep convention identity.role_assignment.ends_at/agent_duty_grant.ends_at already use. See PROJECT_DECISIONS #23, schema_docs/crm.md DR-33 through DR-47. Migration 20260706060000_crm_module.sql applied to the live local Supabase DB (schema crm, 13 tables verified via information_schema.columns). No CrmService exists yet (schema-only this pass) — see OPEN_ITEMS.

¹¹ INVENTORY SCHEMA LOCKED 2026-07-06 (module #12) — the biggest module built so far, first real shared.plant consumer. 24 tables, 335 cols (up from v1's locked 21 tables/238 cols baseline — the same vertical-neutral, post-pivot design, just pre-shared.plant and pre-autonomy; delta is +3 tables/+97 cols, not a rebuild from scratch). Catalog: item, item_variant, category, item_category, tag, item_tag, barcode, item_image. Variant structure: option_type, variant_option. Locations: inventory_location. Stock: stock, stock_movement, stock_movement_line, stock_reservation, stock_adjustment_reason, stock_adjustment_request (NEW — propose/execute stock-adjustment gate, needs_approval always, never may-act-alone; ONE partial unique (tenant_id, site_id, variant_id) WHERE review_status='pending' prevents duplicate pending proposals even across located/unlocated variants — live-tested). Counts: stock_count, stock_count_line. Lots: lot, stock_lot (adds condition_grade CHECK A/B/C/cull, closing v1's never-built plant condition grading). Kits: kit_component. Merge: item_merge_candidate/item_merge (NEW — mirrors crm.customer_merge_candidate/customer_merge's propose/execute split, but closes a real gap crm's own audit found and left open: chk_item_merge_candidate_pair_order CHECK (item_a_id < item_b_id) + a WHERE review_status='pending' partial unique together reject both mirrored-pair and duplicate-pending proposals — live-tested). item.plant_id — nullable FK → shared.plant.id (ON DELETE SET NULL), CHECK restricts it to item_type='plant' rows. UOM columns (sell_uom_code/stock_uom_code/purchase_uom_code/weight_uom_code/capacity_uom_code) retyped to text FKs → shared.unit_of_measure.code, matching the actual locked shape (v1 had assumed uuid). All actor columns retargeted to identity.actor (not identity.identity_user). stock_movement.photo_ref — plain nullable uuid, no FK — deferred forward-ref to an unbuilt files schema (see OPEN_ITEMS). Pure consumer of identity.agent_duty_grant — introduces no new authority mechanism. Migration 20260706070000_inventory_module.sql applied to the live local Supabase DB (schema inventory, 24 tables / 335 cols verified via information_schema, all 24 RLS-enabled). See PROJECT_DECISIONS #24, schema_docs/inventory.md. No InventoryService exists yet (schema-only this pass) — see OPEN_ITEMS. 2026-07-11 companion reopen (see footnote ²³): stock_movement.source_module CHECK widened to accept 'returns' — constraint-shape only, table/column counts unchanged, see PROJECT_DECISIONS #61.

¹² AI SCHEMA LOCKED 2026-07-06 (module #6, cross-cutting) — the agent-runtime schema layer. 7 tables, 118 cols (up from v1's locked 4 tables/71 cols baseline — reconciled with light retargeting only: import_job, import_file, import_record, ai_request; delta is +3 tables/+47 cols, not a rebuild from scratch). New this pass: agent_execution (append-only action ledger — resolves_execution_id nullable self-FK → ai.agent_execution.id links a later "executed" row back to its earlier "proposed" row via direct JOIN, replacing a shared polymorphic target_table/target_row_id key that independent verification proved doesn't match the real dedicated-FK precedent inventory.stock_movement.adjustment_request_id/crm.customer_merge.candidate_id established; idempotency_key + UNIQUE (tenant_id, agent_identity_id, idempotency_key) WHERE idempotency_key IS NOT NULL dedup protection, matching inventory.stock_movement.idempotency_key's precedent — live-tested duplicate rejection). agent_usage_period (atomic-upsert cost/token usage meter — reconciliation formula sums ai_request.total_token_count directly off ai_request.agent_identity_id, not indirectly through agent_execution.ai_request_id, closing a silent-undercount bug caught pre-migration; every increment MUST be a single atomic INSERT ... ON CONFLICT ... DO UPDATE statement, never an app-level read-then-write — closes both a lost-update race and a duplicate-insert race). agent_memory (durable agent memory store — UNIQUE (tenant_id, category, key) WHERE status='active' partial unique, matching identity.agent_duty_grant's own precedent exactly; live-tested that disabling a memory row and inserting a fresh active row for the same key succeeds and preserves both rows as history, not a destructive overwrite). ai_request.agent_identity_id — new nullable FK → identity.agent_identity (v1 predated agent_identity entirely). Pure consumer of identity.agent_duty_grant/agent_identity for all agent authority — introduces no new authority mechanism. import_file.file_id stays a deferred forward-ref (plain uuid, no FK — files schema still unbuilt). Migration 20260706080000_ai_module.sql applied to the live local Supabase DB (schema ai, 7 tables / 118 cols verified via information_schema, all 7 RLS-enabled). See PROJECT_DECISIONS #25, schema_docs/ai.md. No AIService extensions exist yet for the 3 new tables (schema-only this pass) — see OPEN_ITEMS.

¹³ PRICING SCHEMA LOCKED 2026-07-06 (module #13) — the first module of the sell path, outside-critique-resolved design. 4 tables, 75 cols (up from v1's locked 4 tables/56 cols baseline — table count unchanged, +19 cols). price_level (11 cols), price_rule (33 cols), price_list_assignment (19 cols), price_change_log (12 cols, append-only). An outside-expert gap analysis (3 independent cold-read critiques against Odoo/Shopify/Magento/NetSuite/Stripe pricing-engine patterns) found real gaps, all 6 schema-fixable ones resolved directly in schema rather than deferred: supersede-don't-edit price history (chk_price_rule_no_self_supersede, chk_price_rule_superseded_by_consistency, cross-tenant supersession blocked by the new trg_price_rule_validate_supersession trigger — live-tested), explicit site-scoping for customer-scoped rules (chk_price_rule_applies_all_sites_xor_site_id, chk_price_rule_customer_scope_site_explicit), tax_treatment, cost_plus_percent markup pricing (chk_price_rule_price_value bounds enforced, -50 clearance markdown succeeds, 1500 above ceiling rejected — both live-tested), campaign_label, and a pending-scope dedup unique index (price_rule_pending_scope_dedup_unique) that correctly rejects a genuine duplicate pending proposal while letting two different pending price_types for the same scope coexist — both live-tested. 22 CHECK constraints total; RLS enabled on all 4 tables; 4 triggers (set_updated_at on 3 tables + the 1 new trigger function). 3 additional non-schema gaps recorded as binding hard contracts (not vague deferrals): pos.sale_line must snapshot the resolved price at sale time, a single versioned resolvePrice() spec with cross-language golden test vectors, and display rounding as a PricingService-only concern — see module_spec/pricing.md. Migration packages/db/migrations/20260706090000_pricing_module.sql applied to the live local Supabase DB (schema pricing, 4 tables / 75 cols verified via information_schema, all 4 RLS-enabled). Design-phase verification ran twice (original 4-table proposal, then again on the round-2 expanded design after the 6 gaps were folded in), each via an independent 3-agent Workflow, finding and fixing real bugs both times before this build. See PROJECT_DECISIONS #26, schema_docs/pricing.md. No PricingService exists yet (schema-only this pass) — see OPEN_ITEMS.

¹⁴ POS SCHEMA LOCKED 2026-07-07 (module #14) — the offline-first sell-path module. 9 tables, 138 cols (down from v1's locked 19 tables/280 cols baseline — the 10-table cut is deliberate deferral, not a design regression; see OPEN_ITEMS for each deferred table's concrete trigger). register (8 cols), register_session (18 cols), register_cash_entry (9 cols), sale (24 cols), sale_line (14 cols), sale_payment (26 cols), sale_refund (22 cols), sale_refund_line (7 cols), pos_sync_conflict (10 cols). Offline-first is the defining constraint: sale/sale_payment/sale_refund each carry client_uuid + origin + sync_status + idempotency_key + synced_at, with UNIQUE (tenant_id, client_uuid) WHERE deleted_at IS NULL giving duplicate-sale replay a free, conflict-row-free dedup via INSERT ... ON CONFLICT DO NOTHING; the distinct genuine-oversell case (two different offline devices, two different client_uuids, same last unit) is NOT caught by that dedup and instead requires a not-yet-built InventoryService.completeSale() to detect the would-go-negative decrement and write a pos.pos_sync_conflict row (conflict_type='stock_oversell') — only the schema's ability to represent that conflict row is built and tested this pass (test B3). New chk_<table>_origin_sync_status_consistency CHECK on all 3 tables enforces online-created rows stay sync_status='synced'. Pricing's Hard Contract 1 honored verbatim on sale_line (6 snapshot fields: resolved_amount_minor_units, charged_amount_minor_units, currency_code, tax_treatment, resolving_price_rule_id, resolved_quantity) — live-tested (test C1) that superseding the resolving price_rule afterward does not change the already-recorded sale line. No POS-side reservation table: a POS sale calls InventoryService.completeSale() directly, netting out via the existing inventory.stock.available_qty generated column (inventory.stock_reservation.source_type CHECK deliberately excludes 'pos' — reservations stay Orders' own mechanism). sale.total_minor_units is immutable-gross forever (live-tested, test D1: a sale_refund insert never changes it); sale.status CHECK is ('open','completed','voided') with deliberately no 'refunded' value — net refunded position is a query-time sum over sale_refund. A sale.search_vector column from the earlier design draft was dropped during build (no free-text field on sale to index) — logged to OPEN_ITEMS as deferred, not silently dropped. 6 triggers total (5x set_updated_at + 1x trg_pos_sale_requires_open_session), 41 CHECK constraints total; RLS enabled on all 9 tables. New cross-module seam: orders.order_header.fulfilled_sale_id -> pos.sale (FK lives on Orders' side, not built yet; direction Orders → pos, "link-don't-convert; tax finalizes at POS"); Payments seam (sale_payment.stripe_payment_intent_id, sale_payment.charge_account_ref) recorded as forward-refs, no FK yet. Migration packages/db/migrations/20260707000000_pos_module.sql applied to the live local Supabase DB (schema pos, 9 tables / 138 cols verified via information_schema, all 9 RLS-enabled). See PROJECT_DECISIONS #27, schema_docs/pos.md. No POSService exists yet (schema-only this pass) — see OPEN_ITEMS. Tests: 16/16 pos-specific tests passing (apps/api/src/pos/__tests__/pos-schema.spec.ts), full apps/api suite 314/314 passing, no regressions. 2026-07-11 companion reopen (see footnote ²³): sale_refund/sale_refund_line each gained UNIQUE(id, tenant_id), a prerequisite for returns' own composite FKs — constraint-shape only, table/column counts unchanged, see PROJECT_DECISIONS #61.

¹⁶ PURCHASING SCHEMA LOCKED 2026-07-07 (module #16) — the BUY path, completing the supply loop opposite orders/pos. 16 tables, 398 cols (up from v1's locked 16 tables/341 cols baseline — +57 cols, ZERO tables consolidated or dropped; the stale MODULE_INDEX placeholder held 16/341). Second module built under the Design-Phase Integrity rules — the retained 4-block v1→v2 delta record lives in PROJECT_DECISIONS #30, grep-verified in-file at lock (Section 6 items 12+15). Groups: Vendor Master (vendor 37 — master, +linked_customer_id → crm.customer for the grower-who-also-buys-retail overlap, −search_vector; vendor_contact 16; vendor_address 17 — global-reshaped to shared.administrative_region/country, no 'US'/'USD' defaults, +region/country CHECK mirroring crm.address; vendor_item 29 — the cost-change-flag surface), Purchase Orders (purchase_order 44 — the reorder-agent draft target; purchase_order_line 30 — dual-UOM cost snapshot frozen at PO time, preserved verbatim; templates 15/10), Receiving (purchase_receipt 30 — +idempotency_key NULL-safe unique for the InventoryService.receive() dedup; purchase_receipt_line 27), Invoice+3-Way-Match (vendor_invoice 39 — status excludes 'paid', Billing owns payment via write-backs; vendor_invoice_line 20; vendor_invoice_match 18 — N:M line-to-line), Credit+Return (vendor_credit 24, vendor_return 23, vendor_return_line 19 — all LIGHT, the resolved tier). Autonomy: 7 FULL / 8 LIGHT / 1 ZERO; purchasing's LIGHT carries automation_source uniformly (a disclosed deviation from crm's created_by-only light tier). 3 previously-DEFERRED v1 forward-ref FKs are REAL now (targets exist live): purchase_receipt_line.inventory_movement_id/vendor_return_line.inventory_movement_id → inventory.stock_movement (movement_type='received'/'returned' — the receiving/return seam, +avg-cost update on receipt); purchase_order.source_order_id → orders.order_header. The reciprocal orders.order_header.draft_po_id → purchasing.purchase_order — the forward-ref orders locked WITHOUT — is CLOSED this build via an ALTER (constraint order_header_draft_po_id_fkey, not an orders reopen); documented on both sides in CROSS_MODULE_CONTRACTS. +vendor.linked_customer_id → crm.customer. An independent adversarial design-phase verification pass (before build) found and fixed 3 BLOCKER + 2 disclosure issues (a vendor off-by-one, conflicting totals, a crm LIGHT-tier mis-citation now disclosed as a deliberate deviation, a DR-E tier justification, an undisclosed column rename reverted). 6 v1 service-only rules promoted to real CHECKs (PO approved-requires-actor/at, invoice disputed/void/approved-requires-timestamp, invoice-line item-requires-variant, return rma-when-authorized + authorized-when-not-draft, match resolved-requires-actor/at). The reorder→PO draft loop is the flagship buy-side agent path — PO-send stays needs_approval ALWAYS (the C8 financial-autonomy boundary); the cumulative agent spend-ceiling is deferred (cross-ref existing identity/ai/pricing rows, not reopened). Consumes identity.agent_duty_grant — no new authority mechanism. Migration packages/db/migrations/20260707070000_purchasing_module.sql applied live (schema purchasing, 16 tables / 398 cols verified via information_schema, all 16 RLS-enabled, 61 CHECK + 111 FK constraints, set_updated_at on all 16). Two independent non-self-grading verifiers confirmed ZERO FAILs: Section 4 (all A-P+T+U PASS) and the seams+FK pass (receiving seam, orders seam both directions with a live 23503 draft_po_id-rejection reproduction, linked_customer_id, idempotency dedup + NULL-distinctness, FK resolution across all targets, dual-UOM snapshot). See PROJECT_DECISIONS #30, schema_docs/purchasing.md, module_spec/purchasing.md. No PurchasingService exists yet (schema-only). Tests: 35/35 purchasing-specific tests passing (apps/api/src/purchasing/__tests__/purchasing-schema.spec.ts); full apps/api suite 412/412 green run serially (--runInBand) — under default parallel execution 3 identity tests intermittently fail on a pre-existing shared-DB race on global identity tables (pass 25/25 in isolation), unrelated to purchasing.

¹⁵ ORDERS SCHEMA LOCKED 2026-07-07 (module #15) — completes the sell path (crm → pricing → inventory → orders → pos). 7 tables, 173 cols (up from v1's locked 7 tables/133 cols baseline — zero tables consolidated or dropped, pure addition of +40 cols; the stale MODULE_INDEX placeholder this row previously held, 7/133, was never a real v2 build). First module designed AND built under the Design-Phase Integrity rules (SCHEMA_DESIGN_RUNBOOK.md Section 0.5/2.3.6, written earlier the same day) — the full retained 4-block delta-accounting record lives in PROJECT_DECISIONS #29, not just a chat proposal. order_header (44 cols — root commercial-order record, accepted_by text preserved AND joined by a new accepted_by_actor_id per an explicit dual-column decision, confirmed_at-required CHECK deliberately widened beyond v1's literal single-state text), order_line (38 cols — Pricing's Hard Contract 1 6-field snapshot honored verbatim, stock_reservation_id real FK, price_override/price_override_reason restored — a capability pos.sale_line itself dropped), order_payment (27 cols — deposit/milestone/balance schedule, pos_sale_payment_id real FK), order_fulfillment (26 cols — fulfillment batch header, ship_to_address_id real FK), order_fulfillment_line (13 cols, byte-for-byte unchanged from v1, zero autonomy cols), order_template (16 cols), order_template_line (9 cols, byte-for-byte unchanged, zero autonomy cols). 3 real (not deferred) cross-module seams, all live-tested: order_line.stock_reservation_id → inventory.stock_reservation; order_header.fulfilled_sale_id → pos.sale (ON DELETE RESTRICT — deleting a referenced sale is rejected, proven); order_payment.pos_sale_payment_id → pos.sale_payment — Pricing's Hard Contract 1 is now SATISFIED by both pos and orders. An independent adversarial design-phase verification pass (before any table was built) found and fixed 5 real issues: an under-specified cancel-CHECK (only 2 of v1's actual 3 required fields), a fully unaddressed index-disclosure gap, 2 missing non-negativity CHECKs, an unjustified partial autonomy-tier assignment on order_line/order_fulfillment (both upgraded to the full 7-column pack), and an undisclosed CHECK-scope widening (confirmed_at) — all fixed before the design was ever presented for approval. 6 v1 service-enforced-only rules promoted to real DB CHECKs, incl. the fixed cancel-CHECK requiring all 3 fields (cancelled_at/cancelled_by_actor_id/cancellation_reason) — live-tested as a regression (all 3 missing-combinations rejected, all-3-present succeeds). Idempotency: a single light idempotency_key (not pos's offline-sync quintet — Part D D12 explicitly ruled that out, orders is online-first), two-partial-unique NULL-actor-safe split matching pricing.price_rule's precedent — NULL-distinctness explicitly checked (order_number is NOT NULL, no trap possible; both idempotency indexes exclude NULL-key rows, live-verified). pg_trgm fuzzy-search index on order_number confirmed absent live — correctly deferred, not silently dropped, matching inventory's identical gap. Consumes identity.agent_duty_grant for all agent authority — no new authority mechanism introduced. Migration packages/db/migrations/20260707060000_orders_module.sql applied to the live local Supabase DB (schema orders, 7 tables / 173 cols verified via information_schema, all 7 RLS-enabled, 46 CHECK + 43 FK constraints). See PROJECT_DECISIONS #29, schema_docs/orders.md, module_spec/orders.md. No OrderService exists yet (schema-only this pass) — see OPEN_ITEMS. Tests: 41/41 orders-specific tests passing (apps/api/src/orders/__tests__/orders-schema.spec.ts), full apps/api suite 418/418 passing, no regressions. 2026-07-11 companion reopen (see footnote ²³): order_header/order_line each gained UNIQUE(id, tenant_id), a prerequisite for returns' own composite FKs — constraint-shape only, table/column counts unchanged, see PROJECT_DECISIONS #61.

¹⁷ TAX SCHEMA LOCKED 2026-07-07 (module #17) — the first module in this build with ZERO v1 precedent. 2 tables, 37 cols (up from v1's 0 tables — v1 fully outsourced tax to Stripe Tax with no local schema at all: confirmed via find docs/old -iname "*tax*" returning nothing, cross-referenced against Payments' MODULE_INDEX row "tax → Stripe Tax" and Admin's "tax config (Stripe Tax)"). This is the FIRST module in the entire discrepancy-tracking history that is a genuine +1-schema/+2-table addition, not a delta against a stale pre-existing placeholder row. Exists because pos's own 19-to-9 audit found sale_line.tax_amount_cents/tax_rate collapse to a flat aggregate with no per-jurisdiction breakdown — an unrecoverable loss for stacked-tax remittance, logged as a PRE-CUSTOMER OPEN_ITEMS blocker; this build closes that gap. tax_calculation (28 cols — header, one row per calculation event, the nexus/rate-anomaly-flag surface; automation_source DEFAULTS 'system', the first justified deviation from the universal 'human' default anywhere in this codebase — verified live, zero exceptions before this table) and tax_calculation_jurisdiction (9 cols — append-only per-jurisdiction breakdown restoring the lost decomposition; structurally append-only by shape, no updated_at/updated_at/deleted_at columns exist at all, confirmed live via information_schema). Polymorphic tax_calculation.source_refpos.sale_line.id or orders.order_line.id, validated by source_module/source_type CHECK pairs — no reciprocal column on either locked table, matching billing.ar_charge.source_ref's established pattern. supersedes_calculation_id (self-FK) is scoped to same-source_ref corrections only, mirroring pricing.price_rule's supersede-don't-edit pattern. An independent adversarial design-phase verification pass (before build, run jointly with billing's proposal) found and fixed 2 real BLOCKERs: a missing purchase_order.amount_paid_cents write-back target in billing's design (not a tax-side fix, logged against billing), and an over-claimed reconciliation mechanism — the original draft asserted supersedes_calculation_id bridges an orders-time estimate to its later pos-time final calculation, which is structurally impossible since no line-level link exists between order_line and sale_line (only header-level order_header.fulfilled_sale_id → pos.sale); resolved via explicit decision (option b): the two rows deliberately COEXIST as independent records, reconciled only at report time via the header-level join — a documented design decision, not a schema gap. total_tax_amount_cents = SUM(tax_calculation_jurisdiction.tax_amount_cents) is a build requirement, live-tested against a 3-jurisdiction stacked fixture (state 6.00% + county 0.25% + district 1.00%, summing correctly). Migration packages/db/migrations/20260707080000_tax_module.sql applied to the live local Supabase DB (schema tax, 2 tables / 37 cols verified via information_schema, both RLS-enabled, 14 CHECK + 10 FK constraints, 2 triggers on tax_calculation only — set_updated_at + trg_tax_calculation_validate_supersession — the jurisdiction child has neither, having no updated_at column at all). A post-lock independent verification pass (same day) found the same-source_ref supersession scoping (resolved decision 1) was documented but not DB-enforced — closed via trg_tax_calculation_validate_supersession, mirroring pricing.trg_price_rule_validate_supersession, live-reproduced and covered by a new regression test. See PROJECT_DECISIONS #31 (incl. addendum), schema_docs/tax.md, module_spec/tax.md. No TaxService exists yet (schema-only). Tests: 24/24 tax-specific tests passing (apps/api/src/tax/__tests__/tax-schema.spec.ts).

¹⁸ BILLING SCHEMA LOCKED 2026-07-07 (module #18) — the SETTLE half of the financial layer, built same day immediately after tax. 9 tables, 162 cols (up from v1's locked 8 tables/113 cols baseline — all 8 v1 tables preserved 1:1, +1 NEW table ar_adjustment, +49 cols total). ar_account (19, FULL), ar_charge (26, FULL — the tax seam), ar_payment (23, FULL), ar_payment_application (9, LIGHT append-only), ar_statement (21, LIGHT), ar_adjustment (22, FULL, NEW — the write-off/dispute-resolution table v1 deferred, resolved decision 1), vendor_payable (16, LIGHT), ap_payment (17, LIGHT), ap_payment_application (9, LIGHT append-only). Asymmetric autonomy tiering disclosed deliberately: A/R gets FULL (collections/dunning-flag, payment-matching-ambiguity, write-off/dispute-resolution are real judgment surfaces); A/P stays LIGHT (the real judgment already happened upstream at purchasing.vendor_invoice's own FULL gate). ar_charge.tax_calculation_id → tax.tax_calculation.id is the NEW tax seam (nullable, real FK). ar_charge.source_ref/.source_payment_ref are polymorphic → pos.sale/orders.order_header and pos.sale_payment/orders.order_paymentplain uuid, NO single-table FK (structurally impossible for a two-target polymorphic column; the design proposal's "now REAL FKs" language meant the target schemas now exist to validate against, not that a single-column FK is possible), validated by the source_module/source_type pair CHECK instead — identical pattern to tax.tax_calculation.source_ref, established the same day. A live NULL-distinctness bug was caught and fixed during this build's own test-writing pass (not deferred to post-lock): a naive single 5-column idempotency index on ar_charge would let two charges for the same source_ref both land with a NULL source_payment_ref without colliding, since Postgres treats every NULL as distinct — fixed via a two-partial-unique split (ar_charge_idempotency_full_unique + ar_charge_idempotency_no_payment_ref_unique), mirroring orders.order_header's own idempotency_key precedent exactly. An adversarial design-phase pass (run jointly with tax's proposal) found and fixed 1 real blocker specific to billing: a missing purchase_order.amount_paid_cents write-back target, now documented as a SUM-rollup requirement (one PO can have multiple vendor_payables), not a naive 1:1 copy. Migration packages/db/migrations/20260707090000_billing_module.sql applied live (schema billing, 9 tables / 162 cols verified via information_schema, all 9 RLS-enabled, 47 CHECK + 47 FK constraints, set_updated_at on 7 of 9 tables — the 2 append-only application junction tables correctly have none). See PROJECT_DECISIONS #32, schema_docs/billing.md, module_spec/billing.md. No BillingService exists yet (schema-only). Tests: 47/47 billing-specific tests passing (apps/api/src/billing/__tests__/billing-schema.spec.ts); full apps/api suite 483/483 green serially (--runInBand). Reopened 2026-07-18 for the Phase 3 Gift Card + Store Credit build (PROJECT_DECISIONS #71) — +6 tables/+84 cols (10→16 tables, 176→260 cols): gift_card/gift_card_transaction/gift_card_reversal_tracker, store_credit_account/store_credit_transaction/store_credit_reversal_tracker, plus the stored_value_liability VIEW — the first phase in this build sequence to add real business capability, unblocking pos.sale_payment's fail-closed gift_card/store_credit tender gate (real composite FKs replace the old forward-refs; the gate narrows to payment_method != 'reward'). All 8 named guards (negative-balance impossibility, concurrency race, double-redemption, append-only both layers, cross-tenant, code-hash security, doubly-capped clawback, the CHECK relaxation) live-reproduced. billing-stored-value.spec.ts (42 tests, new). No BillingService stored-value methods yet.

¹⁹ PAYMENTS SCHEMA LOCKED 2026-07-07 (module #19) — the money-MOVEMENT layer, executes what billing (#18) records. 9 tables, 158 cols (up from v1's locked 8 tables/112 cols baseline — all 8 v1 tables preserved, +1 NEW table terminal_reader, +46 cols total). stripe_connect_account (16, LIGHT), payment_intent (32, FULL — the central execution ledger), payment_refund (21, FULL — the clearest C8 boundary in this module), payout (20, FULL), dispute (21, FULL), payment_method (15, LIGHT), stripe_event_log (8, ZERO — insert-once/status-updated, global non-tenant-scoped unique on stripe_event_id, nullable tenant_id preserved verbatim from v1), stripe_event_dead_letter (10, ZERO), terminal_reader (15, LIGHT, NEW — closes OPEN_ITEMS row 185's pos.register hardware-pairing gap). payment_intent.source_ref polymorphic → pos.sale_payment/orders.order_payment/billing.ar_payment (plain uuid, no FK — 3-way widened from v1's pos/billing-only pair, 'sale' renamed 'sale_payment'). chk_payment_intent_source_pair is a genuine fix caught by independent adversarial verification DURING THE DESIGN-PHASE PASS (not v1-preserved — v1 never had this CHECK either): rejects invalid source_module/source_type combos, mirroring tax.tax_calculation/billing.ar_charge's own source-pair CHECK. The same adversarial pass also caught terminal_reader.stripe_reader_id missing its expected UNIQUE (fixed) and an arithmetic error in the automation_source='system' table count (corrected to 3 more/4 total). The idempotency/double-charge guard was checked specifically against the same-day billing.ar_charge NULL-distinctness bug class and confirmed clean pre-build. 5 hardcoded 'USD' currency defaults removed (global from day 1). The money boundary (billing records, payments executes, platform is Vrida's own unrelated SaaS revenue) required a corrective column-comment fix on platform.subscription.status, folded into this build's migration. First module built under the new evidenced independent-verification gate (SCHEMA_DESIGN_RUNBOOK §2.3.7/2.7/6.1a, added the same day after this module's own design proposal was initially self-graded "Zero FAILs" and found wrong by adversarial review). A SEPARATE agent independently re-audited the built schema post-build and found zero FAILs — full pasted findings in PROJECT_DECISIONS #33. Migration packages/db/migrations/20260707100000_payments_module.sql applied live (schema payments, 9 tables / 158 cols verified via information_schema, all 9 RLS-enabled with real tenant-isolation policies, 35 CHECK + 32 FK constraints, set_updated_at on 7 of 9 tables — the 2 webhook tables correctly have none). See PROJECT_DECISIONS #33, schema_docs/payments.md, module_spec/payments.md. No PaymentsService exists yet (schema-only). Tests: 46/46 payments-specific tests passing (apps/api/src/payments/__tests__/payments-schema.spec.ts); full apps/api suite 529/529 green serially (--runInBand).

²⁰ ADMIN SCHEMA LOCKED 2026-07-07 (module #20) — the FIRST v2 pass of this module (v1 was 11 tables/145 cols, locked 2026-06-10, never rebuilt for v2 until now). 10 tables, 128 cols (down from v1's 145 — tenant_business_profile, 17 cols, DROPPED entirely: 16 of its 17 columns MOVED to platform.tenant_profile in a 4th Platform reopen, the 1 remaining column, attributes, dropped outright with no successor — zero known consumers, no defined shape; full 17-column fate mapping lives in PROJECT_DECISIONS #34, not re-derived here). The 10 surviving tables are a FAITHFUL PORT of v1 — same columns, types, CHECK constraints, indexes — with exactly ONE schema-level change, the module-wide actor-attribution retarget already applied to every module built since 2026-06-28: tenant_branding (12), compliance_document (14), tenant_setting (11), hardware_device (13), integration_config (13), webhook_config (12), api_key (14), approval_workflow (10), approval_routing_rule (11), approval_request (18) — sums to 128, matching 145 minus 17 exactly. 5 columns across 3 tables retargeted from identity.identity_user to identity.actor: tenant_setting.updated_by_actor_id, api_key.created_by_actor_id, api_key.revoked_by_actor_id, approval_request.requested_by_actor_id, approval_request.resolved_by_actor_id. Admin does NOT own tenant identity — platform.tenant_profile is the single source of truth (PROJECT_DECISIONS #34); every Admin table carries tenant_id → platform.tenant.id and duplicates no identity facts. tenant_branding now owns the logo (logo_ref, a Files-module forward-ref) — platform.tenant_profile.logo_url was deprecated in its favor (PROJECT_DECISIONS #35). The approval engine (approval_workflow/approval_routing_rule/approval_request) is tenant-scoped, tenant-RLS throughout, and stays Admin-internal per DECIDED Option B (PROJECT_DECISIONS #34 Section 5) — it does not converge with platform.contract's separate Vrida-operator review gate (different plane, untouched), but is confirmed purely forward-compatible as the shared mechanism any future tenant-side approval need can route through, with zero changes required to any already-locked module. [SUPERSEDED 2026-07-09] This Option-B decision was reversed 2 days later: all 3 approval-engine tables moved OUT of Admin into a new dedicated approvals schema, reducing Admin to 10 tables/122 cols and 60/60 admin-specific tests (the approval-engine coverage moved to approvals's own 22-test suite) — see footnote ²¹ and PROJECT_DECISIONS #44. A config-surface decision tree (7 branches, PROJECT_DECISIONS #34 Section 6) governs where new tenant-config-shaped settings go: tenant_setting is the branch-7 default/fallback, integration_config.settings is branch 5, hardware_device.config is branch 6. Dependency-blocked register (Block 4, already established, not new discoveries): tenant_branding.logo_ref/compliance_document.document_ref remain unchanged forward-refs to the not-yet-built Files module; integration_config.credentials_ref/webhook_config.secret_ref are vault references depending on the same not-yet-built vault-encryption service platform.tenant_profile.ein_ref already depends on (confirmed via grep — zero Vault/Encryption service classes anywhere in apps/api/src); integration_config/webhook_config are the real, already-documented FK targets for the not-yet-built Integrations module. Part D capability discovery ran during the design phase: Admin is honestly a LOW-AUTONOMY, mostly-human-configuration module — only approval_request reaches FULL (mechanical routing + anomaly/SLA flagging; the approve/reject decision itself stays definitionally human), narrow LIGHT flagging-only surfaces exist on hardware_device/integration_config/webhook_config/api_key (detection only, never autonomous write access to credentials/endpoints/issuance), and tenant_branding/approval_workflow/approval_routing_rule are explicitly human-only (branding is a subjective creative decision; workflow/routing-rule definitions are governance decisions over approval authority itself, not delegable to the system they exist to check) — no automation_source/review_status/decision_provenance columns were added to any of the 10 tables, a deliberate disclosed decision. Migration packages/db/migrations/20260707120000_admin_module.sql applied live (schema admin, 10 tables / 128 cols verified via information_schema, RLS enabled + tenant-isolation policy on all 10, all 5 retargeted actor FKs confirmed resolving to identity.actor). See PROJECT_DECISIONS #34/#36, schema_docs/admin.md. No AdminService exists yet (schema-only). Tests: 46/46 admin-specific tests passing (apps/api/src/admin/__tests__/admin-schema.spec.ts) covering table/RLS/column-count existence, RLS policy correctness, platform.tenant_id FK resolution, the 5 retargeted actor columns, the full approval-engine chain, api_key hash-only storage, NULL-distinctness across every multi-column unique in the module, and CHECK-constraint spot-checks; full apps/api suite 585/585 passing (up from 539 pre-Admin-build), zero regressions.

²¹ APPROVALS SCHEMA LOCKED 2026-07-09 (module #21) — NEW module, consolidating the tenant-side approval engine OUT of Admin, reversing PROJECT_DECISIONS #34 Section 5's Option B (see footnote ²⁰ above). Preceded by a read-only, Ultracode-scale sweep of every approval/review/sign-off/authorization-gate/workflow mechanism across all 15 then-locked modules (81 mechanisms found: 17 real business-process approvals, 2 operator-only, 41 autonomy-review seams deliberately left distributed/untouched, 21 other), then a design proposal for a dedicated module, then TWO independent adversarial verification passes (a 3-lens pass, superseded by a more precise 7-lens pass covering domain-agnosticism, the C8 agent-money boundary, self-approval defeatability — explicitly including the null-approver path prior code reviews had found as a hole — token security, polymorphic integrity/outbox degradation, autonomy-seam exclusion, and admin-reopen scoping) before any schema was touched. 8 tables, 97 cols: approval_workflow/approval_routing_rule/approval_request MOVED from admin via ALTER TABLE ... SET SCHEMA (RLS/triggers/grants carried over automatically, confirmed live) — approval_workflow extended 10→12 cols (+step_mode, +blocks_agent_approver — the C8 boundary anchor), approval_request extended 18→20 cols (requested_by_actor_id renamed to initiator_actor_id and made NOT NULL, closing a null-approver bypass; step_history dropped, superseded by the new approval_step); both tables' workflow_type CHECK-enum removed (was a domain-knowledge leak baked into the engine). Plus 5 net-new tables: approval_policy, approval_step, approval_delivery, approval_token (hash-only one-click approve/reject, atomic single-use redemption), approval_event (append-only audit log, reuses platform.reject_append_only_mutation()). 6 critical guards, every one LIVE-REPRODUCED with a real before/after proof, not asserted: approved-by-nobody, self-approval on every path (header, non-final parallel step, and the null-approver path), parallel-step quorum-spoofing, the C8 agent-money boundary, token replay/expiry/sibling-supersession, and append-only audit enforcement (UPDATE/DELETE rejected even as superuser). Guard 4's own reproduction found a 3rd, previously-unknown bug live: a column-level REVOKE alone does not subtract from an existing broader table-level GRANT (Postgres ACLs are additive across granularities) — fixed by revoking the table-level grant and re-granting column-by-column. A full NULL-in-CHECK sweep across all 13 CHECK constraints found zero bypass surface. A post-build independent adversarial lock-gate verification pass then found one further real bug the build's own reproductions had missed: a cross-tenant FK bypass on all 4 new child tables — Postgres FK checks bypass RLS while the new guard triggers are SECURITY INVOKER, so a crafted cross-tenant reference silently defeated both the self-approval and C8-boundary guards (live-reproduced 3 ways by the verifier) — closed via composite FKs (child_col, tenant_id) → parent(id, tenant_id), a deliberately stronger fix than this codebase's existing trigger-based cross-tenant precedent (tax.trg_tax_calculation_validate_supersession), re-verified live as closed. Zero domain-module dependencies — pure infrastructure, consumed read-only by identity (actor, agent_duty_grant), platform (tenant, outbox), multi_loc (site); 14 domain-module convergence points logged to OPEN_ITEMS, none decided to migrate onto this engine yet. Migration packages/db/migrations/20260709070000_approvals_module.sql (+ a same-day follow-up 20260709080000_approvals_fix_cross_tenant_fk.sql for the composite-FK fix) applied live (schema approvals, 8 tables / 97 cols verified via information_schema, all 8 RLS-enabled). See PROJECT_DECISIONS #44, schema_docs/approvals.md, module_spec/approvals.md. No ApprovalsService exists yet (schema-only). Tests: 22/22 approvals-specific tests passing (apps/api/src/approvals/__tests__/approvals-schema.spec.ts); Admin's own suite was updated in the same pass to drop the 8 tests specific to the relocated approval-engine tables (68 → 60 — the pre-move 68 already reflected Remediation Phase 3/4 test growth beyond the original 46/46 at Admin's 2026-07-07 lock, not itemized further here) — coverage moved to approvals's own suite rather than shrinking; full apps/api suite green, zero regressions. 2026-07-11 companion reopen (see footnote ²³): approval_request.source_module CHECK widened to accept 'returns' — constraint-shape only, table/column counts unchanged, see PROJECT_DECISIONS #61.

²² REWARDS + OFFERS SCHEMA LOCKED 2026-07-11 (PROJECT_DECISIONS #56/#57), REOPENED SAME DAY (PROJECT_DECISIONS #60) for a proportional-reversal fix. This row was never updated at the original lock (a missed-doc-file gap, same class as footnote ²⁰'s own Admin/Approvals precedent) — closed here rather than left stale. rewards: 7 tables/109 cols (up from the original 6/102). offers: 7 tables/123 cols (up from the original 6/115). The same-day reopen fixed rewards.sync_loyalty_account_balance()'s exact-negation-only reversal limitation and a real, independent bug in offers.check_and_sync_offer_budget() (reversal magnitude was completely unvalidated — live-reproduced: a $10 redemption "reversed" by a fabricated $1,000 row succeeded pre-fix). Both now support proportional/partial reversal, cumulative-capped per original entry via a new tracker table each (loyalty_point_ledger_reversal_tracker, offer_redemption_reversal_tracker), live-reproduced sequentially and under genuine concurrent load, then independently re-verified by 3 separate adversarial lenses (all PASS, zero functional defects). Neither module has a service layer yet (RewardsService/OffersService — schema-only). Tests: rewards-schema.spec.ts 28/28, offers-schema.spec.ts 39/39; full apps/api suite 1052/1052.

²³ RETURNS SCHEMA LOCKED 2026-07-11 (module #27) — the customer RMA module, PASS 2 of a 2-pass same-day session (Pass 1: the rewards/offers proportional-reversal fix, PROJECT_DECISIONS #60 — see footnote ²² above). 9 tables, 152 cols: return_authorization (29, FULL), return_authorization_line (28 — freezes discount/tax allocation at RA-creation time, the module's central design decision), return_source_line_tracker (10, NEW — an aggregate-cap cache mirroring offers.offer_redemption_reversal_tracker's own shape, since pos.sale_line/orders.order_line are append-only and off-limits for a counter column; caps cumulative over-return across multiple separate RMAs against the same source line), return_resolution (21), return_resolution_line (8, append-only — write-once financial fact mirroring billing.ar_charge_line/purchasing.vendor_credit_line), return_receipt (14), return_receipt_line (16 — tolerance policy is BLOCK, not flag, a deliberate deviation from receiving.goods_receipt_line's own flag default, since a customer returning goods twice is more dangerous to leave un-gated), return_reason (10), warranty (16, revived v1 pos.guarantee, retargeted so a plant-guarantee claim resolves as a REPLACEMENT not a refund). Proportional loyalty/offer clawback is a REAL, WORKING capability from day one, not deferred — directly crediting Pass 1's (PROJECT_DECISIONS #60) same-day fix to rewards/offers' own reversal triggers, which return_resolution.loyalty_reversal_ledger_id/.offer_reversal_redemption_id call into rather than re-implementing any loyalty/offer math itself; live-reproduced (5 shrubs, 100 points + $10 offer, return 2 of 5 → 40 points + $4.00 clawed back, additive on a 2nd partial return, an over-clawback attempt correctly rejected). 5 small companion reopens bundled into the same migration, all constraint/CHECK-shape only — zero table/column count impact on any of the 5: pos.sale_refund/sale_refund_line and orders.order_header/order_line each gained UNIQUE(id, tenant_id); inventory.stock_movement.source_module, approvals.approval_request.source_module, and files.attachment.entity_type CHECKs were each widened to accept a returns-related value ('returns', 'returns', 'return_authorization' respectively). 3 new trigger functions, all BEFORE INSERT (check_and_reserve_source_line, validate_and_apply_resolution_line, post_and_cap_return_receipt_line), each reusing this codebase's own proven atomic-cap/row-lock patterns — a real NULL-in-ON CONFLICT-predicate bug (missing the WHERE clause needed to match a partial unique index against return_source_line_tracker's 2 mutually-exclusive nullable columns) was caught and fixed during this build's own live-reproduction. Migration packages/db/migrations/20260711040000_returns_module.sql applied live (schema returns, 9 tables / 152 cols verified via information_schema.columns, zero drift against the Drizzle schema files). See PROJECT_DECISIONS #61. No ReturnsService exists yet (schema-only this pass, matching every other schema-only module's own established pattern). Tests: 31/31 returns-specific tests passing (apps/api/src/returns/__tests__/returns-schema.spec.ts); full apps/api suite 1083/1083 passing, confirmed serially (--runInBand) — one pre-existing, already-disclosed, unrelated cross-file concurrency flake (admin-tenants.spec.ts) intermittently reproduces under parallel Jest workers, not caused by this build.

²⁴ PLATFORM REOPENED A 6TH TIME 2026-07-12 (Phase 1 of 6 — agents-v2/v3 build). Build authorization for the full agents/semantics/signals build (design of record: vrida-agents-v2-design-amendment-2026-07-11.md as amended by vrida-agents-v3-correction-pass-2026-07-12.md) is now underway, sequenced in 6 phases with an independent lock-gate verification required between each before the next may begin. This is Phase 1: +7 tables, 0 existing tables alteredmodule_catalog (A1, the global module registry replacing the closed CHECK-enum module-tag pattern, forward-only lifecycle_status via a new trigger), tier_module_entitlement (which modules a tier includes by default, replacing the dead tier_definition.entitled_modules JSONB), tenant_module_activation (the REAL per-tenant module switch, replacing is_toggleable — a mechanism found to have zero callers anywhere in the codebase), module_dependency (module prerequisite graph with real recursive-CTE cycle detection plus a reverse-dependency-on-deactivation guard), ai_capacity_policy/tenant_ai_capacity_usage (A8/E4 — the 6-level AI-capacity precedence chain's atomic enforcement half, platform.try_increment_ai_capacity_spend(), embedding the resolved ceiling as a correlated subquery inside one UPDATE...RETURNING), tenant_regional_policy (E7 — one resolvable regional-placement policy per tenant, for later phases' routing/storage/retrieval/archival decisions to evaluate against). 4 guards live-reproduced (lifecycle transition, recursive cycle rejection, reverse-dependency-on-deactivation, atomic capacity race). platform is now 34 tables / 514 cols (up from 27/450). An independent lock-gate verification (separate agent) found and fixed 4 issues before Phase 2 was authorized to proceed: (1) BLOCKER — a pre-existing schema-wide default-privilege GRANT let authenticated freely mutate 4 global reference tables including the platform-wide AI cost ceiling, closed via explicit REVOKE; (2) MAJOR — the spend-increment function's ceiling check wrongly gated refunds/negative deltas, now bypassed unconditionally for delta <= 0; (3) MAJOR — ai_capacity_policy.priority was a vestigial tie-break, now honored via ORDER BY priority DESC, max_cost_cents ASC; (4) MAJOR — the seeded multi_loc row was misclassified layer='foundation', corrected to 'business' per SCHEMA_CONVENTIONS.md §1. A formal Section 4 self-audit (A-P+T+U) found 3 more GAPs (2 missing RLS-status doc comments, 1 missing supporting index, 1 trigger scoping tightening), all fixed same-pass. Migration packages/db/migrations/20260712000000_platform_module_registry_capacity.sql. See PROJECT_DECISIONS #62. Schema-only this phase — no service-layer changes (every phase of this build stays schema-only until Phase 5 lands the agents module itself). Tests: apps/api/src/platform/__tests__/platform-module-registry.spec.ts, 23/23 passing; full apps/api suite 1111/1111 (serially). Phase 1 lock-gate verdict: CLEAN. Phase 2 (ai reopen) is authorized to proceed.

²⁵ ai REOPENED FOR THE 1ST TIME 2026-07-13 (Phase 2 of 6 — agents-v2/v3 build). Continuing the same 6-phase agents-v2/v3 build authorization Phase 1 opened (footnote ²⁴). This is Phase 2: +13 new tables (PROJECT_DECISIONS #63's own count; an independent cross-check against the Drizzle schema files/migration CREATE TABLE statements — see docs/database/schema_docs/ai.md's own disclosure note — finds 14 distinct new table definitions, a pre-existing off-by-one in that entry's own summary sentence, disclosed here rather than silently perpetuated or corrected out of scope) implementing the C1 model registry/deployment/prompt/routing layer — provider_registry/model_family/model_version (the 3-level provider/model registry), model_deployment + its _limit/_region/_policy/_override satellites + model_deployment_status_observation (3 genuinely separate state classes: durable config, administrative override, transient runtime observation — live-confirmed distinct), prompt_definition/_version/prompt_model_compatibility (the same A2b definition/version split applied to prompts), routing_policy (schema-only — no resolution function built yet) — plus C4's agent_memory_source (a join table, not a single polymorphic column, since one memory may derive from multiple sources). ai.agent_execution and ai.agent_memory — v1 tables, reopened for the first time — are now PARTITIONED BY RANGE(created_at), monthly, the first partitioned tables in this codebase's history. 2 genuine gaps neither the v2 nor v3 design addressed, resolved consistently with Phase 1's own established pattern for structurally-impossible native constraints: agent_execution's at-most-one-resolver + idempotency-dedup partial-unique indexes, and agent_memory's at-most-one-active-per-key partial-unique index, all 3 cannot survive partitioning as native indexes (Postgres requires a partitioned table's unique index to include the partition key, which would silently narrow each global invariant to per-partition-month scope) — all 3 are now enforced by new BEFORE INSERT (or BEFORE INSERT OR UPDATE OF status) advisory-lock triggers instead (ai.check_agent_execution_single_resolver(), ai.check_agent_execution_idempotency(), ai.check_agent_memory_single_active()), plus a composite self-FK (resolves_execution_id/resolves_execution_created_at) replacing agent_execution's old plain self-FK. A mandatory pre-migration audit (required since these are v1 tables reopened for the first time) found 948 live agent_execution rows (0 in agent_memory) and one genuine pre-existing data-integrity violation — a dev-seed row's permission_id referenced a deleted identity.permission row despite a live FK — nulled out and disclosed, not silently dropped; re-verified zero orphans before re-running the migration. model_deployment_override.emergency_traffic_weight is trigger-guarded (ai.check_model_deployment_override_residency()) to only ever raise traffic toward a tenant-restricted deployment whose region already satisfies that tenant's platform.tenant_regional_policy-resolved home region. routing_policy.workload_class_id is a disclosed forward-ref (bare uuid, no FK) to agents.workload_class (Phase 5, not yet built) — logged to OPEN_ITEMS and CROSS_MODULE_CONTRACTS.md. 6 guards + the residency guard all live-reproduced via real psql sessions. ai is now 49 tables / 227 cols (up from 7/121) — 227 counts each distinct table definition once; the 49 physical-table count (live-verified via information_schema.tables) additionally includes 28 partition-child tables (13 monthly + 1 DEFAULT catch-all, × 2 partitioned tables) that inherit their parent's columns rather than adding new ones. A Section 4 self-audit found 3 GAPs, all fixed same-pass (3 JSONB columns missing documented example shapes; a Drizzle comment falsely claiming a resolution function existed for routing_policy; the workload_class_id forward-ref documented in code but not yet logged to OPEN_ITEMS). A disclosed mistake during this phase's own live-reproduction pass (an overly broad DELETE ... WHERE action_code LIKE 'test.%' cleanup query also removed 270 pre-existing dev-seed rows sharing that generic prefix — confirmed harmless, the substantive 678-row dataset fully intact) is logged in PROJECT_DECISIONS #63, not repeated in the regression suite written afterward. Migration packages/db/migrations/20260713000000_ai_registry_partition_memory_governance.sql. See PROJECT_DECISIONS #63. Schema-only this phase — no service-layer changes (every phase of this build stays schema-only until Phase 5 lands the agents module itself). Tests: apps/api/src/platform/__tests__/ai-registry-partition-memory.spec.ts, new file, 18/18 passing; the pre-existing apps/api/src/ai/__tests__/ai-schema.spec.ts (23 tests) required 5 updates for the new partition-aware shape (table count, 2 INSERT-statement updates, 3 assertions moved from index-23505 to trigger-message shape) — all disclosed as expected updates, not defects. Full apps/api suite: 1129/1129, typecheck clean on packages/db and apps/api.

²⁶ semantics SCHEMA LOCKED 2026-07-14 (Phase 3 of 6 — agents-v2/v3 build). Continuing the same 6-phase agents-v2/v3 build authorization Phase 1 (footnote ²⁴) and Phase 2 (footnote ²⁵) opened. This is Phase 3: a brand-new foundation-layer schema with zero v1 antecedent — the shared business ontology the not-yet-built agents (Phase 5) and signals (Phase 4) modules will read from rather than each inventing their own metric/goal/constraint vocabulary. 14 tables, 114 cols: approved_function_registry (11 — I2's supply-chain-hole-closing allowlist; a metric/attribution-model version's computation is a POINTER to a reviewed function via verify_function_still_matches_approval(), re-resolved and re-hashed fresh on every call, never a cached OID or a one-time approval), metric_definition/metric_version (6/8, standard A2b definition/version pattern), tenant_metric_binding (12, RLS-enabled, which version is authoritative for a tenant/date-range), metric_dependency (4, real cycle-detection via a recursive-CTE trigger — closing v3's own disclosed "dead code" finding that an earlier draft of this exact logic had no CREATE TRIGGER statement anywhere), entity_definition/entity_alias (6/5), dimension_definition (5), goal_definition/constraint_definition (8/7) plus tenant_goal_binding/tenant_constraint_binding (15/15, RLS-enabled), attribution_model_definition/attribution_model_version (4/8). EXCLUDE USING gist hard-reject overlap prevention on all 3 tenant-scoped binding tables (reusing platform.accounting_period's own btree_gist precedent, but HARD-REJECT not flag-not-reject — no offline-sync reason applies to a synchronous admin action, unlike POS's offline-first sale posting). tenant_goal_binding.module_id/.site_id and tenant_constraint_binding.module_id/.site_id are deliberately BARE, no FK — a disclosed judgment call, not an oversight (platform.module_catalog already exists but the design of record doesn't specify wiring module_id to it) — logged to OPEN_ITEMS. Section 4 self-audit found and fixed 2 real gaps before lock: 3 lifecycle_status columns missing their CHECK entirely (fixed, matching metric_definition's own correctly-built CHECK); tenant_goal_binding/tenant_constraint_binding missing created_at/updated_at + the maintaining trigger their sibling tenant_metric_binding already had (fixed — column count grew 110→114 as a direct result). A schema-usage GRANT gap was found only by the regression suite, not the read-only Section 4 pass: the migration's original draft never ran GRANT USAGE ON SCHEMA semantics TO authenticated — every prior new-schema migration in this codebase does this explicitly; fixed live and in the migration file. 3 named guards live-reproduced via real psql sessions: EXCLUDE overlap rejection (all 3 tables), the I2 supply-chain-hole guard (register → verify true → redefine → verify false → new binding write rejected), and metric_dependency cycle detection (A→B, B→C succeed, C→A rejected by the trigger, a direct self-reference separately rejected by a CHECK). Migration packages/db/migrations/20260714000000_semantics_new_schema.sql. See PROJECT_DECISIONS #64. Schema-only this phase — no service-layer changes (every phase of this build stays schema-only until Phase 5 lands the agents module itself). Tests: apps/api/src/platform/__tests__/semantics-schema.spec.ts, new file, 19/19 passing; a companion stale-assertion fix landed in the pre-existing platform-module-registry.spec.ts (G3, updated to assert semantics is now 'in_build' in module_catalog while agents/signals stay 'designed'). Full apps/api suite: 1150/1150, typecheck clean on packages/db and apps/api. Phase 3 lock-gate verdict: CLEAN. Phase 4 (signals, HIGHEST RISK) is next.

²⁷ signals SCHEMA LOCKED 2026-07-15 (Phase 4 of 6 — agents-v2/v3 build, HIGHEST RISK). Continuing the same 6-phase agents-v2/v3 build authorization Phase 1 (footnote ²⁴), Phase 2 (footnote ²⁵), and Phase 3 (footnote ²⁶) opened. This is Phase 4 — this build's own explicitly-designated highest-risk phase, combining 3 mechanisms no prior phase combined in one schema: table partitioning, a split-authority pattern for an invariant a native partitioned index cannot express, and SECURITY DEFINER tenant-isolation functions. A brand-new foundation-layer schema with zero v1 antecedent — the feature/forecast/outcome store the not-yet-built agents module (Phase 5) will read from and write to; opposite semantics's own operational profile (observational, high-volume, bitemporal, append-only, partitioned rather than definitional/low-volume/human-curated). 11 tables, 111 cols: feature_definition/feature_version (6/7, standard A2b catalog pattern), experiment/experiment_version (6/9, same pattern plus B4's own allows_reentry/washout_days), feature_value/forecast/anomaly_score (10/11/10, PARTITIONED weekly by recorded_at — 12 weekly partitions + 1 DEFAULT = 13 each — REVOKE ALL from authenticated, readable only through 3 SECURITY DEFINER as-of functions), experiment_assignment/experiment_exposure_event (19/5, RLS-enabled, NOT partitioned), outcome_observation/outcome_authority (19/9, outcome_observation PARTITIONED monthly by created_at — 13 monthly partitions + 1 DEFAULT = 14 — outcome_authority deliberately unpartitioned, the split-authority pattern). 4 named critical guards, each corresponding to a v3 BLOCKER, all live-reproduced: GUARD 1 (BLOCKER 1) — a p_tenant_id parameter on a SECURITY DEFINER function is spoofable by construction (no validation closes it), so the 3 as-of functions (get_feature_as_of/get_forecast_as_of/get_anomaly_score_as_of) derive tenant scope from a new platform.current_tenant_id() session-GUC helper instead, with no tenant argument in the signature at all; each is owned by a new, minimal signals_function_owner role (NOLOGIN, not superuser, not the table owner) with its own narrow SELECT grant on exactly the 3 tables it reads, search_path pinned, REVOKE EXECUTE FROM PUBLIC. GUARD 2 (BLOCKER 2) — a native unique/partial-unique index enforcing "exactly one authoritative observation per scope" cannot survive PARTITION BY RANGE(created_at) (Postgres requires the partition key in the constraint, which would silently narrow the guarantee to per-partition-month scope); resolved via a genuinely separate, unpartitioned outcome_authority table whose own PK (the scope tuple) is the real enforcement, maintained by promote_authoritative_observation() — a native, race-free INSERT ... ON CONFLICT ... DO UPDATE upsert, no advisory lock needed. GUARD 3 (BLOCKER 3) — assigned_at forgery is closed by force_assignment_timestamp(), an unconditional BEFORE INSERT trigger overwriting any caller-supplied value with clock_timestamp() (not now(), which is transaction-start-frozen and forgeable by holding a transaction open); a second, more subtle live-reproduced race — a genuinely-earlier exposure event arriving AFTER the causal basis is already locked — is closed via matching SELECT ... FOR UPDATE row-locks in lock_experiment_causal_basis() (function built this phase; its own CREATE TRIGGER ... ON agents.decision_context_snapshot is deferred to Phase 5, which builds that table) and update_first_eligible_exposure(), which row-lock the assignment FIRST — the actual fix, not the contamination-flagging branch logic alone — flipping contamination_status to 'exposure_revised_post_lock' and outcome_authority.remeasurement_required to true rather than silently moving first_eligible_exposure_at. GUARD 4 (B3 mandatory test 4a) — enforced bitemporal read: a feature_value row with as_of in the business past but recorded_at after a decision's own knowledge_cutoff is structurally invisible to that decision via get_feature_as_of(), live-reproduced exactly as specified. A genuine gap in the design doc's own literal SQL, found and fixed during THIS phase's own migration-apply pass, not a later audit: the design specified anomaly_score's uniqueness as (id, tenant_id) and outcome_observation's second unique constraint without created_at — both fail live against Postgres's own structural requirement (the identical error class BLOCKER 2 itself documents, just not caught by that section's own adversarial pass for these 2 sibling constraints). Fixed: anomaly_score now carries UNIQUE(id, tenant_id, recorded_at); outcome_observation's scope-version constraint now includes created_at, backstopped by a genuine cross-partition BEFORE INSERT trigger (check_outcome_observation_no_duplicate_version(), using pg_advisory_xact_lock + a real cross-partition SELECT) mirroring Phase 2's own established resolution for the identical bug class (ai.agent_execution.idempotency_key). 2 privilege-mechanics gaps found live during the migration-apply pass itself, neither anticipated by the v2 or v3 design: ALTER FUNCTION ... OWNER TO signals_function_owner needed both role-membership and schema-CREATE grants the migration-running role didn't have by default (this local Supabase stack's own postgres role is confirmed NOT a true superuser, rolsuper = false) — CREATE is REVOKEd again immediately after the 3 ownership transfers; and the SECURITY DEFINER function body's own call to platform.current_tenant_id() needed signals_function_owner to hold USAGE on schema platform, a genuinely separate requirement from EXECUTE on the function itself (PUBLIC-granted by default) — found only when live-reproducing GUARD 1 for the first time. Section 4 self-audit found and fixed 1 real gap before lock: 4 JSONB lineage/stratification_attributes columns had no documented example shape (item J) — fixed, inline example comments added to all 4 in the Drizzle schema files; all other A-P+T+U items PASS or N/A (Item U — Design-Phase Integrity — is N/A, brand-new schema with no v1 predecessor). 2 disclosed forward-refs, both logged to docs/open-items/OPEN_ITEMS.md (signals \| FK): outcome_observation/outcome_authority.agent_action_id are bare uuid — agents.agent_action doesn't exist until Phase 5 (confirmed live: zero tables in the agents schema as of this phase); and the 3 as-of functions have REVOKE EXECUTE FROM PUBLIC but no GRANT EXECUTE to any role — agent_reader doesn't exist until Phase 6, matching this design's own explicit allowance to "stub the REVOKE ahead of the role's full build" (all 4 critical-guard live-reproductions worked around this by temporarily granting EXECUTE to authenticated and revoking it again afterward, both live and in the regression suite's own setup/teardown). subject_type/subject_ref on experiment_assignment resolve a genuine internal inconsistency in the design doc itself (its own column-list prose said assignment_unit, its own "Uniqueness" line said subject_type, subject_ref) — a disclosed judgment call, treated as the same concept, named per the more specific, typed, structurally-consistent form matching this codebase's established polymorphic-pointer convention. Migration packages/db/migrations/20260715000000_signals_new_schema.sql. See PROJECT_DECISIONS #65. Schema-only this phase — no service-layer changes (every phase of this build stays schema-only until Phase 5 lands the agents module itself). Tests: apps/api/src/platform/__tests__/signals-schema.spec.ts, new file, 20/20 passing; a companion stale-assertion fix landed in the pre-existing platform-module-registry.spec.ts (G3, updated to assert signals now correctly bumps to 'in_build' alongside semantics in module_catalog, while agents alone stays 'designed'). Full apps/api suite: 1172/1172, typecheck clean on packages/db and apps/api. Phase 4 lock-gate verdict: CLEAN. Phase 5 (agents, module #28, ~30 tables) is next.

²⁸ agents SCHEMA LOCKED 2026-07-16 (module #28) — Phase 5 of 6 of the agents-v2/v3 build authorization, the module the entire authorization exists to build (Phase 1 reopened platform, PROJECT_DECISIONS #62; Phase 2 reopened ai, PROJECT_DECISIONS #63; Phase 3 built semantics, PROJECT_DECISIONS #64; Phase 4 built signals, PROJECT_DECISIONS #65; this is Phase 5). 47 tables, 475 cols — the full v1 agent-orchestration baseline (21 v1 tables: agent_task, agent_thread, agent_event_log, agent_schedule, agent_trigger, agent_eval_suite, agent_eval_run, agent_eval_case, agent_performance_profile, agent_shadow_run, agent_shadow_decision, agent_autonomy_profile, agent_action, rollback_recipe, rollback_execution, tool_catalog, agent_tool_grant, agent_catalog_entry, agent_catalog_entry_required_tool, tenant_agent_deployment, agent_incident) merged with v2's own A1–A8 amendments and v3's own BLOCKER1–8/I1–I8 corrections (v3 wins on conflict), organized across 12 Drizzle files (_schema.ts, tool.ts, skill.ts, marketplace.ts, task.ts, eval.ts, shadow.ts, certification.ts, decision_context.ts, action.ts, kill_switch.ts, policy.ts). tool_catalog (v1) is disclosed as SUPERSEDED by the new tool_definition/tool_version version-row pattern (v2's own A2b definition/version split, zero live v1 rows) rather than carried forward. agent_decision, decision_context_manifest, and decision_context_snapshot are PARTITIONED (monthly, by their own respective date columns) — live-confirmed via pg_inherits. 8 distinct trigger-backed critical guards, all live-reproduced (this build's own design named 7; a 6th guard, T1, was added and proven during the Section 4 audit pass itself — disclosed honestly as an 8th rather than force-fit into the original count): E3 (atomic task claim + fencing — lease renewal never bumps the fencing token), BLOCKER7 (kill-switch history/state split + resume-safe propagation, plus a Section-4-audit-found RLS gap — see below), BLOCKER4+I1 (the saga gate enforced against real writes, not a static toolset-shape proxy, plus agent_action/agent_decision consistency), the runaway-ceiling guard (BLOCK not flag, closing v1's own Gap 1), A5 corrected (the kill-switch check fires on UPDATE OF status — i.e. task CLAIM — not just INSERT), A3+BLOCKER5+A8R4 (skill activation: certification + duty-grant authority + a spend-ceiling policy, folded into one function, validate_skill_activation()), and the A2b retirement-version-check trigger (reject_retired_decision_context_versions). 2 genuine PL/pgSQL bugs found and fixed during this build's own live guard-reproduction (not the read-only Section 4 audit): (a) a record-typed PL/pgSQL variable's IS NOT NULL check is UNRELIABLE used directly as an IF condition — empirically confirmed via a bare DO block (a genuinely non-null, fully-populated record still evaluated as if NULL) — silently swallowed the entire A8 Rule 4 spend-ceiling check inside validate_skill_activation(), fixed by rewriting to scalar variables checked individually; (b) SELECT MIN(...) ... FOR UPDATE is syntactically REJECTED by Postgres ("FOR UPDATE is not allowed with aggregate functions"), fixed by splitting into a lock-only PERFORM ... FOR UPDATE followed by a separate SELECT MIN(...) INTO ... over the now-locked rows. 1 genuine security gap found and fixed during the SECTION 4 SELF-AUDIT (not live guard testing): agents.kill_switch_event carried tenant_id + GRANT SELECT, INSERT TO authenticated but ZERO RLS policies, letting any tenant session read every other tenant's kill-switch history and forge a kill/suspend/resume event against another tenant's agents (or a platform-wide 'global' kill, though WITH CHECK correctly blocked that path for ordinary tenant sessions) — fixed with the same 2-policy mixed-scope RLS pattern already established by agent_eval_suite/rollback_recipe in this same build, live-reproduced (cross-tenant SELECT returns 0 rows, cross-tenant/global forged INSERT rejected, legitimate own-tenant INSERT succeeds) and covered by a new regression test (I5). The same audit found and fixed item J (15 JSONB columns across task.ts/eval.ts/shadow.ts/action.ts/policy.ts/decision_context.ts/skill.ts/tool.ts lacked documented example shapes) and disclosed, not fixed, item K (12 tables lack a tenant_id-leading index — logged to OPEN_ITEMS, low severity since RLS correctness is unaffected and each table's natural query path is via an FK-indexed parent); all other Section 4 items (A, C–I except J, L–P) PASS. 3 pre-existing forward-ref OPEN_ITEMS rows CLOSE with this build: ai.routing_policy.workload_class_id → agents.workload_class(id) (plain FK, workload_class is global/no tenant_id) and signals.outcome_observation/outcome_authority.agent_action_id → agents.agent_action(id, tenant_id) (composite FKs) — pre-migration orphan audits found 0 violating rows for all 3; Phase 4's own pre-existing signals-schema.spec.ts used 5 hardcoded placeholder agent_action_id UUIDs that had to be retrofitted with a real minimal fixture chain (identity.actor → agent_identity → decision_context_snapshot → ai.agent_execution → agents.agent_decision → agents.agent_action) to keep passing. ai.agent_execution gained 8 columns this same migration (agent_task_id, sequence_index, tool_version_id, approval_request_id, retry_count, blocked_by_policy, confidence_threshold, escalation_reason), ai.routing_policy.workload_class_id is now wired, and approvals.approval_request's chk_approval_request_source_module CHECK was widened to add 'agents' (11 values total) — all disclosed as companion touches inside this one migration, not separate reopens. platform also gained 1 table this same migration (polymorphic_target_registry, I6, 4 cols) — platform is now 35 tables / 518 cols (up from 34/514). Disclosed design judgment calls (in code comments in the Drizzle files): A1's domain→module_id retype scoped to EXACTLY 3 columns (agent_task.domain, tool_definition.domain, skill_definition.domain) per A1's own explicit scope-limiting text — NOT extended to agent_performance_profile.domain/agent_autonomy_profile.domain (both stay free-text); agent_eval_run.eval_suite_version_id retargets to agent_eval_suite_version.id (A2b version-pinning), not the identity row; agent_action has NO agent_task_id column — BLOCKER4's trigger derives it via a join through agent_execution_id instead (a genuine gap in v3's own literal SQL, fixed structurally not patched); A3's "trigger on tenant_agent_deployment" resolved instead as a trigger on agents.agent_skill_assignment (the only table with both agent_identity_id + skill_version_id), folding A3 cert-check + BLOCKER5 duty-check + A8 Rule4 policy-bound check into ONE function; A2b retirement-rule-1 checks split by actual column location (ai.agent_execution checks only its own tool_version_id; agents.decision_context_snapshot checks its own model_version_id/prompt_version_id/skill_version_id/toolset_version_id). A transitional Drizzle barrel collision — identity.agentSkillAssignment (v1, still used by production IdentityService) and agents.agentSkillAssignment (I5's new destination table) — coexists until Phase 6 drops the old one, resolved via explicit named re-exports in packages/db/src/schema/index.ts. Migration packages/db/migrations/20260716000000_agents_module_new_schema.sql (1650 lines, incl. a same-day SECTION 10 appended after the Section 4 self-audit's own 2 fixes). Consumes identity.agent_duty_grant/agent_identity for all agent authority — no new authority mechanism introduced (this module is itself the primary consumer, not a new grant surface). Schema-only — no AgentsService yet (every phase of this build stays schema-only until Phase 6, which builds the agent_reader role + connection helper + an ESLint ban on adminDb/tenantDB/set_config from agent read paths). Tests: apps/api/src/platform/__tests__/agents-schema.spec.ts, new file, 24/24 passing; apps/api/src/platform/__tests__/signals-schema.spec.ts (Phase 4's own file) retrofitted with the real agent_action fixture chain described above. Full apps/api suite: 1198/1198 (serially — a known pre-existing connection-pool-exhaustion flake in database/__tests__/rls-cross-tenant.spec.ts intermittently fails only under parallel workers, unrelated to this build). See PROJECT_DECISIONS #66. Phase 6 (identity's 6th reopen — DROPPING the legacy identity.agent_skill/agent_skill_assignment tables now fully superseded by this module's own skill_definition/skill_version/agent_skill_assignment, plus the new agent_reader role/connection helper and an ESLint ban on adminDb/tenantDB/set_config from agent read paths — see footnote ²⁹) is the final phase of this 6-phase build authorization — CLOSING IT.

²⁹ IDENTITY REOPENED A 6TH TIME 2026-07-17 (Phase 6 of 6 — agents-v2/v3 build, CLOSES THE ENTIRE BUILD). Continuing the same 6-phase agents-v2/v3 build authorization Phase 1 (footnote ²⁴) through Phase 5 (footnote ²⁸) opened. This is Phase 6, the final phase: identity.agent_skill (9 cols) and identity.agent_skill_assignment (10 cols) — the v1 skill-catalog tables — were DROPPED, now fully superseded by agents.skill_definition/skill_version/agent_skill_assignment (built Phase 5, footnote ²⁸). A mandatory pre-migration audit found agent_skill_assignment had 0 live rows and agent_skill had 13 live rows, all test debris (test.skill.%/ti3.skill.% patterns, no real data) — cleaned up in the migration, not preserved. IdentityService's skill methods (listSkillCatalog, assignSkillToAgent, removeSkillFromAgent, listAgentSkillAssignments, and the skill-check half of authorizeAgentAction) were rewritten in apps/api/src/identity/identity.service.ts to join through agents.skill_version/skill_definition instead — assignment is now at SKILL VERSION granularity (agents.agent_skill_assignment.skill_version_id), not skill-identity granularity; callers now pass skillVersionId (renamed from skillId), listAgentSkillAssignments's return field is now skillVersionId, and listSkillCatalog's return shape changed to {id, code, name, lifecycleStatus} (dropped category/moduleCode — no equivalent on the flatter agents.skill_definition catalog). Zero controllers/DTOs referenced any of these 4 methods (grep-confirmed) — no live HTTP consumer affected. This resolves the transitional Drizzle-barrel collision footnote ²⁸ disclosed (identity.agentSkillAssignment vs agents.agentSkillAssignment coexisting) — the old export is gone. identity is now 36 tables / 410 cols (down from 38/429). Also this phase: a NEW agent_reader Postgres role (NOLOGIN NOINHERIT, mirrors consumer_authenticated's shape exactly), granted to authenticator — scoped to (a) files.document_chunk/document_index (GRANT SELECT + 2 new dedicated RLS policies, document_chunk_agent_reader_select/document_index_agent_reader_select, the same tenant_id = current_setting('app.current_tenant_id')::uuid predicate the existing authenticated-scoped policies already use) and (b) EXECUTE on signals' 3 *_as_of() functions (get_feature_as_of/get_forecast_as_of/get_anomaly_score_as_of, all (uuid, uuid, timestamptz, timestamptz)) — closing the disclosed forward-ref Phase 4 (footnote ²⁷, PROJECT_DECISIONS #65) left open. files stays 6 tables/89 cols and signals stays structurally unchanged — both are GRANT/policy-only touches, no table/column impact. A real gap was found and fixed during this build: the pre-existing authenticated-scoped RLS policies on document_chunk/document_index do NOT automatically extend to agent_reader (Postgres role-scoped policies apply only to the exact role named — confirmed live via pg_policies, roles = {authenticated}) — GRANT SELECT alone would have left every agent_reader query silently returning zero rows; fixed with the 2 new SELECT-only policies. A new agentReaderDB() connection helper (packages/db/src/client.ts, exported from packages/db/src/index.ts) mirrors consumerDB() exactly (SET LOCAL ROLE agent_reader + set_config('app.current_tenant_id', ...)). A new ESLint rule (apps/api/eslint.config.mjs, scoped to src/agents/**/*.ts excluding __tests__) bans adminDb/getAdminDb/tenantDB imports from @vrida/db (no-restricted-imports) and raw set_config(...) calls (no-restricted-syntax) — verified firing via a temporary probe file (removed after verification; no src/agents/ directory exists in the committed tree yet, since no AgentsService exists). Honest disclosure: agent_reader/agentReaderDB() have ZERO real call sites today (no AgentsService or other agent-execution read path exists yet) — logged as a new OPEN_ITEMS row, not silently treated as fully closed. 4 guards live-reproduced (tenant-scoped read, cross-tenant isolation, write rejection 42501, EXECUTE on the 3 signals functions) via a new regression describe block, "agent_reader role / agentReaderDB() (Phase 6, 2026-07-17)", in apps/api/src/database/__tests__/rls-cross-tenant.spec.ts (4 tests). 4 OPEN_ITEMS rows CLOSED this phase (files/agent_reader, ai/ESLint rule, signals/GRANT EXECUTE, and a stale row about identity.agent_skill_assignment needing a reverse-lookup index — closed-obsolete, since the table it named was dropped); 1 new open row logged (agentReaderDB() zero call sites). Migration packages/db/migrations/20260717000000_identity_reopen_drop_legacy_agent_skill.sql + packages/db/migrations/20260717000001_agent_reader_role.sql. See PROJECT_DECISIONS #67. Full apps/api suite: 1205/1205 (up from 1201 — 4 new agent_reader tests). Phase 6 lock-gate verdict: CLOSES THE ENTIRE 6-PHASE agents-v2/v3 BUILD AUTHORIZATION (Phase 1 platform, Phase 2 ai, Phase 3 semantics, Phase 4 signals, Phase 5 agents, Phase 6 identity + agent_reader — footnotes ²⁴–²⁹, PROJECT_DECISIONS #62–#67).

A separately-authorized Phase 1: Security & Integrity Remediation (2026-07-18) then ran — a live-DB structural sweep across all 25 business schemas (GRANT/REVOKE fixes, append-only enforcement on 16 tables, consumer.event consent enforcement, a new platform.processor_catalog reference table, a cross-module bare-FK audit, a new drift CI gate, and 4 missing set_updated_at triggers), not a new module and no service-layer work. platform gained 1 table (processor_catalog) — now 36 tables / 524 cols (up from 35/518); every other touched schema is constraint/trigger-shape-only. shared's own write-permission lockdown is explicitly deferred to this same effort's Phase 2. Full apps/api suite: 1205/1205 (unchanged). See PROJECT_DECISIONS #68.

³⁰ INVENTORY CORE WRITE PROTECTION REOPEN — RE-LOCKED, 2026-07-15 task run (PROJECT_DECISIONS #75). Schema-only hardening of direct Inventory writes: 26 base tables / 362 base-table columns / 1 view. Ordinary roles retain only catalog edits excluding avg_cost_cents and the four ruled stock-planning columns; operational stock, reservation, movement, movement-line, and Returns receipt posting paths are structurally protected. Transfer tables, Transfer wrappers, executor grants, services, APIs, UI, and runtime wiring remain unbuilt. The first independent verifier returned NOT CLEAN; all ten findings were dispositioned, and the final fresh-context verifier found and forced two more additive corrections before returning CLEAN. Final live shape includes 137 indexes, with complete reservation fulfillment source-unique and every Inventory function unreachable to inventory_command_executor.

³¹ STOCK TRANSFER SCHEMA BUILD — 2026-07-16 task run. Inventory gains transfer/transfer_line/transfer_reconciliation_event: exactly 3 tables / 63 columns / 28 CHECKs / 20 FKs / 11 triggers. Current live Inventory is 29 tables / 425 columns / 1 view. One atomic shipment, partial receipt, one optional lot per line, exact outbound movement-line cost provenance, protected reservations/movements, lifecycle soft-delete guards, and canonical locking are built at the schema layer. Additive verifier correction 20260720000025 makes same-key receipt races deterministic, centralizes the exact FOR UPDATE hierarchy, freezes lifecycle evidence and pointers, and hardens exact retries; 20260720000026 aligns finalization retries for fully cancelled lines. Neither changes the ruled shape. Consequential wrappers remain dormant to all runtime roles. No Transfer service, API, UI, worker, module wiring, or trusted credential gateway was built.

Module Schema locked Docs API Pages Issues Done
platform ✅⁸,²⁴ ✅² 🚧³ 21
identity ✅¹,⁸,⁹,²⁹ ⬜⁵ 🚧⁴
shared ✅⁶,⁸ N/A N/A
multi_loc ✅⁷,⁸,³¹ N/A N/A
pricing ✅¹³
payments ✅¹⁹
integrations
files
ai ✅¹²,²⁵
semantics ✅²⁶
signals ✅²⁷
agents ✅²⁸
search
inventory ✅¹¹,³⁰,³¹ ✅³⁰,³¹
crm ✅¹⁰
pos ✅¹⁴
orders ✅¹⁵
purchasing ✅¹⁶
tax ✅¹⁷
billing ✅¹⁸
admin ✅²⁰
approvals ✅²¹
audit
notifications
reporting
consumer
rewards ✅²²
offers ✅²²
returns ✅²³
consumer_app
Last modified: Jul 16, 2026, 9:35 AM PT
On this page
Esc