Open Items

All open items across the build — dependencies, deferred FKs/APIs/integrations, and issues found. One row per item. Checked by the module runbook lock gate after every lock; resolved items marked closed. This folder should hold no open items at project completion.

Type values: dependency · FK · API · integration · issue

Module Type What's needed Status Trigger
(all) issue Final cross-module integration audit — before migration/launch, run a system-wide pass: walk EVERY cross-schema FK across ALL modules and verify target consistency + every locked cross-module decision, catching drift the per-module seam-checks miss. Per-module Section 4 audits cover each module internally + adjacent seams; this is the one-time global consistency sweep. open before first production migration
(all) issue Transaction-flow diagrams + ERD (high-level module map + per-module ERDs) in architecture/. Confirm docs.vrida.app renders Mermaid before authoring diagram-as-code. open Build incrementally as modules complete; per-module ERD authored as step-7 output of the runbook.
platform dependency Reconciled 2026-07-07 at the payments module build (module #19, PROJECT_DECISIONS #33) — the naming corrected, the service still unbuilt. A DISTINCT Vrida-own-billing ingestion service (NOT payments.PaymentsService, which is scoped exclusively to tenant-customer-facing Stripe Connect money) must normalize Stripe status values to Vrida's vocabulary on ingestion: canceledcancelled, trialingtrial, uncollectiblewritten_off. Platform CHECKs keep Vrida spellings; the service does the mapping. This row's original wording ambiguously said "PaymentsService," which the payments module's own adversarial design-phase pass caught as a real naming conflation risk — corrected in platform.subscription.status's column comment at this build (see CROSS_MODULE_CONTRACTS.md's Platform↔payments row, also reconciled). See PROJECT_DECISIONS #33. open when Vrida's own SaaS-billing ingestion service (distinct from payments.PaymentsService) is built
platform issue Author platform's 7 formal rationale entries (dunning-inline-not-table; promo_code non-tenant-scoped; tenant_entitlement absorbs subscription_addon; source_contract_id plain FK; agreement_acceptance immutable; payment soft-delete; promo_code.discount_value percent-or-cents) in modules/module_spec/platform.md. closed — rationale embedded in §8–§10 of docs/modules/module_spec/platform.md step 7 (author module spec)
platform issue Verify platform schema defaults (trial_days=30, tier prices, retention=90d) match the locked Platform decision in PROJECT_DECISIONS; correct any mismatch. open before/at platform migration
platform→identity dependency Impersonation / support-access audit: sessions recording which operator accessed which tenant, when, why, duration. Also closes operator_audit_log.operator_user_id → identity.identity_user FK. closed — support_access_grant table exists (starts_at, ends_at, reason, access_level, access_level); grantSupportAccess / useSupportAccess / revokeSupportAccess / checkSupportAccess implemented in IdentityService Phase 5D (2026-06-29); identity_access_event logs support_access_granted/used/revoked; operator_audit_log.operator_user_id FK closed in migration 0001_true_loners.sql. Platform admin surface (displaying grants in admin.vrida.app) is a platform/admin pages concern. when identity schema is worked on
platform→identity dependency Console RBAC: operator roles/permissions for admin.vrida.app (support/CS/billing-ops/sales-ops/admin), enforced at API. open when admin module and admin.vrida.app pages are built (identity provides the mechanism — roles, permissions, role assignments; operator role seeds and page enforcement belong to the admin build)
platform→identity dependency Security-events surface: tenant-side auth events (logins, MFA changes, failed logins, SSO/SCIM) for display on tenant detail. open when platform/admin pages are built (identity_access_event is the data source; the display surface is a platform admin pages concern)
platform→notifications dependency Comms/ticket history for the tenant unified timeline (emails/notifications sent + helpdesk tickets). open when notifications module is worked on
platform→ai dependency AI credit enforcement (check balance/spend_limit before an AI call) + per-call consumption ledger writes to ai_credit_transaction, via PlatformService. RE-TRIAGED 2026-07-06: the ai module is now schema-locked (2026-07-06) — the original trigger's technical prerequisite is met. The remaining work is real service-layer integration (PlatformService reading/writing ai-schema tables), not a docs-only fix, so it's re-deferred rather than actioned in this pass. open ai module schema now exists (2026-07-06) — build when PlatformService's AI credit consumption logic is extended to actually read/write ai schema tables
platform dependency AI credit top-up billing — charging the tenant for purchased credit — depends on the Vrida-billing vendor (not yet decided); payment_ref is vendor-neutral until then. open when Vrida-billing vendor is decided
platform issue Existing platform Stripe-coupling (billing_account.stripe_customer_id, subscription.stripe_subscription_id, payment provider default 'stripe') assumes Stripe for Vrida-side billing, but the Vrida-billing vendor is undecided. Reconcile when vendor chosen. open when Vrida-billing vendor is decided
identity issue GIN index on sso_provider.allowed_domains (JSONB array) for email-domain → SSO provider routing lookup. Without it, the login flow that matches a user's email domain to their tenant's SSO provider does a full scan. Defer until the SSO domain-routing query is built. open when SSO domain-routing query is implemented
identity issue role_assignment bare (tenant_id) index missing — the two partial indexes (on (tenant_id, actor_id) WHERE actor_id IS NOT NULL and (tenant_id, actor_group_id) WHERE actor_group_id IS NOT NULL) cover resolution hot-paths but not a bare WHERE tenant_id = X admin list query ("all role assignments in this tenant"). Add when the admin list-all-assignments UI query is built. open when admin role-assignment list query is built
platform→identity dependency Tenant-context interceptor currently reads x-tenant-id into req.tenantId but does NOT yet wrap requests in a tenantDB() transaction — so RLS isn't enforced through the request path; reads use adminDb/getAdminDb() + manual tenant filters. UPDATED 2026-07-08 (Remediation Phase 1, Item 1): tenantDB() itself had zero real call sites anywhere in the codebase and a latent bug — SET LOCAL app.current_tenant_id = $1 is invalid Postgres syntax with a bind parameter, never caught because the function was never exercised. Both the infra bug (fixed via select set_config(...)) and the missing authenticated Postgres role/GRANT closure are now fixed and live-proven (rls-cross-tenant.spec.ts, 5/5 passing — a real cross-tenant write via the authenticated role is now genuinely REJECTED, 42501). Scoped deliberately infra-only this phase (user-confirmed): PlatformService/IdentityService still call getAdminDb() exclusively; migrating either service (or this interceptor) to actually call tenantDB() remains the same open gap this row has always tracked — now unblocked at the infra layer for the first time. See PROJECT_DECISIONS #37. open infra prerequisite now satisfied (Remediation Phase 1 Item 1) — implement when a service or the interceptor is migrated to call tenantDB() for real requests
platform→identity FK Add the 8 deferred FK constraints from platform. 4 → identity.actor: tenant_entitlement.granted_by_user_id, tenant_setup_task.completed_by_user_id, tenant_data_lifecycle.requested_by_user_id, tenant_lifecycle_event.actor_user_id. 4 → identity.identity_user (human-semantics): tenant_contact.identity_user_id, agreement_acceptance.accepted_by_user_id, tenant_internal_activity.performed_by_user_id, operator_audit_log.operator_user_id. All created as plain UUIDs in Phase 1; FK constraints + Drizzle schema update deferred to identity-complete. See platform.md Cross-Phase FK table. closed — all 8 FK constraints wired in platform Drizzle files (.references()) and applied to DB in migration 0001_true_loners.sql (2026-06-29) when identity migrates (Phase 3)
platform→multi_loc FK Add the deferred FK constraint platform.tenant.primary_site_id → multi_loc.site. Plain UUID in Phase 1; constraint added once multi_loc exists. open when multi_loc migrates (Phase 4)
platform+identity+payments dependency Build the tenant registration/signup flow (downstream cross-module surface on www/tenant.vrida.app) — creates the tenant, first user, billing setup, and records agreement acceptance via PlatformService + IdentityService. Cannot be built until platform, identity, and payments exist. open when platform + identity + payments are built
platform→ai dependency AI-driven dunning execution — adaptive retry timing, recovery messaging, and recovery/churn prediction over platform's dunning state (subscription.dunning_*). Platform holds the state; the AI logic + execution are built with the AI module and depend on the Vrida-billing vendor. Resolves the spec §10 "see OPEN_ITEMS" reference. RE-TRIAGED 2026-07-06: the ai module dependency is now met (schema-locked 2026-07-06); the Vrida-billing vendor decision remains genuinely undecided — only half of the original compound trigger has fired. open ai module dependency met (2026-07-06); still blocked on the Vrida-billing vendor decision (unchanged)
platform dependency Seed tier_definition rows (Starter / Pro / Enterprise) — needs finalized tier pricing, caps (max_users, max_sites, max_skus, max_zones), included_locations, price_per_additional_location_cents, and entitled_modules. Cannot seed until pricing is locked. open when tier pricing finalized
platform dependency Seed agreement_version rows (ToS, privacy policy, DPA, MSA) — needs actual legal document content, hosted URLs (R2 or public CDN), effective dates, and content hashes. Cannot seed until legal docs drafted and published. open when legal docs drafted
platform issue tenant_lifecycle_event.event_type CHECK is missing 'past_due'. The active → past_due lifecycle transition has no matching event_type value in the CHECK constraint — so the lifecycle state machine skips writing the event for this transition (billing-driven; no appropriate enum value). Fix: add 'past_due' to the tenant_lifecycle_event_event_type_check CHECK via a migration. Identified during PlatformService Phase 1 build (2026-06-29). closed'past_due' and 'recovered' added to CHECK in migration 20260629030000_platform_past_due_event_type.sql (2026-06-29); Drizzle schema + schema_docs/platform.md updated; PlatformService Phase 2 dunning now writes the lifecycle event on active→past_due and past_due→active(recovered). 28/28 Phase 2 tests green including C3 verifying the CHECK.
platform issue PlatformService Phase 2 logic stubs: entitlement resolution (resolveEntitlements — active-window + precedence override>contract>beta>addon>tier + add-on stacking, spec §9); dunning workflow (startDunning/advanceDunning/recoverDunning — retrying/grace/suspended/recovered, drives tenant.status, AI-driven, spec §10); AI credit consumption (consumeAiCredit — balance draw-down + spend_limit check + per-call ledger write). Billing action writes (changeTier, recordPayment, recordInvoice, grantOverride, AI credit grant/purchase, validatePromoCode, contract CRUD) are naive writes without validation. closed — PlatformService Phase 2 COMPLETE (2026-06-29): resolveEntitlements real logic (active-window filter + 5-source precedence + metered stacking); dunning workflow (recordPaymentFailure, processDunning, recoverDunning — full state machine); consumeAiCredit (atomic balance check + spend_limit enforcement + ledger write); billing/subscription/invoices/payments/promos/audit all built. 28/28 Phase 2 tests green. PlatformService DONE (both phases, 56 tests total).
platform API HTTP controllers/routes not built — PlatformService is service-only. Build controllers (REST endpoints) with the admin pages. partial — Admin tenant-management reads built 2026-06-29: AdminTenantsController (GET /admin/tenants, /admin/tenants/:id, /subscription, /entitlements, /profile, /onboarding, /billing) + Supabase JWT AdminAuthGuard (operator-gated, is_platform_user=true required) + pagination on listTenants; 18 HTTP integration tests green. Remaining: write endpoints + other admin pages + tenant-facing controllers. step 11 / when pages built
platform issue Automated tests for entitlement resolution math and dunning progression deferred. Tenant mgmt cluster: 28/28 tests green (Phase 1). closed — Phase 2 tests built and green (2026-06-29): entitlement resolution (D1–D6: tier baseline, override precedence, metered stacking tier+addon, expired/revoked exclusion, FK); dunning (C1–C3: recordPaymentFailure→past_due event, recoverDunning→recovered event, CHECK verified); credits (E1–E4: grant/consume/ledger/spend_limit). 28/28 Phase 2 + 28/28 Phase 1 = 56/56 total.
platform→identity dependency User-context params (actorUserId, grantedByUserId) are stubbed/optional in PlatformService action methods — audit trail is incomplete until real authenticated-user identity flows in. Distinct from interceptor-wrapping row (which is about tenantDB SET LOCAL / RLS); this is about the identity value reaching business-logic and ledger writes. open — AdminAuthGuard now populates req.actor (actorId, email, fullName) for all admin routes (2026-06-29); wiring actorId into PlatformService write-method parameters is the remaining step. UNBLOCKED — implement at pages/write-endpoint build
platform dependency SUPABASE_URL and SUPABASE_ANON_KEY env vars required at runtime by SupabaseModule (used by AdminAuthGuard for Supabase JWT verification). Documented in apps/api/.env.example. Local dev values from supabase status; production values from Supabase project dashboard. Must be set in all deploy environments (staging, prod). open before any deploy environment is provisioned
identity→audit issue audit.audit_log.actor_user_id must be generalized to identity.actor (polymorphic actor refactor) and gain an actor_type column covering 'user' | 'service_account' | 'agent'. Identity Batch A (actor table) and Batch C (service_account + agent_identity) are now LOCKED 2026-06-28 — trigger condition met. Implementation awaits audit schema build phase. open when audit schema is designed/built
identity issue identity.actor has no index on status. Low-priority — primary access pattern is PK lookup with status read from the fetched row. Needed for admin sweep queries ("list all suspended actors") once the actor admin UI is designed. open when actor admin UI is designed
identity issue identity.identity_access_event.event_type CHECK enum is missing 'scim_user_provisioned' and 'scim_user_deprovisioned'. These values will be produced when SCIM is built. Adding them before SCIM build would leave unexercised enum values; deferring until the SCIM event writer is authored. open when SCIM is built
identity issue identity.actor_group has no (tenant_id, status) composite index for "list inactive groups in tenant" queries. Expected table size per tenant is small (≤50 groups); full tenant_id-filtered scan is acceptable until an admin-UI inactive-group sweep query is built. open when actor-group admin UI is designed
identity dependency role_permission_group wiring table (role_id FK → role, permission_group_id FK → permission_group) — attaches Vrida-defined permission bundles to roles. Deferred from Batch A Pass 2; belongs with the full role-management overhaul. When designed: tenant-scoped (one wiring row per tenant role + bundle pair), RLS-enabled, UNIQUE(role_id, permission_group_id). See DR-13 in identity.md. closed — authored in identity Batch B Pass 1 (2026-06-28); see role_permission_group table + DR-17 in identity.md identity Batch B design
identity issue identity.sod_violation composite index (sod_rule_id, status) for waiver-check query during SoD detection (WHERE sod_rule_id = $rule AND status IN ('waived','open')). Current plan: (sod_rule_id) index + post-filter on status — acceptable at SMB scale. Add the composite index when the violation table exceeds ~10K rows per tenant OR when the daily sweep query plan shows index-scan degradation. open when violations exceed ~10K rows for any single tenant (checkable via a periodic COUNT(*) ... GROUP BY tenant_id sweep) — the "or sweep query plan degrades" disjunct is dropped as unmonitored; if a manual EXPLAIN ANALYZE on the sweep query ever shows a sequential scan under real data volume, treat that as an equivalent, explicitly-observed trigger rather than passively waiting for someone to notice
identity issue sod_rule_role table for role-level SoD constraints (e.g., "no actor should hold both PO Approver and PO Creator roles simultaneously") — role-combination toxic pairs that are detectable at the role level without expanding permissions. Deferred from Batch B Pass 2 (permission-level SoD subsumes role-level for correctness; role-level SoD is an enterprise IAM concept). When built: sibling to sod_rule/sod_rule_permission; global; append-only join of (sod_rule_id, role_id) pairs; separate detection path collecting effective roles (not effective permissions). See DR-19. open v2 / first enterprise customer requiring role-level SoD compliance
identity issue api_key auto-rotation — automatic key expiry + renewal based on a rotation policy (e.g., 90-day forced rotation). Deferred from Batch C (Batch C ships manual rotation only: revoke + create new). tenant_security_policy.api_key_rotation_days (Batch D — AUTHORED 2026-06-28) drives the rotation policy; a background sweep auto-expires keys past their rotation window and emits a notification. api_key.expires_at already supports expiry; auto-rotation adds the enforcement sweep in IdentityService. See DR-23. closed (schema + service CRUD) — schema: tenant_security_policy.api_key_rotation_days + api_key.expires_at complete. CRUD: createApiKey / revokeApiKey built in IdentityService Phase 4 (2026-06-29). Auto-expiry enforcement sweep (background cron: auto-revoke keys past their rotation window + emit notification) is a separate deliverable; deferred to background-jobs / cron phase. background-jobs / cron phase
identity issue consent_record — per-user explicit data-use consent tracking (GDPR Article 7, CCPA opt-in/out). Investigated in Batch D: crm schema owns end-customer/shopper consent; platform.agreement_acceptance covers org-level platform legal consent; US SMB employment basis (GDPR Art. 6(1)(b)) covers staff data processing. No v1 identity schema gap confirmed. See DR-29. open HUMAN-SIGNALED, no automatic detection — legal/sales must flag explicitly when a signed contract or DPA includes a per-user GDPR Article 7 consent clause for staff; check: does the executed contract/DPA contain this clause? No technical event detects this on its own
identity issue tenant_security_policy enterprise knobs — 4 knobs deferred from v1 (ip_allowlist cidr[], require_reauth_for_sensitive boolean, max_session_duration_minutes integer, password_expiry_days_override integer). Each requires enterprise IT context or undefined prerequisites. See DR-27. Add when: (a) ip_allowlist — enterprise tier designed + CIDR matching logic specified; (b) require_reauth_for_sensitive — "sensitive action" taxonomy defined; (c) max_session_duration_minutes — compliance framework requiring wall-clock session ceiling; (d) password_expiry_days_override — enterprise compliance framework. open per-knob, HUMAN-SIGNALED: (a) ip_allowlist — when enterprise tier is designed AND CIDR matching logic is specified; (b) require_reauth_for_sensitive — when a "sensitive action" taxonomy is defined; (c) max_session_duration_minutes — when a compliance framework requiring a wall-clock session ceiling is adopted; (d) password_expiry_days_override — when an enterprise compliance framework requiring it is adopted. No single umbrella event — check each knob's own condition independently
identity issue api_key.key_prefix index is not partial — auth fast-path candidate set includes revoked/expired/deleted rows (correctness fine; service layer filters). Optimize to partial on (key_prefix) WHERE status = 'active' AND deleted_at IS NULL when auth hot-path query is built and benchmarked. open when auth hot-path built
identity issue api_key.last_used_at has no index for unused-key cleanup sweep (WHERE last_used_at < threshold AND status = 'active'). Add when the cleanup sweep is implemented. open when unused-key cleanup sweep built
identity issue [CLOSED-OBSOLETE 2026-07-17, agents-v2/v3 build Phase 6] identity.agent_skill_assignment (the table this row named) was DROPPED — moved to agents.agent_skill_assignment in Phase 5, the old table itself dropped in Phase 6 (see PROJECT_DECISIONS #66/#67). The successor table's own agent_skill_assignment_identity_skill_unique index on (agent_identity_id, skill_version_id) partially serves the same reverse-lookup shape this row named; a dedicated leading-skill_version_id index remains a genuine, separate future optimization if the provisioning audit query is ever built — not re-logged here as a new row, since no such query exists today to benchmark against. closed
identity issue approval_workflow + approval_step (multi-step approval engine) — deferred post-v1 from Batch D. approval_workflow defines a named workflow template (steps, approver assignments per step, SLA). approval_step records each step's state for a specific access_request. Attaches to access_request via two nullable columns (workflow_id UUID nullable FK → approval_workflow, workflow_step_index integer nullable) — no structural change to access_request needed when built; those columns added at that time. When workflow_id IS NULL: single-approver v1 flow. When NOT NULL: workflow engine drives approval. Design + attachment pattern in DR-22 in identity.md + Decision 11a in identity_expansion_intent.md. open first multi-step approval use case (enterprise tenant or compliance requirement)
identity issue agent_execution log table — per-run record of what an AI agent did (inputs, outputs, token cost, latency, model used, skill exercised). Deferred from Batch C (Batch C ships agent identity; execution history is an operational concern, not an identity concern). When built: belongs in the ai schema alongside ai_request (Bedrock-call log), NOT in identity. Likely: ai.agent_execution with FK → identity.actor (agent_id) + identity.agent_skill (skill exercised). See DR-24. closed — built 2026-07-06 as ai.agent_execution (self-FK resolves_execution_id for propose/execute linkage, idempotency_key dedup, agent_identity_id/permission_id FKs to identity, not identity.actor/agent_skill as originally sketched). See PROJECT_DECISIONS #25.
identity issue sso_provider v1 single-provider assumption — v1 service layer enforces at-most-one SSO provider per tenant (no UNIQUE constraint at DB level). When multi-provider SSO is designed (post-v1), evaluate adding UNIQUE(tenant_id, provider_type) or restructuring the config model to allow multiple active providers. Drop the service-layer guard at that point and replace with the DB constraint. open when multi-provider SSO is built
platform issue No operator login/session flow for admin.vrida.app (apps/web/admin). AdminAuthGuard requires a real Supabase JWT for a user with is_platform_user=true; until a login page exists, local dev must mint ADMIN_API_TOKEN out-of-band (createUser + provisionUser + signInWithPassword, mirroring admin-tenants.spec.ts's fixture pattern). 2026-06-30: used this exact method to mint a real token and verify Tenants list/detail render real data end-to-end (first true E2E validation) — confirms the fetch code is correct; a real login page is still not built. open when the admin console needs a working login for real use, not just local dev
platform issue apps/api boot gap — closed 2026-06-30. packages/db declared "type":"module" with main/types pointing directly at raw .ts source (no build step), so nest build && node dist/main.js crashed with SyntaxError: Unexpected token 'export' — invisible in Jest, which has its own moduleNameMapper + forced-CommonJS transform bypassing Node's module resolution entirely. Fix: added a real tsc build step to packages/db (CJS output via rootDir/outDir, dropped the unnecessary "type":"module" — confirmed via grep that apps/api is the only consumer), pointed main/types at the compiled dist/. Verified: node dist/main.js boots and serves real traffic; 125/125 Jest tests still pass; npm run build from repo root via npm workspaces builds both packages. Follow-up caught in final verification: with packages/db now a real build dependency, apps/api's own npm run build failed standalone if packages/db/dist didn't already exist — added a prebuild script (npm run build --workspace=@vrida/db) to apps/api/package.json so the dependency always builds first, confirmed working both from apps/api directly and via npm run build --workspace=api from repo root. (Aside, not fixed: root turbo build itself fails on a pre-existing Missing packageManager field — unrelated Turborepo version requirement, noted but out of scope.) closed
platform dependency Admin console (admin.vrida.app) mock pages need real cross-tenant endpoints to go live: (a) list-all-subscriptions/-invoices/-payments (platform.subscription/subscription_invoice/payment currently only have per-tenant reads); (b) cross-tenant tenant_usage_summary aggregate for Usage & metering; (c) a "list operators" read (identity.identity_user WHERE is_platform_user = true) — no such method exists in IdentityService today. open HOUSEKEEPING, recurring check: re-verify sub-items (a) list-all endpoints, (b) cross-tenant usage aggregate, (c) list-operators read at every admin console build session; close each sub-item independently as its real endpoint ships
platform issue listOperatorAudit() exists as a PlatformService method (row 33, closed) but has no HTTP route on AdminTenantsController or elsewhere — needed to make the admin console's Audit log page real instead of 🟡 sample. closed 2026-07-08 — routed via new AdminOpsController's GET /admin/audit-log; Audit log page now calls it, real data confirmed rendering in browser. See PROJECT_DECISIONS #41.
platform issue Tier taxonomy mismatch resolved 2026-06-30: Seed/Grow/Bloom are now the official DISPLAY NAMES for the real tier keys (starter→Seed, pro→Grow, enterprise→Bloom) — no separate marketing ladder. The mockup's 4th "Crown" tier is dropped (there are only 3 real tiers); its distinguishing features (Multi-location, etc.) folded into Bloom/enterprise. DB keys, seed data, and service layer are unchanged — this is display-label-only, no migration. apps/web/admin's TIER_PALETTE is now the single source for both real and mock pages (MarketingTierBadge/MARKETING_TIER_PALETTE removed). closed
platform issue Feature Flags admin console page removed 2026-06-30 — redundant with Plans & Entitlements (same underlying tables/resolveEntitlements(); a separate runtime-toggle concept was never confirmed as a real product need). closed — removed
platform issue Announcements + Settings — 🔴 PROPOSED FEATURE closed 2026-06-30, now skeleton tables. platform.announcement (mixed-scope, RLS) and platform.platform_setting (global key-value) added through the Section 4 audit gate — see PROJECT_DECISIONS #15. Both pages now render off real tables (🟡 sample-flagged, not 🔴) with read-only service stubs (listAnnouncements, getSettings) behind GET /admin/announcements / GET /admin/settings. Still open: no write endpoint for either; platform_setting's scope (operator preferences vs. platform-wide config) was deliberately left undecided — the table just doesn't encode a scope at all this pass (no tenant_id), so the product decision is deferred without blocking the skeleton. open write endpoints + a real Settings authoring flow, when prioritized
platform issue Admin console Tenants list omits a "Primary contact" column that appears in the source mockup — getContacts(tenantId) would need to be called per row (N+1) since listTenants() returns bare tenant rows with no embedded contact. Add either an embedded-contact variant of listTenants() or a dedicated bulk-contacts-by-tenant-ids endpoint before restoring this column. open when a bulk/embedded contact fetch exists
platform issue 4 admin console nav items (Contracts & Promotions, Legal & Agreements, Work Queue, Support Access) had no home in the Vrida Admin Console mockup's 5-group nav (Overview/Customers/Billing/Platform/System) — they were slotted in during build-time reconciliation (Contracts & Promotions + Legal & Agreements → Billing; Work Queue → Overview; Support Access → System) rather than dropped. Confirm this placement is acceptable, or re-home them, before the nav is treated as final. open HUMAN DECISION, no auto-trigger — resolve before admin.vrida.app's nav is treated as final, or before public launch, whichever comes first; owner: whoever next redesigns the admin nav
(all) issue Root turbo build fails with "Could not resolve workspaces — Missing packageManager field in package.json" — a Turborepo version requirement, not something this build introduced (surfaced 2026-06-30 while verifying the packages/db/apps/api boot fix; per-workspace npm run build --workspace=<x> works fine and is unaffected). Add a packageManager field to root package.json (e.g. pinning the actual npm version in use) to restore turbo build/turbo dev as a working entry point. open when someone needs turbo build/turbo dev to work again (currently bypassed via direct per-package npm scripts)
(all) issue Drizzle snapshot lineage stale: migrations/meta snapshots stop at 0001; several migrations since were hand-applied without advancing the snapshot chain, so drizzle-kit generate emits phantom statements (incl. the "undefined"."inet" codegen bug on identity_access_event.ip_address). No real drift — schema↔DB verified 0 drift. Fix = re-baseline/pin the drizzle snapshot to match live DB in a dedicated pass so generate is trustworthy again. Not urgent (migrations are hand-written, never applied from generate output). open when a dedicated pass re-baselines the migration snapshot chain (currently bypassed via hand-written migrations)
identity issue Tenant admin UI (tenant.vrida.app) — mockup built 2026-06-30, all 9 page areas from identity.md §17 now exist as 🟡 sample-data skeletons in apps/web/tenant (Members, Groups, Roles & Permissions, Service Accounts, AI Agents, Access Requests, Active Sessions, Security Policy, SoD Compliance). Still open: zero HTTP wiring (no controllers exist for any of IdentityService's 86 methods — see the HTTP-layer audit finding), no auth guard for tenant users (only AdminAuthGuard for operators exists), no tenant-context/RLS request wrapping (row above on TenantContextMiddleware). Tenant-side operators still have no way to manage their own members/roles/sessions for real today. open when tenant.vrida.app pages are wired to real endpoints
identity issue Small-nursery scoping decision needed for 5 of the 9 tenant.vrida.app mockup pages, flagged during the 2026-06-30 mockup build as reading enterprise-heavy relative to what a single-location nursery needs day-to-day: Roles & Permissions (6 sub-tabs — role hierarchy, permission bundles, templates, time-boxed overrides is a full RBAC/ABAC surface), Service Accounts (machine-to-machine API keys), AI Agents, Access Requests (approval workflow), SoD Compliance. Members, Groups, and Active Sessions read as appropriately scoped. This is a product decision (simplify/hide/defer for the small-nursery tier vs. build in full), not a technical blocker — the mockups exist either way. open HUMAN DECISION, no auto-trigger — product/design lead must decide (simplify / hide / defer vs. build in full) for the 5 named pages before tenant.vrida.app's identity pages are treated as final for the small-nursery tier; no technical event surfaces this
platform issue Announcements beyond the admin skeleton: listAnnouncements() filters only on tenant_id scope and deleted_at — it does NOT filter by status (drafts/expired are still returned), the starts_at/ends_at active window, or audience; there is no publish/expire lifecycle service, and no display surface where a tenant or public visitor actually sees a published announcement (tenant.vrida.app, www). Row 58 tracks the admin write-endpoint gap; this is the separate, larger consumption-side build. open when Announcements moves beyond the admin-only skeleton
platform issue The 6 tenant-detail tabs (Lifecycle, Contacts, Onboarding, Usage, Data & Retention, Internal Notes) added 2026-06-30 are UI-skeleton only — real schema shapes (tenant_lifecycle_event, tenant_contact, tenant_setup_task, tenant_usage_summary, tenant_data_lifecycle, tenant_internal_activity) but 🟡 sample data, no endpoint wiring. See TenantSkeletonPanels.tsx. closed 2026-07-08 — all 6 wired to real endpoints (Onboarding reuses the Overview tab's already-fetched getTenantOnboarding(), zero new fetch); the Contracts and Support Access tabs (added later, tracked separately — see row 60) also went real in the same pass. See PROJECT_DECISIONS #41.
platform issue 15 admin console pages remain fully 🟡 sample-flagged with no real endpoint at all: dashboard, work-queue, reports, audit, onboarding, subscriptions, invoices, payments, entitlements, plans, contracts-promotions, legal-agreements, usage, operators, support-access. Distinct from Announcements/Settings (real read endpoint, no write yet) and Tenants/Tenant-detail-Overview (fully real). Rows 54/55/58/59/60 track specific known blockers for some of these; this row is the aggregate inventory so the remaining surface isn't lost between the specific items. recounted 2026-07-08 (PROJECT_DECISIONS #41): down to 4, by explicit in-session decision, not oversight. dashboard ✅ (Work Queue folded in as a KPI tile), work-queue ✅ (folded into dashboard), audit ✅, onboarding ✅, subscriptions ✅, invoices ✅, payments ✅, contracts-promotions ✅ (folded into the tenant Contracts tab + the global Promotions page), legal-agreements ✅ (folded into Agreement Versions page + the Contracts tab's acceptance list), support-access ✅. Still 🟡 sample, left there deliberately when asked: entitlements (catalog page — separate from the now-real per-tenant Entitlements tab), plans (pricing STRUCTURE is an unresolved business decision — row 68), operators (blocked on a real schema gap — platform-level roles aren't representable in identity today), reports. usage is split: the per-tenant Usage tab is real, the top-level Usage page's daily-chart/trend-line portion is still sample (no time-series endpoint). open
platform issue Pricing STRUCTURE unresolved — only the tier DISPLAY NAMES were settled 2026-06-30 (row 56: Seed/Grow/Bloom). Whether pricing is per-location, flat-tiered, usage-metered, or a hybrid is still an open strategic/business decision, not a build task — it blocks tier_definition seeding (row 28) and the Plans & pricing page going real. open HUMAN DECISION, no auto-trigger — pricing STRUCTURE (per-location / flat-tiered / usage-metered / hybrid) requires an explicit product/business decision; no technical event will surface this. Once decided, unblocks row 28 (tier_definition seeding)
(all) issue 1 of the 13 nursery product/vertical modules (Inventory, POS, Orders, Purchasing, CRM, Reporting, Production, Customer App, Multi-location, Admin, Billing, Audit, Notifications) has entered the module pipeline so far — Multi-location (schema locked 2026-07-05, schema only, no service layer or pages yet). Platform (built), Identity (schema-design-complete, no pages), and Shared (schema locked + seeded 2026-07-05, foundation reference data, not a vertical module) have also started. The vertical product layer that makes this an actual nursery ERP/POS, beyond the platform+identity+shared foundation, has only just begun. open when the module pipeline picks the next module after Platform/Identity/Shared/Multi-Location
(all) issue docs/business-requirements/MARKET_RESEARCH_GAPS.md tracks 33 nursery vertical-strategy gaps with its own Accepted/Deferred/Rejected/Open status column (6 Accepted · 17 Deferred · 5 Rejected · 5 Open-pending-decision per the doc's own tally) — none of these decisions have been translated into per-module OPEN_ITEMS rows or fed into any module's Part A (Features + gaps) yet, since no product module has entered the pipeline (see the row above). Risk: when a module's design starts, its Part A audit needs to actively pull the relevant gaps from that registry, not rediscover them from scratch. open when each product module's Part A (Features + gaps) is authored — pull relevant gaps from MARKET_RESEARCH_GAPS.md
(all) issue All commits are local-only — no git remote is configured and nothing has been pushed. Single point of failure: if this machine is lost or the working copy is corrupted, all work since project start is unrecoverable; no shared visibility, no CI, no backup. open before this becomes a team effort, or as soon as a remote is available — whichever comes first
(all) issue MODULE_INDEX.md header states "216 tables · 3,116 cols" (updated 2026-07-05 for the shared module lock) but summing all 23 table rows gives 221 tables / 3,194 cols — a 5-table / 78-column gap, unchanged in size since it was first root-caused. Root-caused 2026-06-30: traced via git log/git blame across the file's full history. At the very first commit (bcdc761), Platform=21/378 and Identity=11/140, header=188 tables/2,816 cols — meaning the OTHER 21 modules' rows already summed higher than the header's arithmetic implied for them, a gap that has existed unchanged since the document was first written and has simply been carried forward untouched through every later edit (each of which only ever added the correct delta for the module it touched — Platform, Identity, now Shared — on top of the already-wrong baseline for the rest). Since none of the remaining 19 un-locked modules are migrated (no live DB) or have their own schema_docs/ files, there is no independent ground truth to determine whether one specific row is wrong or whether the original header arithmetic itself was the mistake. Platform, Identity, Shared, and Multi-Location rows are all independently confirmed correct against the live DB. open when a product module's Section 4 audit or migration provides real ground truth for one of the other 19 un-locked modules, enabling the specific row (or the header) to be corrected with confidence — not before
platform issue apps/web/admin/lib/nav.ts still tags Announcements and Settings with tier: 'proposed' (the 🔴 flag) even though both pages' own content now renders DataFlagBanner tier="sample" (🟡) and calls real endpoints (GET /admin/announcements, GET /admin/settings) — found during the 2026-06-30 docs audit while verifying no 🔴-proposed pages remained. The sidebar nav badge and the page content now disagree. Fix: update both tier values in nav.ts to 'sample'. open next admin.vrida.app nav touch
shared dependency shared.country_currency join table deferred — country.default_currency_code (single FK) covers the common case; a join table only matters for countries with genuinely simultaneous multiple legal tenders (Panama's PAB+USD, Timor-Leste's USD, etc.), out of scope for a US-market product today. See PROJECT_DECISIONS #17. open when a dual-currency-country tenant needs both currencies modeled
shared issue shared.plant_climate_zone.system must match the system embedded in its own min_zone_code/max_zone_code (e.g. a row with system='usda' must have both zone codes prefixed 'usda:') — not DB-enforceable without a trigger. Verified consistent in the current seed (0 mismatches, covered by a regression test), but nothing structurally prevents a future write from violating it. Whichever service first writes to this table (likely alongside SharedService, not yet built) must validate at write time — same pattern as identity.role.parent_role_id's app-enforced cross-tenant guard (DR-12). open when SharedService or any writer of plant_climate_zone is built
shared issue shared.plant filter-browsing indexes deferred to v1.5 (same deferral v1's schema_shared.md already made): (plant_type) WHERE is_active, (usda hardiness range), (is_verified) for the future consumer-app plant browser and AI-review queue. Direct-ID/botanical_name/slug/common-name lookups are already indexed and sufficient for v1. open when the consumer-app plant browser or AI-review queue is built
shared issue Every shared.* table has an updated_at column, but no table has an updated_at-maintaining trigger (verified live: zero triggers exist on any shared.* table) — updated_at is set once at insert (default now()) and never touched again, unlike every tenant-scoped module which uses platform.set_updated_at(). Found 2026-07-06 during the SCHEMA_DESIGN_RUNBOOK.md reconciliation's independent fact-check pass. Low-stakes today (reference data is rarely mutated post-seed) but silently wrong if any future write ever updates a shared.* row without also touching updated_at by hand. open when SharedService or any writer of shared.* tables is built — add the trigger then, or decide explicitly that shared.updated_at is seed-time-only and drop the column
multi_loc FK UPDATE 2026-07-10 (2nd): 3 of the original 4 candidate columns are now RESOLVED — the blocking orphan was investigated (isolated dev-seed fixture junk, confirmed via a DB-wide tenant-isolation scan) and deleted, then identity.user_site_assignment.site_id, identity.tenant_user.default_site_id, and identity.invitation_site_assignment.site_id were all wired to real composite FKs → multi_loc.site(id, tenant_id) (multi_loc reopened 1st time since its 2026-06-29 lock; identity reopened 5th time). See PROJECT_DECISIONS #54. Still genuinely deferred, untouched by this pass: platform.tenant.primary_site_idmulti_loc.site (would reopen the locked platform module) and identity.user_permission_override.scope_idmulti_loc.site (when scope_type='site') — independently confirmed via pg_constraint/\d to still carry zero FK on either column. open (2 of 5 candidate columns remain) dedicated pass to reopen platform (for primary_site_id) — no blocker known; user_permission_override.scope_id has no known live blocker either, just not prioritized yet
multi_loc issue multi_loc.site.climate_zone_code's system-appropriateness (USDA for US, RHS for UK, etc.) is not DB-enforced — same category of gap as shared.plant_climate_zone.system (module #3). open when a MultiLocService or site-creation UI is built
multi_loc issue multi_loc.site.measurement_system has no FK target in shared — shared.country carries no per-country unit-system column; service-layer must default it from country_code at creation time. open when a country-level (not just site-level) unit-system lookup is needed elsewhere
multi_loc issue transfer/transfer_line (cross-site stock movement), site-level pricing/permission overrides, and the PostGIS geo-proximity index on lat/long remain deferred to v1.5, unchanged from the v1 design's own deferral. open v1.5, when multi-site operational features are prioritized
(all) issue RECONCILED 2026-07-06. SCHEMA_DESIGN_RUNBOOK.md §6 previously called for two files never created for ANY v2 module lock — docs/DESIGN_RATIONALE.md and docs/FORWARD_FK_REGISTRY.md — plus referenced docs/SCHEMA.md and a project-ROOT DOCS_INDEX.md, neither matching the actual v2 layout. The runbook has been rewritten to describe actual practice: module-level rationale lives in PROJECT_DECISIONS.md numbered entries (Item P satisfied there); finer-grained per-choice rationale lives inline in module_spec/<module>.md via the existing "DR-N" citation convention (no separate DESIGN_RATIONALE.md file — recommended and applied, see PROJECT_DECISIONS for the confirmation record); deferred forward-ref FK tracking lives in CROSS_MODULE_CONTRACTS.md seam rows + OPEN_ITEMS FK-type rows (no dedicated registry file created — whether one should exist is still an open question, see the row below); file-path references corrected to docs/database/schema_docs/<module>.md (per module) and docs/DOCS_INDEX.md (inside docs/, not root). Also added: module_spec/<module>.md and OPEN_ITEMS.md as explicit lock-gate items (both were consistently produced by every real lock but never listed in the gate), and a docs-rebuild-and-deploy step. closed
(all) issue Whether to create a dedicated docs/FORWARD_FK_REGISTRY.md file remains an open process question (not resolved by the 2026-07-06 runbook reconciliation above, deliberately left open rather than created unprompted). Current tracking (CROSS_MODULE_CONTRACTS.md seam rows + OPEN_ITEMS FK rows) works but has no single OPEN/READY/DONE view across all deferred FKs system-wide. open if/when someone needs a single cross-module view of deferred FK status, rather than scanning CROSS_MODULE_CONTRACTS + OPEN_ITEMS separately
(all) issue docs/process/runbooks/module-design.md, smoke-test.md, and ship.md — referenced by both CLAUDE.md's "module pipeline" section and docs/process/runbooks/README.md as the actual 4-part (A–D) module pipeline every module runs through — do not exist as files (only README.md and SERVICE_LAYER_RUNBOOK.md exist in that folder). Found during the 2026-07-06 SCHEMA_DESIGN_RUNBOOK.md reconciliation pass. All 4 real module locks (platform, identity, shared, multi_loc) were run entirely through docs/database/SCHEMA_DESIGN_RUNBOOK.md plus direct build instructions — not through a followed module-design.md. This is a bigger, pre-existing process-architecture gap than the runbook drift above (a whole tier of documented process was never implemented as files), out of scope for a runbook-only reconciliation pass. open a dedicated pass to either write the 3 missing runbook files (retroactively describing what SCHEMA_DESIGN_RUNBOOK.md + practice already cover) or update CLAUDE.md/process/runbooks/README.md to stop referencing files that don't exist — human call on which
(all) issue SUPERSEDED by the autonomy-first retro-audit below (2026-07-06). Autonomy-first was elevated from an "AI Capability Plane pass" to SCHEMA_DESIGN_RUNBOOK.md §0's governing rule (non-skippable, 6 explicit questions), and all 4 locked modules were retro-audited against it — see the module-specific rows below, not just a generic "AI-plane decision missing" note. closed superseded by the rows below
(all) issue CLOSED 2026-07-06. Autonomy-first retro-audit summary (2026-07-06) — the gap analysis that motivated the same-day canonical-pattern backfill across all 4 locked modules. See PROJECT_DECISIONS #19 for the pattern and per-module additions; the 5 module-specific rows below (now all closed) covered the actionable findings. closed resolved by PROJECT_DECISIONS #19
platform issue CLOSED 2026-07-06. operator_audit_log.operator_user_id and tenant_internal_activity.performed_by_user_id retargeted from identity.identity_user to identity.actor (zero-backfill — identity_user.id is itself a FK to actor.id). See PROJECT_DECISIONS #19. closed resolved by PROJECT_DECISIONS #19
platform issue CLOSED 2026-07-06. tenant_entitlement and promo_code gained automation_source; contract gained the full human-in-the-loop seam (review_status/review_reason/reviewed_by_actor_id/reviewed_at). payment.refund_amount_cents doesn't exist yet (payments module unbuilt) — noted as a forward-looking design point for that module's own eventual autonomy pass, not fixed here. See PROJECT_DECISIONS #19. closed resolved by PROJECT_DECISIONS #19; payments module's own future autonomy pass should reference this precedent
identity issue PARTIALLY CLOSED 2026-07-06 (agent-approval gate CLOSED 2026-07-06). sod_violation gained decision_snapshot (jsonb, nullable) — column shipped, exact key shape deliberately deferred to whoever next builds SoD detection's next iteration (still open, see PROJECT_DECISIONS #19). role.requires_approval_for_agents — the schema-level piece of the agent-elevation approval gate — is now fully enforced: assignRole() branches on it, routing gated agent-assignee requests through submitAccessRequest() instead of assigning directly (loop-prevented via skipApprovalGate at the one call site that needs it, approveAccessRequest()). Return type changed to `{status:'assigned' 'pending_approval', id}. Tests added (C6–C8, identity-governance.spec.ts`). See PROJECT_DECISIONS #21. open
shared issue CLOSED 2026-07-06. plant_common_name and plant_climate_zone gained data_source/is_verified matching plant's exact shape, plus created_by_actor_id; all 3 plant tables now have actor-attribution. Existing 70/68 rows backfilled to data_source='seed', is_verified=true. The AI-review-queue MECHANISM (not just the index) is still not built — still open, unchanged from the existing filter-browsing-indexes row above. See PROJECT_DECISIONS #19. open the review mechanism itself is tracked in the existing plant filter-browsing-indexes row above (consumer-app plant browser / AI-review queue build)
multi_loc issue CLOSED 2026-07-06. site gained the full canonical set: created_by_actor_id/updated_by_actor_id, automation_source, decision_provenance (field-level, for climate_zone_code/measurement_system), and the full human-in-the-loop seam. See PROJECT_DECISIONS #19. closed resolved by PROJECT_DECISIONS #19
(all) issue CLOSED 2026-07-06. The 2026-07-06 autonomy-first retro-audit and backfill (PROJECT_DECISIONS #19) covered only the schema-translation half of the AI Capability Plane pass — none of the 4 locked modules had been run through the capability-discovery half (Part D's 15-question module-walk). Now done: all 4 modules run through Part D by fresh agents, findings recorded in each module's own module_spec.md (platform §14, identity §20, shared §9, multi_loc §9) and summarized in PROJECT_DECISIONS #20. The 4 locked modules are now at the same AI-Capability-Plane bar crm will be held to. closed resolved by PROJECT_DECISIONS #20
platform issue [Part D discovery, 2026-07-06, deferrable] No home exists for a system/agent-detected finding (anomaly, reconciliation break) distinct from an operator-performed action — tenant_internal_activity.activity_type's CHECK list has no such value, and operator_audit_log is scoped to privileged operator actions. See PROJECT_DECISIONS #20 / module_spec/platform.md §14 (D6/D9). open once a concrete B6/B13 service layer exists to populate it — design once, ideally across modules that hit the same need, not per-module
shared issue CLOSED 2026-07-06. No review/approval seam exists on plant/plant_common_name/plant_climate_zone. All 3 tables now have review_status (CHECK IN not_required/pending/approved/rejected, DEFAULT not_required), review_reason, reviewed_by_actor_id (FK → identity.actor), reviewed_at — matching platform.contract's seam, plus a new CHECK (is_verified = false OR review_status IN ('not_required','approved')) that contract's own seam doesn't yet have. Partial index on review_status='pending' per table for the future review queue. Nullable/defaulted — all 252 existing rows satisfy the CHECK via the default, zero backfill needed. See PROJECT_DECISIONS #21 / module_spec/shared.md §9 (gap 1, closed) / schema_docs/shared.md. closed resolved by PROJECT_DECISIONS #21
identity issue [Part D discovery, 2026-07-06, deferrable] role_assignment has no terminal status value or expiry sweep when ends_at lapses — unlike invitation/access_request/support_access_grant/api_key, which all have one. Audit views of "active" assignments can show functionally-expired rows still labeled active; no identity_access_event is written on lapse. See PROJECT_DECISIONS #20 / module_spec/identity.md §20 (D14). open when an admin audit/reporting view over role_assignment is built, or when seasonal-staff offboarding hygiene becomes a real complaint
identity issue [Part D discovery, 2026-07-06, deferrable] No rate-limiting/lockout state exists for repeated login/API-key auth failures — identity_access_event's event_type CHECK already includes account_locked/account_unlocked, implying lockout is intended, but nothing tracks the counter/threshold that would trigger those events. May belong to Supabase Auth's domain (credentials/lockout) rather than identity — needs a design decision before a column is added. See PROJECT_DECISIONS #20 / module_spec/identity.md §20 (D11). open when brute-force/credential-stuffing defense is prioritized, or when Supabase Auth's native lockout capability is evaluated and found insufficient
identity issue [Part D discovery, 2026-07-06, deferrable] No structured "before" snapshot exists for tenant_security_policy changes or cumulative role_permission state changes, unlike sod_violation.decision_snapshot's pattern for SoD detection. Reversal currently relies on unstructured identity_access_event.metadata if the caller happened to log prior values. See PROJECT_DECISIONS #20 / module_spec/identity.md §20 (D10). open if one-click/automatic rollback of a security-policy or bulk role-permission change is ever required (currently low-frequency actions with adequate manual reconstruction via existing append-only history)
shared issue [Part D discovery, 2026-07-06, deferrable] No scheduled consistency-check job exists for the two known, DB-unenforceable invariants: plant_climate_zone.system vs. its own min_zone_code/max_zone_code prefixes, and the countrylocale circular reference. Both are only "verified consistent in seed data" today, with no ongoing guard once writes happen outside seed migrations. See PROJECT_DECISIONS #20 / module_spec/shared.md §9 (D9). RE-TRIAGED 2026-07-06: the original "first consuming module" framing assumed a consumer's arrival would motivate building SharedService — that assumption was wrong. inventory, pricing, AND crm are now ALL built and all consume shared directly via FK, with no SharedService ever having been built as an intermediary. The literal "first consumer" condition has fired 3 times over with no SharedService to show for it. open when SharedService is built — the "first consumer" trigger is retired; 3 consumers now exist without it, so consumer-count is not a reliable signal for when this gets built
shared issue [Part D discovery, 2026-07-06, deferrable] No defined reversal window (D10) for AI-generated inserts into plant/plant_common_name/plant_climate_zone — no stated SLA for how long a clean hard-delete stays safe before downstream FKs (from inventory.item, a future consumer_interest.interest_ref) make deletion unsafe and force deactivate-instead-of-delete. See PROJECT_DECISIONS #20 / module_spec/shared.md §9 (D10). RE-TRIAGED 2026-07-06: inventory is now built and is explicitly documented as the first real FK consumer of shared.plant (PROJECT_DECISIONS #24) — the originally-stated condition has technically fired. However, no service layer (SharedService or InventoryService) exists yet to actually need or enforce a reversal-window SLA — this is a policy decision that needs a service to implement, not something that becomes actionable just because a consuming table exists. open when SharedService or InventoryService is built and implements delete/deactivate logic for rows referencing shared.plant — define the SLA then, not before
(all) issue G5 (agent memory) — no memory subsystem exists. docs/ai/AI_CAPABILITY_GAPS.md names this the one gap with real schema-design implications; AI_CAPABILITY_PLANE.md B12 (tenant operating memory) hints at it but is under-formalized. No dedicated table exists anywhere (confirmed via grep, 2026-07-06). SCHEMA_DESIGN_RUNBOOK.md §2.2.2's new question 7 asks every future module whether it reads/writes memory, but routes any answer through decision_provenance's jsonb (memory_refs key) rather than a real table — deliberately, since there's no runtime to populate one yet. closed — built 2026-07-06 as ai.agent_memory (partial-unique WHERE status='active', decision_provenance.memory_refs on crm/inventory tables now resolves to a real target). See PROJECT_DECISIONS #25.
(all) issue G5 (agent memory) — memory-poisoning defense, schema side. Once a real memory table exists, it needs the same trusted-source/automation_source discipline A11 requires for documents and tools (see the bridge note in SCHEMA_DESIGN_RUNBOOK.md §2.2.1). Not designable yet — there's no table to defend. open when the memory table is designed (immediately follows the row above)
(all) issue G5 (agent memory) — AI_CAPABILITY_PLANE.md A11 doesn't yet name memory-poisoning as a category distinct from prompt-injection/tool-poisoning. A documentation update to the Plane itself, not a runbook or schema change — SCHEMA_DESIGN_RUNBOOK.md §2.2.1 bridges the gap in the meantime by treating D11 as if it already covers this. open Plane documentation update — separate track from the schema runbook, owned by whoever maintains AI_CAPABILITY_PLANE.md
(all) issue G3 (multi-agent handoff) — no agent-to-agent delegation/handoff table exists. AI_CAPABILITY_PLANE.md's A4/A5 only govern a human delegating to one agent; B8 (multi-agent plans / outcome planner) is the only multi-agent concept in the Plane and is itself listed as deferred. SCHEMA_DESIGN_RUNBOOK.md §2.2.2 question 2 routes any delegation-chain info through decision_provenance (delegated_by_actor_id key) rather than a real table, deliberately — there's no multi-agent runtime to populate one yet. open when B8 (multi-agent plans) stops being deferred — design the actual handoff/delegation table then
(all) issue G1 (agent-readable catalog exposure) — ruled OUT for schema, per the inventory module's AI Capability Plane pass (2026-07-06). A tenant's catalog/inventory needs an "agent-readable/exposed" flag for external buying-agent protocols (ChatGPT Instant Checkout, Google AP2, Shopify Storefront MCP, etc.) — AI_CAPABILITY_GAPS.md's highest-revenue-relevance gap. inventory.item now exists (built 2026-07-06) but the gaps doc's own closing scope note says agentic commerce exposure is "not the schema-design runbook"'s concern — no flag column was added. See PROJECT_DECISIONS #24. open when the API layer builds agentic-commerce exposure
(all) issue G8 (proactive/scheduled agents) — no schedule/trigger table exists. "Scheduled agent, with its own A3 budget and A4 authority" is a first-class runtime concept per AI_CAPABILITY_GAPS.md, currently unbuilt (confirmed via grep — no schedule/trigger table anywhere). open when the agent runtime exists — design the schedule/trigger table as part of that build
(all) issue G2 (MCP server endpoints) — not a schema concern. The market standard is exposing a governed MCP server (in addition to A1's inward-only service boundary) so internal/third-party agents discover tools via a unified, OAuth-2.1-secured interface. Pure API-layer work; explicitly excluded from the schema runbook. open when the API layer is built
(all) issue G4 (governed semantic layer / business ontology) — not a schema concern beyond minor table annotations. "Ask your data" (B16/B17) and reporting (B19) are only as reliable as a semantic layer beneath them; B3 (self-maintaining master data) is adjacent but distinct. Explicitly excluded from the schema runbook (query/API-layer work). open when the query/API layer is built
(all) issue G6 (simulation/sandbox) + G7 (trajectory evals, CI-gated regression, OpenTelemetry GenAI conventions) — testing/observability-layer work, not schema. A9 is post-hoc replay; pre-production simulation against synthetic scenarios (spring rush, frost event, spoilage) is a distinct, unbuilt capability. Explicitly excluded from the schema runbook. open when the testing/observability layer is built
(all) issue G11 (standardized agent identity protocols — W3C DIDs/VCs, IETF agent-auth, SCIM-for-agents) — future-proofing, not urgent. A5's agent passport is a proprietary analog that works today; aligning to emerging standards is identity/API-layer work, not a schema-runbook concern. open when A5 is aligned to external standards (no urgency signal yet)
(all) issue G9 (agent marketplace) and G10 (voice/vision capture) — tracked, no dedicated action needed now. G9 is appropriately not covered yet (low priority for a single-vertical SMB product). G10 is partially covered today at the design-question level by SCHEMA_DESIGN_RUNBOOK.md Part D's D15 (capture modality); the actual built capability (photograph a packing slip → draft PO) is a capture/API-layer concern, not schema. Both remain fully described in docs/ai/AI_CAPABILITY_GAPS.md. open G9: when a multi-tenant agent marketplace becomes a real product priority; G10: when the capture/API layer builds voice/vision ingestion
(all) issue Question 7 (agent/tenant memory, added 2026-07-06 to SCHEMA_DESIGN_RUNBOOK.md §2.2.2) has not been retro-checked against the 4 locked modules — same category of gap as the Part D retro-check above, deliberately not run now (there's no memory subsystem yet for any module's answer to attach to). open when the memory subsystem exists — retro-check all locked modules' data against question 7 at that point, informed by what's actually being remembered
identity issue agent_duty_grant (the A5 agent-authority passport, built 2026-07-06) has no service-layer methods yetgrantAgentDuty()/revokeAgentDuty()/checkAgentDutyAuthority() do not exist; schema-only this pass. Without the dispatch-time enforcement method, no code path actually reads this table yet. See PROJECT_DECISIONS #22. RE-TRIAGED 2026-07-06: crm (module #5) is now built and its own design maps agent authority directly onto agent_duty_grant as the mechanism (PROJECT_DECISIONS #23) — the "before crm needs to consume this passport as a precedent" half of the original trigger has technically already passed. No functional gap exists today, though: agent_duty_grant's own service methods still don't exist, and CrmService doesn't exist either, so nothing anywhere actually calls into this table yet. Re-deferred with a corrected, still-genuinely-future trigger. open before IdentityService's grantAgentDuty()/checkAgentDutyAuthority() methods are built, OR before CrmService is built and needs to call them — whichever comes first (both are now real, scheduled dependencies, not hypothetical ones)
identity issue agent_duty_grant has no outbound integration/external-tool scope — A5 mentions "scopes which outbound integrations an agent may call," but identity.permission only covers Vrida's own internal module:resource:action space, not external API/integration identifiers (Stripe, QuickBooks, etc.). No integrations module/registry exists yet to FK against. See PROJECT_DECISIONS #22. open when a tool/integration registry exists
identity issue [SECURITY LIMITATION] agent_duty_grant.spend_limit_cents is per-action only — no cumulative/period spend tracking. Provides no protection against volume-based abuse: an agent could take many small actions each individually under the ceiling with no aggregate cap. Deferred to a future ai-schema usage ledger (alongside A6's execution ledger), not solved by this table. See PROJECT_DECISIONS #22. RE-TRIAGED 2026-07-06: ai.agent_usage_period (the usage ledger) is now built (ai module locked 2026-07-06) — the original trigger's technical prerequisite is met. The remaining work is adding agent_duty_grant.spend_ceiling_cents (+ period) and wiring enforcement — a schema change, out of scope for this docs-only pass (see the identical, already-correctly-triggered ai row 129 for the same gap from the ai side). open ai.agent_usage_period now exists (2026-07-06); add spend_ceiling_cents/period to agent_duty_grant before the first cumulative-spend-limited agent runs (mirrors ai row 129 exactly — resolve both together, don't duplicate the fix)
crm issue Merge-reversal 24h window is a service-layer requirement, not schema. customer_merge.metadata snapshot supports reversal in principle, but no CrmService method implements it, and v1's "24h reversal window" policy isn't enforced by anything in this schema (a business rule, not a DB constraint). See PROJECT_DECISIONS #23. open when CrmService is built
crm issue customer.consumer_id forward-ref deferred — no FK, plain UUID, until the consumer module is actually designed/built under the v2 pipeline (verified live: the consumer schema doesn't exist in v2 today). See PROJECT_DECISIONS #23. open when consumer is built
crm issue customer.search_vector (Search FTS touch) deferred — the search module doesn't exist in v2 either (verified live), and no v2-built table anywhere uses a generated tsvector column yet, so there is no Drizzle precedent to follow. See PROJECT_DECISIONS #23. open when the Search module is actually designed/built in v2
crm issue customer_tax_certificate.document_ref forward-ref deferred — the files module doesn't exist in v2 either (verified live). See PROJECT_DECISIONS #23. UPDATED 2026-07-11: files is now schema-locked (PROJECT_DECISIONS #58/#59) — the FK-target table exists. Wiring the real composite FK is folded into the new Files FK-wiring bundle row (below), a dedicated 6-module coordinated reopen, not fixed in this build. open the Files FK-wiring bundle (see the dedicated row below)
crm issue crm service-layer methods entirely absent — no CrmService exists yet; this build is schema-only (Drizzle + migration + tests against raw SQL, no NestJS service). Parallels the same OPEN_ITEMS pattern used for agent_duty_grant's missing service methods (PROJECT_DECISIONS #22). See PROJECT_DECISIONS #23. open when CrmService is built
crm issue Vrida-default segment definitions not seededcustomer_segment_definition supports built-in (NULL tenant_id) segments like vip/at_risk/seasonal/wholesale_like, but none are actually seeded in this pass (schema-only). See PROJECT_DECISIONS #23. open when CrmService is built, seed the Vrida-default segment catalog
crm issue customer_merge_candidate has no dedup protection — found in the post-build Section 4 audit: both an exact-duplicate proposal and a mirrored (customer_a_id, customer_b_id)/(customer_b_id, customer_a_id) pair can be inserted with no conflict, risking review-queue flooding from repeated or agent-driven duplicate proposals. Schema has no unique constraint on the pair (order-independent). See PROJECT_DECISIONS #23. open when CrmService is built — add a normalized-pair uniqueness check (e.g. enforce customer_a_id < customer_b_id + unique index, or a service-layer pre-check)
crm issue customer.tax_exempt/customer.marketing_opt_in maintained-cache columns have no reconciliation trigger or documented sync formula — found in the post-build Section 4 audit; this is the runbook's own named "maintained cache without documented reconciliation formula" bug class. Both columns denormalize state that should derive from customer_tax_certificate/customer_consent but nothing keeps them in sync today. See PROJECT_DECISIONS #23. open when CrmService is built — implement and document the reconciliation formula/trigger
crm issue Found by the 2026-07-07 crm/inventory/pricing v1→v2 erosion audit. customer.tax_exempt_id's removal (v1's own already-deprecated backward-compat column) is documented in schema_docs/crm.md (DR-35) but had never been cross-referenced from OPEN_ITEMS — the audit's adversarial pass confirmed this is a deliberate, correctly-recorded drop, not a silent erosion. Logged here only so OPEN_ITEMS' own completeness sweep finds the trail rather than re-flagging a false gap. See PROJECT_DECISIONS #28 (delta-accounting entry). closed — recorded, not an action item
inventory issue stock_movement.photo_ref forward-ref deferred — plain nullable uuid, no FK; the files module doesn't exist in v2 (verified live via \dn), same treatment as crm.customer.consumer_id. See PROJECT_DECISIONS #24. UPDATED 2026-07-11: files is now schema-locked (PROJECT_DECISIONS #58/#59). Folded into the Files FK-wiring bundle row (below). open the Files FK-wiring bundle (see the dedicated row below)
inventory issue InventoryService (all service-layer methods) does not exist yet — this build is schema-only (Drizzle + migration + tests against raw SQL, no NestJS service), same pattern as agent_duty_grant's and crm's deferred service layers. See PROJECT_DECISIONS #24. open when InventoryService is built
inventory issue pg_trgm fuzzy-search GIN indexes deferred — v1's spec called for gin_trgm_ops indexes on name/SKU columns, but the pg_trgm Postgres extension is not yet enabled in this database (verified live via pg_extension). See PROJECT_DECISIONS #24. open when pg_trgm is enabled / the Search module properly builds fuzzy search
inventory issue item.attributes/item_variant.attributes JSONB per-item_type example shapes still not fully documented — carried forward from v1's own deferred note, narrowed in scope now that shared.plant owns taxonomy/care facts. See PROJECT_DECISIONS #24. open when InventoryService is built
inventory issue stock_adjustment_request.estimated_impact_cents relies on agent_duty_grant.spend_limit_cents, which is per-action only with no cumulative/period trackinginventory is the first module to actually rely on this limit for a real money-moving (balance-sheet-affecting) action; cross-referencing identity's existing security-limitation row rather than duplicating it. See PROJECT_DECISIONS #22, #24. RE-TRIAGED 2026-07-06: same underlying gap as identity row 114 and ai row 129 (agent_duty_grant.spend_limit_cents has no cumulative tracking) — ai.agent_usage_period now exists (2026-07-06), so the technical prerequisite is met; the fix itself (adding spend_ceiling_cents to agent_duty_grant) is identity's schema to change, not inventory's, and remains out of scope for this docs-only pass. open when agent_duty_grant.spend_ceiling_cents is added (tracked primarily at identity row 114 / ai row 129 — this row exists so inventory's own dependency isn't lost)
inventory issue item_merge does not re-point the source item's variant subtree (item_variant, stock, stock_movement_line, lot, barcode, option_type, item_category, item_tag, item_image, kit_component) to the target item — found by independent adversarial verification, a larger-blast-radius analogue of the already-logged crm.customer_merge gap (which doesn't re-point contact/address). Live-reproduced: inserting an item_merge row leaves every one of those tables still pointing at the "merged-away" item's variant tree, with no trigger or constraint catching it. See PROJECT_DECISIONS #24. open when InventoryService.executeMerge() is built — must re-point every listed table or document why not
inventory issue Found by the 2026-07-07 crm/inventory/pricing v1→v2 erosion audit — re-logging a pre-existing v1.5 deferral, not a new v1→v2 loss. stock_movement.movement_type's v1 spec explicitly punted a 'produced' value to "when Production module schema is designed" — still absent from v2's CHECK ('received'/'sold'/'transferred'/'adjusted'/'counted'/'returned'), and the deferral had fallen out of active OPEN_ITEMS tracking until now. See PROJECT_DECISIONS #24, #28. open when the Production module is designed/built — add 'produced' to chk_stock_movement_movement_type
inventory issue Found by the 2026-07-07 crm/inventory/pricing v1→v2 erosion audit — re-logging a pre-existing v1.5 deferral, not a new v1→v2 loss. stock_movement_line.from_site_id/to_site_id (line-level multi-site transfer accounting, distinct from the existing from_location_id/to_location_id within-site columns) was explicitly deferred to v1.5 in v1's own design rationale — still absent from v2, and the deferral had fallen out of active OPEN_ITEMS tracking until now. See PROJECT_DECISIONS #24, #28. open when multi-site transfer accounting at the line level is prioritized
inventory issue Found by the 2026-07-07 crm/inventory/pricing v1→v2 erosion audit — re-logging a pre-existing v1.5 deferral, not a new v1→v2 loss. variant_uom_conversion (a dedicated three-way UoM conversion table beyond item_variant's two scalar factor columns) was explicitly deferred to v1.5 in v1's own design rationale — never built in v1 or v2, and the deferral had fallen out of active OPEN_ITEMS tracking until now. See PROJECT_DECISIONS #24, #28. open when a UoM conversion need arises that the existing purchase_to_stock_factor/sell_to_stock_factor scalar columns can't express
ai issue Cumulative spend-ceiling column on agent_duty_grant — DEFERRED. Do not reopen identity now. agent_usage_period (built this pass) already supplies the aggregation data; only the ceiling column itself is missing. See PROJECT_DECISIONS #25. open before the first cumulative-spend-limited agent runs — add spend_ceiling_cents + period to agent_duty_grant; agent_usage_period already supplies the data
ai issue enrichment_job (batch re-enrichment tracking) — DEFERRED v1.5, re-logged from docs/old/design_rationale/rationale_ai.md DR6 / schema_ai.md's own "Deferred items" section now that ai actually exists in v2. See PROJECT_DECISIONS #25. open when scheduled batch re-enrichment (re-verify all unverified plants) becomes a product requirement
ai issue ai_response_cache (popular query result cache) — DEFERRED to consumer phase, re-logged from v1's DR6 forward-decision (belongs in the ai schema, not consumer_app). See PROJECT_DECISIONS #25. open build at consumer phase
ai issue ai_feedback (thumbs-up/down quality signals) — DEFERRED v1.1, re-logged from v1's DR6 / schema_ai.md deferred-items note. See PROJECT_DECISIONS #25. open HUMAN-SIGNALED, no automatic detection — product/support must flag recurring qualitative complaints about AI output quality with no structured signal to act on; a product-prioritization call, not an auto-firing condition
ai issue anomaly_alert (persisted anomaly detection results) — DEFERRED v1.1, re-logged from v1's DR6 / schema_ai.md deferred-items note. See PROJECT_DECISIONS #25. open when anomaly persistence/acknowledgement workflow is a product requirement
ai issue import_file.file_id forward-ref — plain uuid, no FK; already existed as a concept in v1, re-confirmed now that ai actually exists in v2 (the files schema still doesn't exist, verified live via \dn). See PROJECT_DECISIONS #25. UPDATED 2026-07-11: files is now schema-locked (PROJECT_DECISIONS #58/#59). The Files build's own independent verification also found import_file.file_id is live .notNull(), contradicting its own code comment claiming nullable — a pre-existing ai-module bug, unrelated to the Files build, to fix at a future ai touch alongside the FK wiring itself (folded into the Files FK-wiring bundle row below). open the Files FK-wiring bundle (see the dedicated row below); the NOT NULL/comment mismatch should be resolved in the same pass
ai issue No AIService yet — this build is schema-only (Drizzle + migration + tests against raw SQL, no NestJS service), same pattern as every other module's deferred service layer. See PROJECT_DECISIONS #25. open HOUSEKEEPING, no external trigger — this is the standard placeholder every schema-only module uses for its own not-yet-built service layer (identical pattern: crm row 119, inventory row 124, pricing row 146); it closes when AIService enters a dedicated build phase, not on any external event
ai issue ai_request.tenant_id/agent_identity_id consistency — no CHECK. Every agent belongs to exactly one tenant (identity.agent_identity.tenant_id NOT NULL), so a NULL-tenant ai_request row with agent_identity_id set to a real tenant-scoped agent is nonsensical, and nothing prevents it. Found by post-build adversarial verification; low severity (data-hygiene gap for a not-yet-real platform-level write path, not an RLS leak). No cross-table CHECK possible in Postgres without a trigger. See PROJECT_DECISIONS #25. open when a real platform-level AI call path with agent_identity_id set is built — add a trigger or validate at that write path
ai issue decision_provenance.memory_refs has no tenant-consistency enforcement against ai.agent_memory. A tenant-A row's memory_refs JSONB array could reference a tenant-B memory id with nothing preventing it — found by post-build adversarial verification. Structurally unfixable at the schema level: JSONB array elements cannot carry FK constraints in Postgres. See PROJECT_DECISIONS #25. open when AIService (or a shared cross-module runtime-logging helper) is built — validate memory_refs tenant-scoping at the service layer
pricing issue agent_duty_grant discount/margin-ceiling gapspend_limit_cents/quantity_limit don't map onto "how much may this agent discount by." See PROJECT_DECISIONS #26. open when a markdown-proposing agent actually runs and needs an enforced discount/margin ceiling beyond human review
pricing issue No overlap-prevention EXCLUDE constraint on price_list_assignment for time-windowed customer/group assignments. See PROJECT_DECISIONS #26. open if/when overlapping assignment periods become a real support burden — would need btree_gist
pricing issue Full promotion/campaign header object (usage caps, coupon codes, stacking rules) not builtcampaign_label is a lightweight tag only. See PROJECT_DECISIONS #26. open when coupon-code or usage-capped promotions become a real product requirement
pricing issue tax_treatment's residual gap at the bare item_variant.base_price_cents fallback path (no price_rule matched) — tax treatment is undefined at the schema level for that case. See PROJECT_DECISIONS #26. open when a Tax module is built or item_variant is revisited
pricing issue Hard Contract 1 (pos.sale_line snapshot requirement) not yet satisfiedpos/orders don't exist yet. See PROJECT_DECISIONS #26. open when pos/orders are built — must include resolved_amount_minor_units/charged_amount_minor_units/currency_code/tax_treatment/resolving_price_rule_id/resolved_quantity
pricing issue Hard Contract 2 (shared resolvePrice() spec + golden test vectors with the named minimum coverage bar) not yet satisfied — neither PricingService nor the Dart resolver exist yet. See PROJECT_DECISIONS #26. open when Node PricingService and/or the Dart offline resolver are first built
pricing issue Hard Contract 3 (display rounding is a PricingService-only concern) not yet satisfied. See PROJECT_DECISIONS #26. open when PricingService's display layer is built
pricing issue price_value numeric vs. the project's _cents bigint money convention — a deliberate, now-recorded deviation (mirrors shared's own PK-type deviation, PROJECT_DECISIONS #17). Already resolved by being recorded; tracked here only for the module's full open-item picture. See PROJECT_DECISIONS #26. closed — recorded, not an action item
pricing issue No PricingService yet — schema-only this pass, same pattern as every other module's deferred service layer. See PROJECT_DECISIONS #26. open when PricingService is built
pricing issue Found by the 2026-07-07 crm/inventory/pricing v1→v2 erosion audit. price_change_log.old_value/new_value (jsonb, storing raw rule parameters) cannot reconstruct the actual historical dollar delta for percent_off/amount_off/cost_plus_percent rows the way v1's old_price_cents/new_price_cents scalars could for every rule type — reconstructing a real $-delta requires the historical item_variant.base_price_cents/avg_cost_cents as they stood at that moment, and no such point-in-time snapshot is captured anywhere. Only fixed_price rows are directly $-reportable today. Accepted v1.1 gap, not fixed this pass (reporting-only, not a day-one blocker). See PROJECT_DECISIONS #26, #28. open when pricing-change $-delta reporting is needed, or when a point-in-time base-price/cost snapshot mechanism exists to attach to price_change_log
(all) issue authenticated Postgres role has no schema-level GRANT on pricing, crm, inventory, multi_loc, or shared (only platform has one, likely a bootstrapping artifact) — found by pricing's post-build adversarial verification (2026-07-06), confirmed systemic and pre-existing, not caused by any of these modules' own builds. RLS policies on all 5 schemas are structurally correct (verified empirically) but currently unreachable by any real connection using the authenticated role — permission denied for schema <x> fires before RLS is even evaluated. See PROJECT_DECISIONS #26. closed 2026-07-08GRANT USAGE + blanket table grants issued to authenticated across ALL 15 schemas (not just these 5) via 20260708150000_phase1_rls_wiring.sql (Remediation Phase 1, Item 1). A real, non-superuser authenticated Postgres role now exists (previously the app connected only as postgres superuser, which bypasses RLS entirely — a deeper version of this same gap). RLS is now genuinely reachable and enforced through authenticated — live-proven: a cross-tenant write that used to succeed is now REJECTED (42501), see apps/api/src/database/__tests__/rls-cross-tenant.spec.ts (5/5 passing). See PROJECT_DECISIONS #37. n/a — resolved
identity issue Found by the 2026-07-06 OPEN_ITEMS completeness audit. Every identity table except agent_duty_grant has no updated_at-maintaining trigger wired (no platform.set_updated_at() call) — updated_at is set once at INSERT and never touched again on UPDATE. agent_duty_grant is the first identity table to wire the trigger (see schema_docs/identity.md); the other 34 tables share this gap, which was previously asserted in identity.md as "logged to OPEN_ITEMS.md" but never actually landed as a row until now. Distinct from shared's own version of this gap (row 77) — shared's reference tables never had the trigger by design (rarely mutated post-seed); identity's tenant-scoped operational tables ARE expected to update, so this is a real gap, not an accepted convention. open a mechanical migration adding BEFORE UPDATE ... EXECUTE FUNCTION platform.set_updated_at() to the other 34 identity tables — no schema/column change needed, can be done independent of any other module work
shared issue Found by the 2026-07-06 OPEN_ITEMS completeness audit. docs/database/SCHEMA_CONVENTIONS.md §5 only documents "UUID or smallint" for lookup/reference-table PKs — it does not yet document shared's own natural-key PK pattern (ISO/short codes on currency/country/language/locale/administrative_region/unit_of_measure/climate_zone) as an authorized third option, even though PROJECT_DECISIONS #17 explicitly recorded this as a deliberate, accepted deviation. A pure documentation follow-up — no schema impact. open a dedicated pass amending SCHEMA_CONVENTIONS.md §5 to add natural-key PK as an authorized third option for pure reference-code tables, citing PROJECT_DECISIONS #17
shared issue Found by the 2026-07-06 OPEN_ITEMS completeness audit. Australian and EU shared.climate_zone rows are seeded as simplified, lower-confidence approximations (no official EU hardiness standard exists) per PROJECT_DECISIONS #17 / schema_docs/shared.md — flagged at seed time as needing verification against an authoritative source before being treated as definitive, but no row tracked the follow-up until now. open when an authoritative AU/EU climate-zone source is identified and the seeded rows are checked/corrected against it — likely alongside the first tenant onboarding in either region
shared issue Found by the 2026-07-06 OPEN_ITEMS completeness audit. D5 negative-space finding (module_spec/shared.md §9): at least 46 of the 114 seeded plant rows have zero plant_climate_zone rows (plants may also be missing plant_common_name rows, unconfirmed count). Distinct from row 76 (missing DB indexes for a future plant browser) — this is the underlying DATA-completeness gap itself, not an indexing gap. open when SharedService or a data-quality sweep backfills/flags plants missing plant_climate_zone or plant_common_name coverage
shared issue Found by the 2026-07-06 OPEN_ITEMS completeness audit. Seed-scope narrowing disclosed in PROJECT_DECISIONS #17's "Honest seed-scope note" never got its own row: administrative_region (186 of ~3-4K global ISO 3166-2 subdivisions — comprehensive for US/Canada/Mexico/UK/Australia, representative only for Germany/France/Italy/Spain/Japan), plant (114 seeded, narrower than the originally-discussed larger target), and language (94 of ~184 ISO 639-1 codes, with Hawaiian excluded entirely — no 2-letter ISO 639-1 code exists for it). open when a specific tenant's onboarding needs a region/language/plant outside the current seeded set — expand that table's seed data on demand rather than pre-seeding the full global set speculatively
multi_loc issue Found by the 2026-07-06 OPEN_ITEMS completeness audit. No MultiLocService exists yet — multi_loc is schema-only this pass (Drizzle + migration + tests, no NestJS service), same pattern as every other schema-only module (see crm row 119, inventory row 124, ai row 135, pricing row 146) — but unlike those 4, multi_loc never got its own dedicated row stating the absence itself, per module_spec/multi_loc.md §6. open when MultiLocService is built
multi_loc issue [Part D discovery, found by the 2026-07-06 OPEN_ITEMS completeness audit] D3 (Maintenance) hygiene-sweep findings on site (module_spec/multi_loc.md §9) never got a row: stale operating_hours JSONB never updated after seasonal changes; address drift after a physical site move without closed_at/new-site recorded; orphaned climate_zone_code/measurement_system when country_code is corrected later (the CHECKs don't re-derive on update); duplicate near-identical active sites after a typo'd slug. Structurally analogous to platform's D6/D9 finding (row 93) and shared's D9 finding (row 98), both of which got rows at the time. open when a hygiene/data-quality sweep agent for multi_loc.site is designed — likely alongside MultiLocService
multi_loc issue [Part D discovery, found by the 2026-07-06 OPEN_ITEMS completeness audit] D14 (Lifecycle/perishability) finding on site (module_spec/multi_loc.md §9): status/opened_at/closed_at form a real lifecycle, but nothing currently detects or alerts on transitions — three named unwired signal/action pairs: a site stuck inactive for a long time with no flag; closed_at in the past while status is still active/inactive; opened_at in the future while status='active'. open when a lifecycle-monitoring sweep for multi_loc.site is designed — likely alongside MultiLocService
inventory issue Found by the 2026-07-06 OPEN_ITEMS completeness audit. item_image.file_id carries the identical deferred files-module forward-ref treatment as stock_movement.photo_ref (row 123, plain nullable uuid, no FK) — schema_docs/inventory.md explicitly pairs the two ("carries the identical deferred treatment for the same reason"), but row 123's text only ever named photo_ref. Same underlying gap, same trigger — this row exists so item_image.file_id is literally covered, not just implied. UPDATED 2026-07-11: files is now schema-locked (PROJECT_DECISIONS #58/#59). Folded into the Files FK-wiring bundle row (below), alongside stock_movement.photo_ref. open the Files FK-wiring bundle (see the dedicated row below — resolve both FKs together)
pos issue UPGRADED 2026-07-07 (19-to-9 functionality audit found the original wording understated this) — sale_line_tax's per-jurisdiction tax stacking is an UNRECOVERABLE loss, not a simplification. v1 stored one row per tax jurisdiction (state/county/city/district/special), each with its own rate and amount, explicitly append-only because "tax records are legal records." v2's sale_line carries only two flat scalars, tax_amount_cents (aggregate) and tax_rate (a single combined rate) — confirmed live, no JSONB or side table anywhere in the built pos schema holds a breakdown. Concrete failure: a line taxed by 4 stacked jurisdictions (e.g. CA state 6.00% + county 0.25% + a district add-on 0.25% + a transit assessment 0.75%) collapses to tax_rate=0.0725/tax_amount_cents=725 with no way to recover which portion belongs to which jurisdiction — the combined rate has infinitely many possible decompositions, and jurisdiction names were never captured. Per-jurisdiction remittance reporting and an itemized-by-jurisdiction receipt are both impossible to produce from the schema as built. See PROJECT_DECISIONS #27. open PRE-CUSTOMER DECISION, not open-ended: must be resolved before the first customer transacts in a multi-jurisdiction/stacked-tax location (e.g. any CA site with county/city/special-district tax stacking) — OR when a real Tax module is built, whichever comes first. This is not a someday-nice-to-have; it blocks onboarding a real tenant in a stacked-tax jurisdiction.
pos issue v1 table receipt (delivery tracking) deferred this passpos (module #14, PROJECT_DECISIONS #27). See PROJECT_DECISIONS #27. closednotifications is now schema-locked (2026-07-19, module #29, PROJECT_DECISIONS #73). notification.printed_at is the durable print-execution signal for POS receipts (no delivery_attempt row is created for print, per Ruling 5); actual receipt-delivery-tracking wiring (POS→Notifications integration) remains service-build work, tracked as a new deferred item below rather than reopening this row again.
pos issue v1 table gift_card deferred this passpos (module #14, PROJECT_DECISIONS #27); tender-tagging preserved via payment_method enum value + sale_payment.gift_card_id forward-ref column. See PROJECT_DECISIONS #27. closed — built 2026-07-18 as billing.gift_card (Phase 3 stored-value build, PROJECT_DECISIONS #71; billing placement reverses v1's pos placement per the returns-era "billing should eventually own" row). sale_payment.gift_card_id is now a REAL composite FK (sale_payment_gift_card_tenant_fkey).
pos issue v1 table gift_card_transaction deferred this passpos (module #14, PROJECT_DECISIONS #27). See PROJECT_DECISIONS #27. closed — built 2026-07-18 as billing.gift_card_transaction (append-only ledger, atomic balance-sync trigger, PROJECT_DECISIONS #71).
pos issue v1 table store_credit deferred this passpos (module #14, PROJECT_DECISIONS #27); sale_payment.store_credit_id forward-ref column preserved. See PROJECT_DECISIONS #27. closed — built 2026-07-18 as billing.store_credit_account (v2's recorded name, PROJECT_DECISIONS #71). sale_payment.store_credit_id is now a REAL composite FK (sale_payment_store_credit_tenant_fkey).
pos issue v1 table store_credit_transaction deferred this passpos (module #14, PROJECT_DECISIONS #27). See PROJECT_DECISIONS #27. closed — built 2026-07-18 as billing.store_credit_transaction (PROJECT_DECISIONS #71).
pos issue v1 table layaway_payment deferred this passpos (module #14, PROJECT_DECISIONS #27). See PROJECT_DECISIONS #27. open when installment-payment-plan support is prioritized
pos issue v1 table sale_template deferred this passpos (module #14, PROJECT_DECISIONS #27). See PROJECT_DECISIONS #27. Re-confirmed still absent by the 2026-07-19 gap-validation pass (Flow A2/B2) — \dt pos.* lists exactly 10 tables, no sale_template. open when saved-cart/recurring-order UX is prioritized (register build)
pos issue v1 table sale_template_line deferred this passpos (module #14, PROJECT_DECISIONS #27). See PROJECT_DECISIONS #27. Re-confirmed still absent by the 2026-07-19 gap-validation pass, same as the row above — one combined capability gap, not two independent ones. open when saved-cart/recurring-order UX is prioritized (register build)
pos issue v1 table guarantee deferred this passpos (module #14, PROJECT_DECISIONS #27). Caveat: CROSS_MODULE_CONTRACTS.md's Files section names guarantee.signature_ref -> files.file.id as a live (non-stale) seam that cannot be honored until guarantee itself is built — log this caveat alongside the table's own deferral, do not drop it. See PROJECT_DECISIONS #27. UPDATED 2026-07-11: files is now schema-locked (PROJECT_DECISIONS #58/#59) — the FK target now exists; guarantee itself is still the blocker, unchanged. pos.sale.signature_ref (a real, live column, unlike guarantee.signature_ref which doesn't exist yet) is separately folded into the Files FK-wiring bundle row below. open when guarantee issuance/claim-instance tracking is prioritized (unchanged trigger)
pos issue Rewards/Offers seams stay unhonored this pass — both flagged stale in CROSS_MODULE_CONTRACTS.md; neither module exists in v2. See PROJECT_DECISIONS #27. open when Rewards and/or Offers modules are designed/built in v2
pos issue sale_payment.status enum (Stripe-vocabulary-derived) may need revisiting once Payments' actual Terminal integration is built against a real webhook payload. See PROJECT_DECISIONS #27. open when Payments' Stripe Terminal integration is built
pos issue sale.search_vector (generated tsvector, GIN) dropped during build — the PROPOSE+STOP report listed this column "matching the precedent applied to item/item_variant/customer," but unlike those 3 tables, sale has no free-text field (name/description/sku) to feed it; the only candidate was status, a 3-value enum, which would produce a near-useless index. Logged as a deferred add-later item, not silently dropped. See PROJECT_DECISIONS #27. open when a customer-name-join or receipt-number search becomes a real requirement
pos issue Found by the 2026-07-07 lock-gate verification (Section 6 item 12's new standing rule, first real application). module_spec/pos.md's DR-1 asserts InventoryService.completeSale() MUST detect a would-be-negative available_qty during offline sync replay and create a pos_sync_conflict row (conflict_type='stock_oversell') instead of allowing negative stock or hard-failing — a binding build requirement that was previously referenced as "logged to OPEN_ITEMS" but had no matching row. InventoryService does not exist yet; only the schema's ability to represent the resulting conflict row is built and tested (test B3). See PROJECT_DECISIONS #27, module_spec/pos.md DR-1. open when InventoryService is built — must implement the stock-oversell-to-conflict-row detection described in DR-1, not just create the table
pos issue Found by the 2026-07-07 lock-gate verification. module_spec/pos.md's DR-2 asserts the refund-issuance path MUST verify sum(sale_refund.refunded_amount_minor_units) for a sale, including the refund being created, never exceeds sum(sale_payment.charged_amount_minor_units) for that same sale — not DB-CHECK-enforceable (a cross-row, cross-table aggregate), previously referenced as "logged to OPEN_ITEMS" but had no matching row. See PROJECT_DECISIONS #27, module_spec/pos.md DR-2. open when the refund-issuance service path is built — must implement the sum-never-exceeds-payments check described in DR-2
pos issue Found by the 2026-07-07 lock-gate verification. module_spec/pos.md's DR-3 asserts idempotency_key (on sale/sale_payment/sale_refund) MUST be deterministically derived from client_uuid and passed unchanged to inventory.stock_movement.idempotency_key on every retry — a documented build requirement, not DB-enforced, previously referenced as "logged to OPEN_ITEMS" but had no matching row. See PROJECT_DECISIONS #27, module_spec/pos.md DR-3. open when the offline-sync write path (POSService or equivalent) is built — must derive idempotency_key from client_uuid, never generate it independently
pos issue Found by the 2026-07-07 lock-gate verification. module_spec/pos.md and PROJECT_DECISIONS #27 both state "no POSService exists yet — schema-only this pass," matching every other module's identical pattern (crm row 119, inventory row 124, ai row 135, pricing row 146) — but pos never got its own dedicated row stating the absence itself. Added here to close the gap the 2026-07-06 completeness audit's Bug Class 11 was written to catch, applied to this module's own lock the very next day. open when POSService is built
pos issue chk_sale_payment_no_unvalidated_stored_value_tender + companion chk_sale_payment_stored_value_ref_matches_method are TEMPORARY fail-closed gates, added 2026-07-07 — closes a real live bug the 19-to-9 functionality audit found: sale_payment.payment_method's enum already permitted gift_card/store_credit, and gift_card_id/store_credit_id are unenforced forward-ref columns with no FK target (both stored-value subsystems deferred, rows above), so a tender against a nonexistent/empty/expired card was silently accepted with zero validation. The first CHECK rejects gift_card/store_credit as a payment_method outright. The companion CHECK was added the same pass after independent Section 4 verification found the first CHECK alone left a residual gap in the other direction: nothing stopped a cash/card/check/charge_account/reward row from carrying a fabricated or stale gift_card_id/store_credit_id (live-reproduced before the fix: payment_method='cash' + a random gift_card_id inserted with zero complaint) — the identical silent-acceptance class of bug, just on the forward-ref columns instead of the enum. The companion CHECK forbids a non-stored-value tender from carrying either reference at all. This row exists so both CHECKs' temporary nature is tracked — neither is a permanent design decision. See PROJECT_DECISIONS #27 addendum. closed — resolved 2026-07-18 exactly per this row's own trigger contract (Phase 3 stored-value build, PROJECT_DECISIONS #71): real composite FKs added from gift_card_id/store_credit_id to billing.gift_card/billing.store_credit_account — NOT just a CHECK drop. chk_sale_payment_no_unbacked_tender_type narrowed to payment_method != 'reward' (reward is still genuinely unbacked — see its own new row below); the companion CHECK was STRENGTHENED to full coherence (a stored-value tender now REQUIRES its matching ref); a new chk_sale_payment_stored_value_online_only CHECK enforces v1's online-required boundary. Live-reproduced in both directions.
pos issue Found by the 2026-07-07 19-to-9 functionality audit — column-level erosion, never individually logged. sale's v1 cart hold/resume capability (held_at/hold_expires_at — pause an in-progress cart and auto-expire it) has no v2 equivalent column. sale.status CHECK is ('open','completed','voided') with no held/paused state. See PROJECT_DECISIONS #27 addendum. Re-confirmed by the 2026-07-19 gap-validation pass (A2) as a CONFIRMED GAP, live via \d pos.sale + chk_sale_status's pasted definition — the same underlying capability as the 2 sale_template/sale_template_line rows above; all 3 should be closed together by one register-build reopen, not independently. closed (this specific capability) — resolved 2026-07-20 (Gap-Fill Batch, PROJECT_DECISIONS #74), but NOT via sale-level columns as this row's own text anticipated — via 2 new tables instead: pos.parked_cart/parked_cart_line (status parked/resumed/discarded, a terminal-state guard trigger, expires_at). Parked carts deliberately NEVER reserve stock (a v1 decision, documented in a table comment). This row's own note that all 3 (this + the 2 sale_template rows below) "should be closed together" is disclosed as NOT followed — sale_template/sale_template_line (recurring/saved-order templates, a genuinely different capability from ad-hoc cart hold/resume) remain open, see the 2 rows immediately below.
pos issue Found by the 2026-07-07 19-to-9 functionality audit — column-level erosion, never individually logged. sale's v1 B2B/contractor fields (po_number, job_reference, delivery_date, pickup_window) have no v2 equivalent columns. See PROJECT_DECISIONS #27 addendum. open when B2B/contractor-order support is prioritized for the register build
pos issue Found by the 2026-07-07 19-to-9 functionality audit — column-level erosion, never individually logged. sale's v1 loyalty-points receipt snapshot (points_earned/points_redeemed) has no v2 equivalent column. Distinct from the already-logged Rewards/Offers seam row above (that row covers the Rewards module integration broadly; this row covers the specific receipt-snapshot columns on sale itself). See PROJECT_DECISIONS #27 addendum. open when a Rewards/loyalty module is designed/built in v2
pos issue Found by the 2026-07-07 19-to-9 functionality audit — column-level erosion, never individually logged. sale_line's v1 line_type CHECK ('sale'/'comp'/'sample'/'replacement') plus comp_reason — the distinction between a revenue sale line and a comped/sample/replacement line for accounting purposes — has no v2 equivalent. Every v2 sale_line is implicitly a revenue sale line. See PROJECT_DECISIONS #27 addendum. open when comp/sample/replacement line accounting is prioritized
pos issue Found by the 2026-07-07 19-to-9 functionality audit — column-level erosion, never individually logged. sale_line's v1 manual price-override tracking (price_override, price_override_reason) and line-level discount-approval columns (discount_amount_cents/discount_percent/discount_approved_by) have no v2 equivalent — sale_line's charged_amount_minor_units can differ from resolved_amount_minor_units in the built schema, but nothing records WHY (no override reason, no approving manager) if that difference isn't explained by resolving_price_rule_id. See PROJECT_DECISIONS #27 addendum. open when manual price-override audit tracking is prioritized
pos issue stripe_reader_id PARTIALLY closed 2026-07-07 at the payments module build (module #19, PROJECT_DECISIONS #33) — restored as payments.terminal_reader (a Payments-owned asset, referenced by site_id + an optional register_id, not restored onto pos.register itself — a physical Stripe Terminal reader is inherently a Stripe/Payments concern). printer_config (JSONB) and drawer_config (JSONB) remain genuinely unbuilt — non-Stripe hardware, out of this module's scope. Originally found by the 2026-07-07 19-to-9 functionality audit — column-level erosion, never individually logged. register's v1 hardware-pairing configuration had no v2 equivalent at all before this partial fix. See PROJECT_DECISIONS #27 addendum and #33. open (printer_config/drawer_config only) when non-Stripe till hardware (printer/cash-drawer) pairing is built
pos issue Found by the 2026-07-07 19-to-9 functionality audit — column-level erosion, never individually logged. register_cash_entry.entry_type's v1 CHECK had 6 values ('open'/'mid_count'/'close'/'drop'/'payout'/'no_sale'); v2 narrowed to 3 ('paid_in'/'paid_out'/'count'). Nuance, not a blanket loss: 'open'/'close' are likely redundant with register_session's own opening_float_minor_units/closing_counted_minor_units columns (arguably not lost, just relocated to the session header); 'mid_count' maps to v2's 'count'; 'drop' (cash removed to a safe for security) and 'payout' (cash removed to pay a non-sale expense) — two semantically distinct business reasons in v1 — are BOTH merged into v2's single 'paid_out', relying on the free-text note column to distinguish them instead of a structured value; 'no_sale' (drawer opened without a sale, e.g. to make change) has no v2 equivalent at all and is a genuine dropped audit-trail capability. See PROJECT_DECISIONS #27 addendum. open when structured drop-vs-payout distinction or no-sale-open audit tracking is prioritized
orders dependency order_header.draft_po_id forward-ref deferred — plain nullable uuid, no FK; the Purchasing module doesn't exist in v2. See PROJECT_DECISIONS #29 (Block 4). open when Purchasing is designed/built → add real FK
orders dependency order_payment.stripe_payment_intent_id forward-ref deferred — plain nullable text, no FK; the Payments module doesn't exist in v2. See PROJECT_DECISIONS #29 (Block 4). open when Payments module is built
orders dependency order_payment.charge_account_ref forward-ref deferred — plain nullable text, no FK; the Billing module doesn't exist in v2 (identical treatment to pos.sale_payment.charge_account_ref). See PROJECT_DECISIONS #29 (Block 4). open when Billing module is built
orders dependency order_fulfillment.tracking_number/.carrier forward-refs deferred — plain nullable text, no FK/enum; a future Shipping/Fulfillment/Delivery module doesn't exist yet (v1's own "Module 14" deferral, unchanged; MODULE_INDEX.md already carries a placeholder row for a future delivery module depending on orders+admin). See PROJECT_DECISIONS #29 (Block 4). open when the Delivery/Shipping module is designed/built
orders dependency order_header.search_vector deferred — the Search module doesn't exist anywhere in v2 yet (matches every other module's identical treatment). See PROJECT_DECISIONS #29 (Block 4). open when Search module is designed/built in v2
orders issue order_header.order_number fuzzy/prefix (pg_trgm) search index deferred — the pg_trgm Postgres extension is not yet enabled in this database (verified live via pg_indexes — confirmed absent), same already-logged gap as inventory's own deferred trgm indexes. See PROJECT_DECISIONS #29 (Block 4). open when pg_trgm is enabled
orders issue orders service-layer methods entirely absent — no OrderService exists yet; this build is schema-only (Drizzle + migration + tests against raw SQL, no NestJS service), same pattern as every other product module's deferred service layer (crm row 119, inventory row 124, ai row 139, pricing row 150, pos row 178). See PROJECT_DECISIONS #29. open when OrderService is built
orders issue Cross-consumer oversell — orders is now a second writer against inventory.stock/stock_reservation, alongside pos. Not a new gap: this is the identical genuine-two-writer oversell case pos's own DR-1 already names (see the pos row above, "PROVEN REGRESSION" section) — inventory.pos_sync_conflict's conflict_type enum is generic enough to already represent it without a schema change; InventoryService.completeSale()/reserve() don't exist yet to detect it. Cross-referenced here so orders' own dependency isn't lost, not tracked as a separate fix. See PROJECT_DECISIONS #29 (Block 4), module_spec/orders.md DR-3. open when InventoryService.completeSale()/reserve() implement oversell detection — extend to cover orders as a second writer; resolve together with pos's existing row, don't duplicate
orders issue Agent discount/margin ceiling on order_line.price_override — cross-reference, not a new gap. Same underlying agent_duty_grant.spend_limit_cents no-cumulative-tracking limitation already logged against identity (row 114), pricing (row 142), and ai (row 133). Cross-referenced here so orders' own dependency isn't lost. See PROJECT_DECISIONS #29 (Block 4). open same trigger as the existing identity/pricing/ai rows — resolve once, not per-module
orders issue order_header.attributes JSONB example shape not yet documented — a deliberate deferral (resolved decision at build time, not an oversight): real vertical-attribute needs aren't known yet. See PROJECT_DECISIONS #29 (Block 4, resolved decision 4). open when real vertical-attribute needs are known
orders issue A potential future Files-module seam (signed quote/contract document) registered, not built. v1 never had one either, and the Files module doesn't exist in v2. Registered as a dependency-blocked item per the Design-Phase Integrity rules' Block 4, not silently omitted. See PROJECT_DECISIONS #29 (Block 4, resolved decision 5). UPDATED 2026-07-11: files is now schema-locked (PROJECT_DECISIONS #58/#59) — half the compound trigger has fired. Still blocked on the other half: a signed-document requirement for orders specifically has not been confirmed. open a signed-document requirement is confirmed for orders specifically (the Files half of the trigger is now satisfied)
purchasing dependency Billing A/P write-back columns are placeholdersvendor_invoice.billing_ap_ref/.payment_status_ref/.paid_at and purchase_order.amount_paid_cents carry no FK; PurchasingService writes no value to them. Purchasing owns the invoice DOCUMENT + 3-way match; Billing owns PAYMENT (vendor_invoice.status deliberately has no 'paid' value). See PROJECT_DECISIONS #30. open when the Billing module is built — Billing writes these columns back
receiving dependency shipment_photo_ref forward-ref deferred — plain text, no FK; the Files module doesn't exist in v2 (v1's own forward-ref, unchanged). UPDATED 2026-07-10: this column moved with its table in the Receiving extraction (packages/db/migrations/20260710090000_receiving_extraction.sql) — was purchasing.purchase_receipt.shipment_photo_ref, now receiving.goods_receipt.shipment_photo_ref; the gap itself (no Files module to FK against) is unchanged, only its module/table location. Confirmed live via \d receiving.goods_receipt. See PROJECT_DECISIONS #30. UPDATED 2026-07-11: files is now schema-locked (PROJECT_DECISIONS #58/#59). Folded into the Files FK-wiring bundle row (below). Also newly disclosed by the Files build: this column structurally caps damage documentation to ONE photo per receipt — files.attachment (the new many-to-many join) could resolve this, not fixed in either build. open the Files FK-wiring bundle (see the dedicated row below); the single-photo cap is a separate, not-yet-scheduled follow-up
purchasing issue vendor.search_vector (Search FTS touch) + the (name gin_trgm_ops) fuzzy index deferred — the Search module doesn't exist and pg_trgm is not enabled (same already-logged gap as inventory/orders). search_vector was NOT built (column absent). See PROJECT_DECISIONS #30. open when the Search module is built / when pg_trgm is enabled
purchasing issue PurchasingService (all service-layer methods) does not exist yet — schema-only this pass (Drizzle + migration + tests against raw SQL, no NestJS service), same pattern as every other product module (crm/inventory/ai/pricing/pos/orders). See PROJECT_DECISIONS #30. open when PurchasingService is built
purchasing dependency The reorder→PO autonomy loop is not built — the flagship buy-side agent path (an agent reads inventory.stock.reorder_point/reorder_qty/available_qty, drafts a purchase_order with automation_source='agent' + review_status='pending'). The schema fully supports it; the reorder-detection + drafting service is unbuilt. Note the reorder signal lives on inventory.stock (reorder_point/reorder_qty), NOT stock_adjustment_request (the adjustment gate). See PROJECT_DECISIONS #30. open when the reorder-detection service is built — reads inventory.stock reorder signals and drafts POs (PO SEND stays human-gated)
purchasing dependency AI Invoice OCR draft path is not built — an OCR/document-extraction pipeline that drafts a vendor_invoice from a scanned document (v1's own Group 6 "Future" feature). The schema is ready (vendor_invoice full pack, review_status='pending' gates the human confirm); the OCR pipeline is unbuilt (ai has an import pipeline but not OCR-to-invoice-line extraction). See PROJECT_DECISIONS #30. open when an OCR/document-extraction capability is built
purchasing issue [CROSS-REFERENCE, not a new gap] Cumulative agent spend-ceiling on agent_duty_grant — a reorder agent drafting a PO is the first agent with real money attached, but PO-send is needs_approval ALWAYS (human-gated), so the cumulative ceiling is not yet load-bearing (deferred by decision, PROJECT_DECISIONS #30 resolved decision 2). Same underlying gap already logged at identity row 114 / ai row 133 / pricing row 142 — resolve there, not here. open when a may_act_alone PO-send-under-budget capability is contemplated — add agent_duty_grant.spend_ceiling_cents; tracked at identity row 114 / ai row 133 (do not duplicate the fix)
purchasing issue [CROSS-REFERENCE, not a new gap] Over-receipt / cross-writer stock races — purchasing is now a writer against inventory.stock/stock_movement alongside pos/orders. The InventoryService.receive() idempotency discipline (deterministic idempotency_keyinventory.stock_movement.idempotency_key, which has its own unique index) is the mechanism. Same class as pos/orders' existing InventoryService idempotency rows. See PROJECT_DECISIONS #30. open when InventoryService.receive() is built — must honor the deterministic idempotency key (extend the existing pos/orders discipline, do not duplicate)
purchasing, receiving issue JSONB example shapes not yet documentedvendor.attributes, vendor.blackout_config (v1 had an example), vendor_item.attributes need documented example shapes before they carry real vertical data (Section 4 Item J, same class as orders' attributes GAP). UPDATED 2026-07-10: this row originally also named a 4th column, purchase_receipt.attributes — that table moved to receiving.goods_receipt in the Receiving extraction (packages/db/migrations/20260710090000_receiving_extraction.sql); goods_receipt.attributes carries the identical, still-undocumented gap under its new name, tracked here rather than opening a duplicate receiving-module row for the same unresolved shape. See PROJECT_DECISIONS #30. open when PurchasingService is built or real vertical-attribute needs are known
tax dependency tax_calculation.provider_ref forward-ref deferred — plain nullable text, no FK; the Payments module doesn't exist in v2 (same treatment as pos.sale_payment.stripe_payment_intent_id/order_payment.stripe_payment_intent_id). See PROJECT_DECISIONS #31 (Block 4). open when the Payments module is built
tax issue TaxService does not exist yet — schema-only this pass (Drizzle + migration + tests against raw SQL, no NestJS service); binding future contract (calculate() calls Stripe Tax, writes 1 header + N jurisdiction rows) recorded in module_spec/tax.md §5, not yet implemented. Same pattern as every other product module's deferred service layer (crm row 119, inventory row 124, ai row 139, pricing row 150, pos row 178, orders row 193, purchasing row 201). See PROJECT_DECISIONS #31. open when TaxService is built
tax dependency Nexus/rate-anomaly-detection agent logic is not built — the only legitimate agent surface this module defines (flag a tax_calculation for human review, e.g. a jurisdiction rate dropping to 0% or new unregistered-nexus volume); schema fully supports it (tax:calculation:flag_anomaly permission code, review_status/review_reason/reviewed_by_actor_id review seam), the detection service is unbuilt. Cross-references Admin's nexus/registration CONFIG scope (not duplicated here — tax only flags, Admin configures). See PROJECT_DECISIONS #31, module_spec/tax.md §4/§8. open when a nexus/rate-anomaly-detection service is built
billing dependency ar_payment.stripe_payment_intent_id/ap_payment.stripe_payment_intent_id forward-refs deferred — plain nullable text, no FK; the Payments module doesn't exist in v2 (v1's own forward-ref, unchanged; same treatment as pos/orders' identical columns). See PROJECT_DECISIONS #32 (Block 4). open when the Payments module is built
billing issue BillingService does not exist yet — schema-only this pass (Drizzle + migration + tests against raw SQL, no NestJS service). Same pattern as every other product module's deferred service layer (crm row 119, inventory row 124, ai row 139, pricing row 150, pos row 178, orders row 193, purchasing row 201, tax row 208). See PROJECT_DECISIONS #32. open when BillingService is built
billing dependency The write-back to purchasing.vendor_invoice (billing_ap_ref/payment_status_ref/paid_at) is unwritten — target columns already exist live on purchasing.vendor_invoice (confirmed), but no service exists to write them. See PROJECT_DECISIONS #32 (Block 4). open when BillingService is built
billing dependency The write-back to purchasing.purchase_order.amount_paid_cents is unwritten — target column exists live (confirmed bigint NOT NULL DEFAULT 0), adversarial-caught as a second write-back target distinct from the vendor_invoice triple (one PO can have multiple vendor_payables, so this MUST be a SUM(vendor_payable.paid_amount_cents) rollup, never a naive 1:1 copy). See PROJECT_DECISIONS #31/#32 (Block 4). open when BillingService is built — must maintain as a SUM-rollup, not a direct copy
billing dependency The write-back into pos.sale_payment.charge_account_ref/orders.order_payment.charge_account_ref is unwritten — target columns exist live (text, no FK — a type change to UUID would require reopening pos/orders, deliberately avoided). See PROJECT_DECISIONS #32 (Block 4). open when BillingService is built
billing dependency Collections/dunning agent DRAFT surface is not builtar_account/ar_charge's review seam is ready (review_status/review_reason/reviewed_by_actor_id); no collections-monitoring/dunning-flag service exists yet. See PROJECT_DECISIONS #32, module_spec/billing.md §4. open when a collections-monitoring capability is built
platform issue dbas's exact JSONB element shape is not yet pinned down — the retyped tenant_profile.trading_namedbas column (absorbing admin.tenant_business_profile.dbas per PROJECT_DECISIONS #34) needs a decided element shape (bare string array ["Acme Garden Co"] vs. object array [{"name":"Acme Garden Co","since":"2020"}]) before the reopen migration is written. See PROJECT_DECISIONS #34. resolved — pinned to a bare string array (e.g. ["Acme Garden Co","Rose Garden Nursery"]) before the reopen migration was written; tenant_profile.dbas is now live as jsonb NOT NULL DEFAULT '[]'. See PROJECT_DECISIONS #35. closed — no further action
platform issue tenant_profile.mailing_address's "NULL = same as legal_address" convention is documented-not-enforced — no CHECK/trigger encodes this; it's a service-layer convention only, matching the accepted pattern for multi_loc.site's climate-zone-system gap (PROJECT_DECISIONS #18 Decision D). See PROJECT_DECISIONS #34. open if a service method reading effective mailing address is ever built, encode the NULL-fallback there, not as a DB rule
platform dependency tenant_profile.ein_ref is schema-only — a vault reference column superseding the deprecated tax_id, but no vault-encryption service exists anywhere in the codebase yet (confirmed via grep — zero Vault/Encryption service classes exist in apps/api/src). See PROJECT_DECISIONS #35. open when a vault-encryption service is built
platform dependency tenant_profile.tax_id's eventual DROP is deferred — the column is deprecated in place (column-comment only, no DDL change) now that ein_ref supersedes it; the actual DROP COLUMN is pushed to a separate, later, explicitly-flagged migration. See PROJECT_DECISIONS #35. open blocked on BOTH: the vault-encryption service existing AND the application-code cutover being verified complete (PlatformService/packages/types no longer read/write tax_id)
platform dependency tenant_profile.logo_url's eventual DROP is deferred — the column is deprecated in place (column-comment only, no DDL change) now that admin.tenant_branding.logo_ref is intended to supersede it; the actual DROP COLUMN is pushed to a second, later reopen of platform, timed to Admin's actual v2 build. See PROJECT_DECISIONS #35. open when Admin v2 is designed/built and tenant_branding.logo_ref is live
admin dependency tenant_branding.logo_ref / compliance_document.document_ref forward-refs to the not-yet-built Files module — unchanged from v1, plain nullable text, no FK; already a documented seam in CROSS_MODULE_CONTRACTS.md. See PROJECT_DECISIONS #34/#36. UPDATED 2026-07-11: files is now schema-locked (PROJECT_DECISIONS #58/#59). Folded into the Files FK-wiring bundle row (below). open the Files FK-wiring bundle (see the dedicated row below)
admin dependency integration_config.credentials_ref / webhook_config.secret_ref are vault references with no vault-encryption service existing anywhere yet — the SAME net-new dependency platform.tenant_profile.ein_ref (row 218 above) already depends on, not a second one; resolve once for both, not twice. See PROJECT_DECISIONS #34/#36. open when a vault-encryption service is built (cross-reference row 218 — do not duplicate the fix)
admin dependency integration_config / webhook_config are the real, already-documented FK targets for the not-yet-built Integrations module (integrations.connector.integration_config_id → admin.integration_config; integrations.webhook_delivery.webhook_config_id → admin.webhook_config) — nothing incomplete on Admin's own schema; Admin is the target, not the blocked side. See PROJECT_DECISIONS #34/#36. open when the Integrations module is built, add the FKs from that side
admin issue AdminService does not exist yet — schema-only this pass (Drizzle + migration + tests against raw SQL, no NestJS service), same pattern as every other product module's deferred service layer. See PROJECT_DECISIONS #36. open when AdminService is built
admin issue [FUTURE OPPORTUNITY, not a blocker] purchasing.purchase_order.approval_status (its own bespoke PO-approval column) could someday migrate to route through Admin's shared approval engine instead — Admin's own schema already accepts this traffic today with zero change needed (approval_request.source_module's CHECK already includes 'purchasing'). See PROJECT_DECISIONS #34 §5/#36. open HUMAN DECISION, no auto-trigger — a product choice about which system of record wins for PO approval; resolve if/when purchasing's approval flow is revisited
billing issue Formal customer_invoice/credit_memo, AP payment batches, dunning/collections execution, payment plans, GL posting, and multi-currency remain deferred — all v1's own Billing Module Boundary deferrals, re-confirmed unchanged, not revisited this build. See PROJECT_DECISIONS #32 (Block 4). open when a concrete product requirement names one
payments issue PaymentsService does not exist yet — schema-only this pass (Drizzle + migration + tests against raw SQL, no NestJS service). Same pattern as every other product module's deferred service layer (crm row 119, inventory row 124, ai row 139, pricing row 150, pos row 178, orders row 193, purchasing row 201, tax row 208, billing rows above). Binding future contract recorded in module_spec/payments.md §5/§6. See PROJECT_DECISIONS #33. open when PaymentsService is built
payments dependency The status-writeback to pos.sale_payment.status/orders.order_payment.status/billing.ar_payment.status is unwritten — service-layer only, not FK-enforced (v1's own design, re-confirmed); PaymentsService doesn't exist to write it yet. See PROJECT_DECISIONS #33, module_spec/payments.md §5. open when PaymentsService is built
payments issue payment_method card-expiry alert job/index not built — v1's own deferred item, re-confirmed unchanged: exp_month/exp_year exist, no supporting index yet. See PROJECT_DECISIONS #33. open when a card-expiry alert job is written
payments dependency billing.ap_payment.stripe_payment_intent_id stays permanently unwired — deliberate v1 scope boundary, re-confirmed: Stripe Connect is incoming-only; vendor payments are manual (check/ACH/wire). Not a deferral awaiting a trigger — a confirmed, permanent exclusion. See PROJECT_DECISIONS #33, module_spec/payments.md §10. closed — permanent scope boundary, not awaiting a trigger N/A — re-open only via a deliberate future scope-expansion decision
payments dependency Fraud/anomaly-detection, reconciliation-flagging, and chargeback-evidence-drafting agent logic is not built — the legitimate agent-flagging surfaces this module defines (payments:payment_intent:flag_anomaly, payments:payout:flag_reconciliation_break, payments:dispute:draft_evidence, payments:payment_intent:suggest_retry); schema fully supports it (review_status/review_reason/reviewed_by_actor_id review seam on all 4 FULL tables), the detection services are unbuilt. See PROJECT_DECISIONS #33, module_spec/payments.md §4/§8. open when the respective monitoring/detection services are built
identity issue [DISCOVERED, OUT OF SCOPE] IdentityService.detectSodViolations() has an intermittent test race in identity-governance.spec.ts's SoD lifecycle tests (A2/B1/B3) — found during Remediation Phase 1 (2026-07-08) verification, while running the full apps/api suite dozens of times. Confirmed NOT caused by Phase 1: identity.service.ts has zero diff this pass (git diff empty), and no Phase 1 migration touches identity.sod_rule/identity.sod_violation. Root cause not fully isolated — detectSodViolations() scans the ENTIRE global sod_rule catalog (unscoped by tenant, WHERE is_active = true) on every call rather than filtering to rules relevant to the calling tenant/actor, via a non-atomic check-then-insert (not a real ON CONFLICT upsert) against the sod_violation_tenant_actor_rule_open_unique partial-unique index; symptoms observed: an expected 0-detected re-scan instead detects 1, or an expected freshly-inserted open violation is transiently absent on the very next read. Reproduces roughly 1-in-3 full-suite runs, and disappeared for 5/5 isolated-file reruns immediately after deleting accumulated orphaned sod_violation rows from earlier interactively-aborted test runs — suggesting sensitivity to catalog size/execution-order/timing, not a simple data-hygiene issue alone (cleanup did not fully eliminate it in later full-suite runs). Predates this entire remediation effort (part of Batch B Pass 2, locked 2026-06-28). open needs a dedicated investigation pass — likely fix: scope detectSodViolations()'s rule scan to rules the actor's specific permission set could plausibly match (or wrap the whole detect-then-act sequence in a single transaction with ON CONFLICT DO UPDATE) rather than a non-atomic global scan
platform issue [DISCOVERED, OUT OF SCOPE] admin-tenants.spec.ts's "(B3) limit=1&offset=0 vs offset=1 returns different rows" pagination test has a genuine cross-file concurrency race — found by Remediation Phase 1's own independent adversarial verification pass (2026-07-08, separate agent), reproducing in roughly half of full-suite runs (passed 8/8 in isolation, failed under full-suite parallel worker load). Root cause: this is a live-DB HTTP-integration test with no per-test transaction rollback; it inserts a platform.tenant row and asserts two sequential paginated reads return different rows, but ~19 other spec files concurrently insert into the same shared platform.tenant table in parallel Jest workers with zero cross-file serialization, so a concurrent insert between the test's own two HTTP calls can shift the ORDER BY created_at DESC, id DESC ranking mid-test. IMPORTANT CORRECTION: a same-day fix was applied to PlatformService.listTenants() (apps/api/src/platform/platform.service.ts) adding desc(tenant.id) as a secondary sort key — this closes the same-millisecond-timestamp-tie sub-case (its original motivation) but does not close this broader concurrent-insert-mid-test race; the independent verification pass confirmed the flake still reproduces after that fix. Not caused by any Phase 1 schema/migration change. open needs either per-test transaction isolation (wrap HTTP-integration specs like this one in a rollback), a dedicated non-shared test tenant scope, or serializing platform.tenant-touching spec files (e.g. --runInBand for this subset)
identity issue [DISCOVERED, OUT OF SCOPE] identity-governance.spec.ts's "(D3) revokeSupportAccess → checkSupportAccess returns null" test has a genuine load-sensitive timing race — found by the same independent adversarial verification pass (2026-07-08), reproducing in roughly 1-in-8 full-suite runs (passed 8/8 in isolation). Root cause: grantSupportAccess defaults starts_at to a Node-computed new Date(), while checkSupportAccess gates on Postgres's own now() BETWEEN starts_at AND ends_at — under heavy concurrent load (28 parallel Jest workers hammering local Postgres), clock/scheduling skew can make Postgres's now() transiently lag behind the app-computed starts_at, making the very next checkSupportAccess call incorrectly return null for a grant that should already be active. Not caused by any Phase 1 schema/migration change (identity.service.ts has zero diff this remediation phase). UPDATED 2026-07-08 (tenant-isolation fix pass, PROJECT_DECISIONS #42): a same-day, later investigation measured a static ~51-second clock skew between the local Node process and the Supabase/Postgres dev container (SELECT now() vs. Date.now(), re-measured twice a minute apart, no growth) — not a load-sensitive intermittent race. This is very likely the SAME root cause as this row and the Drizzle-now() row below, just manifesting more/less often depending on which specific test's time margin it happens to exceed; "1-in-8 under load" may really mean "whichever tests have sub-51-second margins, which varies with worker scheduling/ordering." Recommend checking the container clock (not just adding a tolerance buffer) before deciding this is purely an app-level timing-margin issue. open needs either a small tolerance buffer on the starts_at comparison, or computing starts_at via Postgres now() at insert time instead of the Node clock, to eliminate the two-clock skew — AND first check/sync the local Postgres container's clock (see PROJECT_DECISIONS #42), since a static ~51s skew was independently measured the same day
payments dependency Full vendor (Stripe) de-primitivization — deferred. Remediation Phase 2, Item 7 (2026-07-08) planted the minimal-now anchor only: payments.payment_intent.processor (NOT NULL DEFAULT 'stripe', CHECK (processor IN ('stripe'))). The full de-primitivization is explicitly NOT done — it requires: 19 other stripe_* columns across 6 schemas (live-counted, excluding payment_intent's own stripe_payment_intent_id/stripe_charge_id): billing.ap_payment.stripe_payment_intent_id, billing.ar_payment.stripe_payment_intent_id, inventory.item.stripe_tax_code, orders.order_payment.stripe_payment_intent_id, payments.dispute.stripe_dispute_id, payments.payment_method.stripe_payment_method_id, payments.payment_refund.stripe_refund_id, payments.payout.stripe_payout_id, payments.stripe_connect_account.stripe_account_id, payments.stripe_event_dead_letter.stripe_event_id, payments.stripe_event_log.stripe_event_id, payments.terminal_reader.location_stripe_id, payments.terminal_reader.stripe_reader_id, platform.billing_account.stripe_customer_id, platform.billing_account.stripe_default_payment_method_id, platform.promo_code.stripe_coupon_id, platform.subscription.stripe_subscription_id, platform.subscription_invoice.stripe_invoice_id, pos.sale_payment.stripe_payment_intent_id; 2 table renames (candidates TBD at design time — e.g. payments.stripe_connect_account/stripe_event_log/stripe_event_dead_letter are the most Stripe-named table identifiers, but the actual rename targets are a design decision for whoever builds this, not decided here); 1 NOT NULL relaxation (payment_intent.processor itself, once a second processor makes 'stripe'-always untrue for some rows); and widening tax.tax_calculation's own chk_tax_calculation_provider CHECK (currently provider IN ('stripe_tax','manual','exempt'), live-verified) to accept a second tax provider if the new payment processor also supplies its own tax service. 3 citation corrections were made to the originally-drafted plan prose before this phase built anything (inventory.item_variant.stripe_tax_code→ actually on inventory.item; platform.contract.stripe_coupon_id → actually on platform.promo_code; purchasing.vendor_credit_return_line → the real table is purchasing.vendor_return_line, which itself has no stripe-related column at all and was miscited) — all 3 corrections live-verified via information_schema before use. See PROJECT_DECISIONS #38. open before a second payment processor is integrated — do the full de-primitivization in one pass (all 19 columns + both renames + the NOT NULL relaxation + the tax CHECK widening together, not piecemeal)
pos issue [RESOLVED] The anonymous-walk-in-return question — DECIDED = ALLOW. Remediation Phase 3, Item 10 (2026-07-08) relaxed pos.sale_refund_line.sale_line_id to nullable (the no-receipt-refund LINE path) but deliberately left pos.sale_refund.sale_id itself NOT NULL, flagging the fully-anonymous case as a separate undecided go/no-go. Remediation Phase 4 (2026-07-08) closed it: sale_id relaxed to nullable, chk_sale_refund_identification CHECK (sale_id IS NOT NULL OR reason IS NOT NULL) requires a refund to identify itself some way. See PROJECT_DECISIONS #40. closed — see the new row below for the deferred service-layer audit-controls dependency this decision creates N/A — closed
crm dependency Credit-limit service-layer enforcement is unbuiltcrm.customer.credit_limit_cents/.credit_terms (Remediation Phase 3 Item 8, 2026-07-08) are schema-only; nothing yet blocks a charge-account tender from exceeding the customer's credit limit. NULL credit_limit_cents means no credit extended (fail-closed default), but no service checks this at charge time. See PROJECT_DECISIONS #39. open when CrmService/BillingService (or the charge-account tender path) is built — must read crm.customer.credit_limit_cents before authorizing a charge-account tender that would exceed it
admin dependency admin.setting_definition (Remediation Phase 3 Item 13, 2026-07-08) covers admin.tenant_setting ONLY — 4 other free-form config surfaces are deliberately out of scope: admin.integration_config.settings, admin.hardware_device.config, identity.agent_identity.config, and the ai module's own config fields. Each is vendor-shaped or system/agent-managed, not a tenant-facing "setting" in the same sense — not an oversight, a scoping decision. See PROJECT_DECISIONS #39. open if/when one of these 4 surfaces needs its own catalog, design it separately — do not force-fit into setting_definition, whose shape (value_type/default/tenant-editability) is tailored to tenant_setting specifically
admin issue admin.setting_definition is NOT enforced against admin.tenant_setting via FK or trigger — a Postgres CHECK cannot reference another table's columns, and retrofitting a validation trigger onto the already-locked tenant_setting table was deliberately deferred rather than bundled into this catalog add (Remediation Phase 3 Item 13, 2026-07-08). Today nothing stops a tenant_setting row from using a (category, key) pair absent from the catalog, or a value shape mismatched with the catalog's declared value_type. See PROJECT_DECISIONS #39. open when a validation trigger (or service-layer check) enforcing tenant_setting.(category,key) against setting_definition is built
tax issue [RESOLVED SAME-DAY, not left open] tax_calculation_jurisdiction's sign-consistency-with-parent requirement was initially disclosed as an unenforced service-layer contract (Remediation Phase 3 Item 9, 2026-07-08) — independent adversarial verification flagged that this table is INSERT-only (append-only: authenticated has no UPDATE/DELETE grant, trg_tax_calculation_jurisdiction_append_only rejects both anyway), so a BEFORE INSERT trigger could close the gap for real with no UPDATE race, mirroring this same item's own trg_tax_calculation_validate_reversal pattern. trg_tax_calculation_jurisdiction_validate_sign was added same-day — this row exists for the audit trail, not as a live gap. See PROJECT_DECISIONS #39. closed — enforced via trg_tax_calculation_jurisdiction_validate_sign, added same-day post-verification N/A — closed
pos dependency Anonymous walk-in return audit controls are unbuilt — Remediation Phase 4 (2026-07-08) decided ALLOW and schema-enforced only the bare identification requirement (chk_sale_refund_identification: a refund must have a sale_id OR a reason, never neither). The real-world controls a production anonymous-return flow needs — a mandatory, non-empty reason specifically for the anonymous case (today reason merely needs to be non-NULL, not meaningfully descriptive), manager/actor attribution on who authorized it, and an approval step for high-value anonymous refunds — are explicitly a SERVICE-LAYER requirement, not built here. See PROJECT_DECISIONS #40. open when POSService (or the refund-processing flow) is built — must enforce reason-quality, attribution, and value-based approval routing for the sale_id IS NULL path specifically
tax, pos, shared, admin issue Item 17's 4 enum→catalog additions (tender_type, payment_terms, jurisdiction_level, integration_provider) are additive-interim only — the new catalog FK column and the pre-existing CHECK-enum column are NOT kept in sync. Remediation Phase 4 (2026-07-08): each sub-item added a nullable FK column alongside its untouched legacy CHECK-enum column, with no trigger/constraint enforcing agreement between them. Additionally, crm.customer.credit_terms's existing 6-value CHECK-enum (Phase 3) is NOT a clean subset of shared.payment_terms_catalog's 10 seeded codes — 4 catalog codes (cod, prepaid, net_7, 2_10_net_30) have no credit_terms equivalent at all, so the eventual cutover requires a real vocabulary-mapping decision, not a trivial 1:1 rename. See PROJECT_DECISIONS #40. open when a service/UI layer is built to actually consume any of the 4 new catalog FKs, either (a) add a sync trigger/constraint keeping the old enum and new FK in agreement, or (b) fully cut over and drop the old enum column — and specifically decide the credit_termspayment_terms_catalog code mapping before cutting that one over
inventory issue inventory.stock_reconciliation_shell's real join/aggregation logic (drift detection) is unbuilt — Remediation Phase 4, Item 20b (2026-07-08) shipped only a minimal shell view (plain LEFT JOIN, stock.last_movement_id watermark exposed alongside the stock row, zero aggregation math), deliberately deferred per the pre-build correction rather than ship unverified logic. The real reconciliation math (sign-aware sums of stock_movement_line deltas since last_movement_id, compared against on_hand_qty, matching chk_stock_movement_movement_type's own sign vocabulary — received/produced/returned positive, sold/transferred negative, adjusted/counted either) was never built or tested against real transfer data. See PROJECT_DECISIONS #40. open design and build the real drift-detection view/query against a synthetic multi-transfer test scenario first, before trusting it against production data
inventory issue Stock-reservation expiry release has no scheduled job — documented contract only, not DB-enforced. Remediation Phase 4, Item 20c (2026-07-08): the schema (inventory.stock_reservation.status/.expires_at) has fully supported "release expired reservations" since inventory's original 2026-07-06 build, but no periodic job exists to actually transition status='active' rows past their expires_at to status='expired'. UPDATED 2026-07-19: the pg_cron extension is now installed codebase-wide (the notifications build's own Ruling 13, PROJECT_DECISIONS #73) — the prior blocker ("no pg_cron extension available") is resolved. Still open: zero jobs exist yet — job definitions (this sweep, plus notifications' own retry-sweep/campaign-scheduling jobs) are deferred to whichever service build reaches them first, per the same ruling. Contract unchanged: UPDATE inventory.stock_reservation SET status='expired' WHERE status='active' AND expires_at < now(). See PROJECT_DECISIONS #40, #73. open now that pg_cron exists, define this job at the next service-layer build that touches inventory reservations or notifications scheduling
platform dependency platform.legal_entity's "1 primary entity per tenant" invariant is a point-in-time backfill guarantee, not an ongoing one. Remediation Phase 4, Item 15 (2026-07-08) backfilled exactly 1 primary legal_entity row per tenant that existed at build time (1997 tenants) — independently confirmed by both verification lenses that, purely from this remediation effort's own continued test-suite churn on the shared dev DB, 130+ tenants created AFTER the backfill already have zero legal_entity row, and the gap will keep growing since no service-layer code creates a legal_entity row when a new tenant is provisioned. See PROJECT_DECISIONS #40. open when tenant-provisioning code (PlatformService.provisionTenant() or equivalent) is extended to also insert a primary legal_entity row for every new tenant
crm dependency crm.customer.pii_vault_ref (Remediation Phase 4, Item 18, 2026-07-08) is schema-only — shares, does NOT duplicate, the existing platform.tenant_profile.ein_ref vault-encryption-service dependency (see rows 218/222 above). No new vault-service work is separately tracked for this column; resolving row 218 resolves this one too. See PROJECT_DECISIONS #40. open when a vault-encryption service is built (cross-reference row 218 — do not duplicate the fix)
(all schemas) issue [DISCOVERED, OUT OF SCOPE] Every Vrida-wide global-reference catalog table — old and new — grants full INSERT/UPDATE/DELETE to the authenticated role, with no RLS restricting who may mutate them. Found during Remediation Phase 4's independent adversarial verification (2026-07-08, live-reproduced: a plain tenant-scoped authenticated session successfully executed DELETE FROM pos.tender_type_catalog WHERE code='cash'). Confirmed to be a systemic, pre-existing gap in the Postgres role/grant model (no distinct "platform-admin-only, tenant-read-only" role exists) — reproduces identically on shared.currency and admin.setting_definition (both pre-existing, long before Phase 4), not something Phase 4 introduced; Phase 4's 4 new catalogs (pos.tender_type_catalog, shared.payment_terms_catalog, tax.jurisdiction_level_catalog, admin.integration_provider_catalog) simply inherited the existing pattern. See PROJECT_DECISIONS #40. open needs a dedicated pass designing a read-only (or platform-admin-only-write) Postgres role for every global-reference table across every schema — a cross-cutting grant-model change, not fixable per-table
platform dependency PlatformService still runs every method through getAdminDb() (superuser, bypasses RLS) with manual tenant_id filtering in app code — the tenant-scoped methods have not been migrated to tenantDB() (RLS-enforced). Remediation Phase 1 (2026-07-08, PROJECT_DECISIONS #37) built and live-proved the DB-side enforcement this migration depends on — a genuine non-superuser authenticated Postgres role, full RLS policy/GRANT closure across all 15 schemas, and a fix to tenantDB()'s own SET LOCAL bind-parameter syntax bug (previously undetected since the function had zero real call sites) — but deliberately scoped the service-layer cutover itself as separate follow-up (see the existing tenant-context-interceptor row above, which this row narrows to PlatformService specifically). Not a schema or infra gap — the DB-side prerequisite is done; this is pending CODE work only: rewiring the service's tenant-scoped methods to call tenantDB() instead of getAdminDb(), removing the manual tenant_id filters those methods rely on today (RLS becomes the real guard, not app-code discipline), and updating the ~100+ tests currently exercised entirely through the superuser bypass. Behavior-changing, not additive — a misclassification of a method (tenant-scoped vs. genuinely cross-tenant, e.g. new-tenant provisioning or operator/CS tooling) either breaks a legitimate cross-tenant operation or leaves an RLS hole, so a full per-method classification pass is required before any rewrite, not folded into this deferral. open before PlatformService handles real multi-tenant production traffic (i.e., before the runtime/service layer goes live)
identity dependency IdentityService has the same getAdminDb()-bypass gap as PlatformService above — its own code comment already flags the intent ("will switch to tenantDB() for RLS enforcement") but the migration was never executed. Same dependency on Remediation Phase 1's now-complete DB-side enforcement (PROJECT_DECISIONS #37) — the authenticated role, RLS policies/GRANTs, and the tenantDB() SET LOCAL fix are all live and proven (rls-cross-tenant.spec.ts, 5/5 passing); only the service-layer cutover itself remains, deliberately deferred as separate follow-up (see the existing tenant-context-interceptor row above and the PlatformService row directly above, which this row mirrors for IdentityService). Same risk profile: behavior-changing, requires a full per-method classification (tenant-scoped vs. genuinely cross-tenant — e.g. SSO/tenant-membership resolution before a tenant context exists, machine/service-account identity, cross-tenant operator/support-access tooling) before any rewrite; a misclassification either breaks a legitimate cross-tenant path or leaves an RLS hole. open before IdentityService handles real multi-tenant production traffic (i.e., before the runtime/service layer goes live)
platform issue [DISCOVERED 2026-07-08, admin console real-data cutover] platform.tenant holds ~2,283 schema-test fixture rows (names like RLS Test Tenant C (append-only, never cleaned up), Gov Test D3-...) left over from this codebase's own build/test work, mixed in with the ~5 real flagship demo tenants — out of ~2,288 total rows. Confirmed via direct query while wiring the admin console's Dashboard: a raw COUNT(*)/ORDER BY created_at DESC surfaced fixture names directly as "Total customers"/"Recent signups." Mitigated for the Dashboard ONLY via a disclosed 5-UUID allowlist (FLAGSHIP_TENANT_IDS in packages/types/index.ts) — see PROJECT_DECISIONS #41. Every OTHER cross-tenant page (Tenants list, Onboarding pipeline, Support Access, Audit log) still shows the full, real, unscoped table, so the fixture noise is visible there, not hidden — e.g. the Onboarding pipeline page currently lists several *-test-*/Gov Test */RLS Test * rows alongside the 5 real tenants. open HUMAN DECISION, no auto-trigger — a cleanup pass (deleting or otherwise excluding the fixture rows) is destructive and needs explicit sign-off before being done; until then this row stands as the disclosed record of the noise
platform issue [DISCOVERED 2026-07-08] agreement_acceptance.accepted_by_user_id (admin console Contracts tab) and operator_audit_log.operator_user_id (cross-tenant Audit log page) have no name-resolution join, unlike every other actor-ID column wired this pass (tenant_internal_activity.performed_by_user_id and support_access_grant.support_actor_id both resolve to a display name via a join to identity_user). The admin console UI shows a truncated raw UUID for the former, and operator_role (a text column, not a name) for the latter — both disclosed, not bugs, just less polished than the other tabs. See PROJECT_DECISIONS #41. open when getAcceptances()/listOperatorAudit() are extended with an identity_user join, matching the pattern already used by listInternalActivityWithNames()/listSupportAccessGrants()
identity issue [HIGH PRIORITY — DISCOVERED 2026-07-08, unrelated to the admin console work that surfaced it] resolvePermissions()'s Step 1/Step 2/Step 4 queries, and platform.service.ts's resolveEntitlements(), can silently return empty/false for role assignments, group role assignments, permission overrides, and tenant entitlements that genuinely exist and are active. Root cause (isolated via 4 rounds of minimal live-DB reproduction, not guessed): each of these 4 call sites combines a Drizzle-typed bind-parameter condition (eq(actor.id, actorId)) with a raw sql fragment containing an unparameterized now() call inside the same and(...) — e.g. and(eq(roleAssignment.actor_id, actorId), sql\${roleAssignment.starts_at} <= now()`). A role assignment written moments earlier (confirmed correct via raw SQL: right actor, status='active', starts_atin the past,ends_atnull) is invisible to this exact query shape, while the identical row IS found by the same query with the raw fragment removed, by Drizzle's ownlte(col, new Date()), by lte(col, sql`now()`)(no raw fragment + bind param mixed), and by plainpsql. Fails closed (denies rather than over-grants) — a correctness/availability bug, not a privilege-escalation risk, but it means role-based permission checks and entitlement resolution can silently show "no access"/"no entitlements" anywhere in the running application, not just in the 15 identity test cases that happened to surface it (identity-crud.spec.ts, identity-governance.spec.ts, identity-machine.spec.ts). Not yet understood: whether the failure is deterministic on every call, or depends on a given Postgres connection's prepared-statement-cache history (this session's local dev connection has run thousands of ad hoc queries) — not chased further past the point of a solid, actionable reproduction. See PROJECT_DECISIONS #41 for the full reproduction writeup. **UPDATED 2026-07-08 (tenant-isolation fix pass, PROJECT_DECISIONS #42) — diagnosis likely corrected, re-classify before further work.** Two new methods reported as sharing this exact pattern (getSubscription, listAnnouncements) were investigated directly: neither's raw sqlfragment actually contains anow()call, and.toSQL()on both showed correct, properly-numbered bind parameters — no fix applied, there was nothing to fix. Chasing why the *original* 4-site claim wouldn't reproduce either (hand-executing the exactpermission-engine.ts Step 1 query shape against a real inserted row returned the correct result twice), the SAME session's full-suite run surfaced this row's 15 failures again, but this time paired with a decisive new measurement: **a static ~51-second clock skew between the local Node test process and the Supabase/Postgres dev container** (SELECT now()vs.Date.now(), re-measured twice a minute apart, not growing) — and every one of the 21 currently-failing tests (this row's 15 plus the 6 in the row above/checkSupportAccess) uses a Date.now() ± 1000ms-scale time margin, which a 51-second skew fully explains without any Drizzle SQL-composition defect. Not conclusively disproven (a real Drizzle bug could still coexist), but the evidence now favors "local container clock drift" over "Drizzle bind-param-ordering bug." **UPDATED 2026-07-09 (operator identity build, PROJECT_DECISIONS #45) — skew has grown substantially, severity raised.** A direct DB connection (bypassing all test code entirely — postgres()client,SELECT now()vs. Node'snew Date()) measured roughly **8 hours** of skew, not ~51 seconds — the container clock has drifted much further since the ~51s measurement above, most likely from laptop sleep/wake cycles across the days between measurements (a container's clock does not track host wall-time across a host sleep unless explicitly resynced). This confirms the root cause is unchanged (same class of bug, same fix needed) but the blast radius is now much larger than "the 21 currently-failing tests" — any test anywhere in this suite using a sub-8-hour Date.now()-relative time margin is now at risk, not just the previously-catalogued ~21-24. Confirmed, in this same build, that the full apps/api suite still shows exactly the pre-existing 4 affected files (identity-crud, identity-governance, identity-machine, platform-billing) and the same 793/813 pass count as the documented pre-build baseline — so no NEW test has yet crossed an 8-hour margin, but this is fragile and will worsen every time the host sleeps again without a resync. **UPDATED 2026-07-09 (later same day, admin console login debugging session) — confirmed this is no longer just a test-timing issue: the skew now breaks real interactive admin-console login.** After the operator-identity cleanup work above, admin@vrida.appstarted getting "Invalid email or password" in the browser. Root-caused via a decoded JWT:iat/expcomputed on the Supabase Auth (GoTrue) container's clock were2026-07-09T21:49:54Z/22:49:54Z, while the host/Node clock at that same instant was 2026-07-10T05:46:56Z— an ~8-hour gap, consistent with the measurement above (Auth and Postgres share the same Docker Desktop VM, so both drift together). A JWT born "expired" relative to the host forcessupabase.auth.getUser()to refresh on every call;apps/web/admin/proxy.tsandapp/(app)/layout.tsxeach callgetUser()once per navigation, and when both fire close together the second refresh reuses an already-consumed refresh token, failing withAuthApiError: Invalid Refresh Token: Already Used(coderefresh_token_already_used) — surfaced to the user as a login failure. Confirmed NOT a regression from any code change this session (proxy.ts's own pre-existing comment already names this exact error class as a known, previously-considered risk). The user then attempted to fix it by "restarting docker," but a live check disproved that it worked: lsof -nP -iTCP:54321/54322 -sTCP:LISTENshowed both Supabase ports still held bycom.docker.backend, and ps -p 1190 -o pid,lstart,commandshowed that process running continuously since **Sat Jun 20 14:59:21 2026** — i.e. the restart attempt (likely just closing the Docker Desktop window, which only hides it to the menu bar on macOS) never actually terminated the VM-backing process, so its clock was never resynced. Re-measured the Postgres clock afterward and the ~8-hour gap was unchanged, confirming this. **Deferred by the user to be checked again tomorrow — no further action taken this session.** **RESOLVED 2026-07-10 (environment stabilization pass).** Did the genuine full restart this row itself prescribed: a plainosascript … quit left the VM-backing processes (com.docker.backend, com.docker.virtualization) still running, so followed up with pkill -9against everyDocker Desktop/com.docker.*process, then relaunched viaopen -a Docker. Verified via direct container exec: host and supabase_db's date -unow agree to the second, andSELECT now(), now() - clock_timestamp()inside Postgres shows sub-millisecond drift. One side effect from the forceful kill:identity.sod_rule_permission_uniquecame back withPostgresError: index ... contains unexpected zero page at block 5on the next test run — an abrupt-shutdown artifact, not a clock symptom — fixed withREINDEX TABLE identity.sod_rule_permission(a follow-upREINDEX DATABASE postgresfound nothing else damaged). Fullapps/apisuite re-run clean: 40/40 suites, 813/813 tests, zero clock-skew or corruption failures. **If this recurs after a future host sleep/wake cycle,pkill -9+ relaunch (not a menu-bar quit) plus aREINDEX` check is the known-working fix — worth turning into a standing dev-environment doc rather than rediscovering each time.** closed none — resolved; re-open only if the skew or index corruption reappears
identity issue [DISCOVERED 2026-07-08, adversarial verification of the tenant-isolation fix pass] The agent-elevation gate (DR-25) can be sidestepped entirely via role creation, not just the actorGroupId path fixed this pass. createRole/cloneRoleFromTemplate (identity.service.ts) create roles with requires_approval_for_agents=false by default (the schema default) and never set it explicitly; addPermissionToRole/wireBundleToRole never check the flag either. A tenant admin (or a compromised/buggy caller) who creates a fresh role, wires it the same permissions/bundles as an existing gated role, then assigns it directly to an agent actor via assignRole, never trips the gate — because that specific role was never marked as needing it. Judged NOT a regression from the 2026-07-08 fix pass (PROJECT_DECISIONS #42) — it's an inherent property of the flag-based gate design, which has always relied on whoever creates a role to mark it requires_approval_for_agents=true when appropriate, not on any automatic sensitivity detection. See PROJECT_DECISIONS #42. open design question, not a bug fix: should permission-sensitivity (e.g. a role wired to a known-sensitive permission or bundle) force requires_approval_for_agents=true regardless of the role's own flag? Needs a design decision (which permissions/bundles count as "sensitive enough") before any code change — resolve when the agent-authority model (agent_duty_grant et al.) gets its next design pass
platform issue [DISCOVERED 2026-07-08, original code-review pass] tenant-context.middleware.ts extracts x-tenant-id from a raw request header with only UUID-format validation — no ownership/entitlement check that the caller is actually allowed to act as that tenant. Currently harmless: no tenant-facing (non-operator) controller exists yet to consume req.tenantId (confirmed via grep — zero real readers), and the module's own comments disclose this as an intentional placeholder pending JWT-based tenant resolution. A header-trust model would let any caller impersonate any tenant the moment a tenant-facing route is wired. open before the first tenant-facing (non-operator) route is wired — replace the raw-header trust with a real ownership check (JWT claim, session-tenant binding, or equivalent) before any code reads req.tenantId for an authorization decision
platform issue [PARTIALLY ADDRESSED 2026-07-09, senior-review Part 6.17 — PROJECT_DECISIONS #43] apps/api/src/main.ts now has a global AllExceptionsFilter, enableShutdownHooks(), and bootstrap().catch(→exit 1); CORS + setGlobalPrefix are WIRED but env-gated OFF (API_CORS_ORIGINS/API_GLOBAL_PREFIX), and helmet is still NOT installed. Still server-to-server only (the Next.js admin app fetches this API from its own server runtime with a bearer token — no browser origin, no /api prefix; the admin client uses no global prefix, so enabling setGlobalPrefix now would break the wired admin pages). So the gates stay OFF and helmet stays deferred. open before any browser-side caller of this API exists — (1) npm i helmet + app.use(helmet()), (2) set API_CORS_ORIGINS to the known browser origins, (3) if adopting a /api prefix, set API_GLOBAL_PREFIX AND update apps/web/admin/lib/api.ts's base paths in the same change
platform, identity issue [senior-review Part 2 FLAG, 2026-07-09 — PROJECT_DECISIONS #43] The platform.service.ts insert-by-DTO-tenantId methods trust a tenantId FIELD in the DTO rather than deriving it from the authenticated actor. recordInvoice, recordPayment, grantEntitlementOverride, createContract, recordAcceptance, recordUsageSummary, recordOperatorAction, writeInternalActivity (plus identity's addTenantMember/provisionUser/provisionServiceAccount/provisionAgent, which self-declare tenantId) each write a row tagged with a caller-supplied tenant with no proof the caller owns it. NOT a cross-tenant read hole (they create new rows, don't cross-reference a foreign entity — confirmed by the Part 2 adversarial re-derivation), but the whole tenant-isolation guarantee rests on the controller layer passing a correct, authenticated tenantId. open when each write controller is wired — the controller MUST derive tenantId from the authenticated actor/session, never from a request-body field, and these service methods should take it as a trusted param, not re-accept it from the DTO
identity issue [senior-review Part 2 adversarial-verification caveat D1, 2026-07-09 — LOW severity, inert] grantOverride and submitAccessRequest do NOT validate a site-scoped scopeId/requestedScopeId against the caller tenant's own multi_loc.site rows. A caller can store a user_permission_override (or access request) with scope_type='site' and a scope_id pointing at a FOREIGN site. Not exploitable as written: permission-engine.ts Step 4 only applies a site-scoped override when opts.siteId === scope_id AND the actor has a tenant-validated user_site_assignment for that site (which assignUserToSite checks) — a foreign site-id in an override is inert, can never match a legitimately-assigned site. Data-hygiene gap (a nonsensical row can be stored), not a cross-tenant authorization hole. See PROJECT_DECISIONS #43. open when grantOverride/submitAccessRequest are hardened (or when the write-controller layer above is wired) — validate scope_id's multi_loc.site.tenant_id === tenantId for scope_type='site', matching assignUserToSite's own check
identity issue [senior-review Part 6.18 note, 2026-07-09] IdentityController uses a bare @Controller() (empty prefix) and grafts the full path onto each @Get('admin/...') route rather than a controller-level prefix — a stylistic inconsistency with the other controllers (which use @Controller('admin')), noted during the standards pass but deliberately not changed (it works, and reshuffling route prefixes risks breaking the wired admin client paths). See PROJECT_DECISIONS #43. open when the controller layer gets a consistency pass — normalize IdentityController to a controller-level prefix, updating any client base paths in the same change
platform, identity issue [senior-review FLAG, 2026-07-09 — PROJECT_DECISIONS #43] Idempotency gaps on the write methods that will eventually be webhook/retry targets (e.g. recordPaymentFailure/dunning transitions, credit grant/consume) — the money-mutation methods are now transactional and atomic (Part 1) but not all are idempotent under at-least-once delivery. Not fixable meaningfully until there's a real delivery mechanism whose retry semantics define what "the same event twice" means. open before any webhook/retry delivery path is live — add idempotency keys (or dedup on a natural key) to the methods that become delivery targets
platform issue [DISCOVERED 2026-07-08, original code-review pass] 29 as any casts in platform.service.ts, all on write paths not currently wired to any controller (recordInvoice, recordPayment, recoverDunning, processDataLifecycle, writeInternalActivity, and similar updateXxx methods casting a dynamic updates object or an enum-typed DTO field). Not currently exploitable — validator-backed-safe today since nothing calls these methods over HTTP yet — but each suppresses real type-checking on the eventual write path. open as each of these write methods gets its own controller route, replace its as any cast(s) with a real, narrowed type at that time — don't let write-route wiring ship with the cast still in place
identity→approvals dependency identity.access_request converges onto the new approvals module — deferred, not yet built. identity's own single-approver access-request flow stays as-is for now; adopting the shared engine (approvals.approval_workflow/approval_request/approval_step) is a real re-architecture, not a drop-in swap — identity's own docs (DR-22 in identity.md) envisioned a direct workflow_id FK on access_request itself, whereas approvals deliberately uses an opaque source_module/source_type/source_ref polymorphic contract instead (matching tax/billing's own precedent). See PROJECT_DECISIONS #44. open when identity's own service/HTTP layer for access requests is built, OR when a second module needs the same multi-step-approval pattern — whichever comes first
identity→approvals issue identity's global/mixed-scope workflow-template intent is not representable under approval_workflow's blanket tenant_id NOT NULL. Per module_spec/identity_expansion_intent.md Decision 11a, identity envisioned a Vrida-shipped, tenant-nullable global workflow template; approvals.approval_workflow.tenant_id has no nullable path today. See PROJECT_DECISIONS #44. open when identity's access_request convergence (row above) is actually built — decide whether approval_workflow.tenant_id becomes nullable (mirroring platform.announcement's own mixed-scope precedent) or global templates are ruled out of scope
identity→approvals dependency identity.sod_violation converges onto approvals — deferred. SoD violation resolution (waive/enforce) is a natural fit for the new engine's step/decision model, but no convergence work has started; sod_violation.decision_snapshot's own still-undefined jsonb key shape (identity row 89 above) is a separate, still-open half of this same table. See PROJECT_DECISIONS #44. open when identity's compliance-review UI is built
crm→approvals dependency crm.customer_tax_certificate converges onto approvals — deferred. Certificate verification currently has no gate beyond the already-closed active-with-no-verifier CHECK fix (crm row 123 above); routing verification through the shared approval engine instead of a bespoke crm-only review flow is unstarted. See PROJECT_DECISIONS #44. open when CRM's service layer is built
crm→approvals dependency crm.customer_merge_candidate/customer_merge converges onto approvals — deferred. Note: this table's existing review_status quintet is NOT an autonomy seam to be swapped out wholesale — it IS the business approval gate itself (the row's existence gates whether the merge happens). Convergence must decide whether to drop review_status, keep it as a denormalized cache of approval_request.status, or leave it as the system-of-record and never converge. See PROJECT_DECISIONS #44. open when CRM's service layer is built
inventory→approvals dependency inventory.stock_adjustment_request converges onto approvals — deferred. Same review_status-is-the-real-gate caveat as the crm merge row above applies — the existing review seam is the actual approval mechanism, not a candidate for blanket removal. See PROJECT_DECISIONS #44. open when Inventory's service layer is built
inventory→approvals dependency inventory.item_merge_candidate/item_merge converges onto approvals — deferred. See PROJECT_DECISIONS #44. open when Inventory's service layer is built
inventory→approvals dependency inventory.stock_count reconciliation sign-off converges onto approvals — deferred. See PROJECT_DECISIONS #44. open when Inventory's service layer is built
ai→approvals dependency ai.import_record accept/reject converges onto approvals — deferred. See PROJECT_DECISIONS #44. open when AI's import-pipeline service layer is built
pos→approvals dependency pos.sale_refund.approved_by_actor_id converges onto approvals — deferred. See PROJECT_DECISIONS #44. open when POS's service layer is built
purchasing→approvals issue purchasing.purchase_order's own bespoke approval_status/approved_by_actor_id gate — decided NOT to converge onto approvals yet (Srini: leave the redundant gate as-is for now). This updates admin row 225 above's older framing ("could someday migrate to Admin's shared approval engine") — the engine has since moved out of admin entirely into its own approvals module (this build), but the underlying future-opportunity decision is unchanged: approval_request.source_module's CHECK already accepts 'purchasing' with zero schema change needed whenever purchasing chooses to adopt it. See PROJECT_DECISIONS #44 and row 225 above (admin). open when purchasing's service layer is built AND revisited as a human product decision — not an auto-firing trigger
purchasing→approvals dependency purchasing.vendor_invoice approve/dispute/void gate converges onto approvals — deferred. See PROJECT_DECISIONS #44. open when purchasing's service layer is built
purchasing→approvals dependency purchasing.vendor_invoice_match discrepancy-resolution converges onto approvals — deferred. See PROJECT_DECISIONS #44. open when purchasing's service layer is built
purchasing→approvals dependency purchasing.vendor_return RMA-authorization converges onto approvals — deferred. See PROJECT_DECISIONS #44. open when purchasing's service layer is built
approvals dependency approval_delivery is a disclosed, deliberate, TEMPORARY duplication of a slice of the not-yet-built notifications module's own planned "delivery attempts" scope (docs/modules/MODULE_INDEX.md already lists an 11-table/166-col Notifications module as planned). NOT a duplicate of platform.outbox (different audience: outbox = domain-facing state-change events, approval_delivery = human-facing approver notifications). See PROJECT_DECISIONS #44. closednotifications is now schema-locked (2026-07-19, module #29, PROJECT_DECISIONS #73). Decided per Ruling 15: approval_delivery's own send-tracking becomes a thin cache written by NotificationsService, not an independently-maintained duplicate — no schema change on the approvals side. The actual NotificationsService write path is service-build work (see the new deferred-item row below), not built this pass.
approvals issue [PERMANENT, SCHEMA-UNSOLVABLE LIMITATION] An agent-initiated request resolved by a different, human-controlled actor (proxy self-approval) is not schema-detectable. trg_approval_step_no_self_approval catches literal self-approval (same actor as initiator); it cannot detect a human colluding with, or controlling, an agent to rubber-stamp that same agent's own request. No trigger can close this — it is a governance/process control, not a data-integrity one. See PROJECT_DECISIONS #44. open no trigger — disclosed as permanently out of schema's reach; revisit only if a process-level control (e.g. mandatory role separation enforced upstream of the schema) is designed
identity→approvals dependency Agent-as-approver (role 3) enablement is a dedicated future decision, not yet made. Before any workflow may set an agent actor as a valid approver, this requires BOTH a deliberate product/governance decision AND a live-reproduction of trg_approval_step_blocks_agent_approver actually rejecting an agent decision on a blocks_agent_approver=true workflow, re-proven at the actual enablement time against real production data — already done once during this build against synthetic data (see PROJECT_DECISIONS #44), but that proof does not substitute for re-proving it live when the capability is actually turned on. open before any workflow may enable agent-as-approver — requires both the governance decision and the live re-proof against real production data, neither done yet
platform issue No domain wires a state-mutating consumer against the platform.outbox push channel alone. approvals.approval_event deliberately does NOT read from or write to outbox — the two rows above already disclose approval_delivery/approval_event as serving a different audience than outbox; this row is the underlying precondition note: outbox itself remains an event-write-only surface with nothing downstream consuming it. See PROJECT_DECISIONS #44. open this precondition holds from the moment any real consumer is built, until platform.outbox has a real dispatcher — see the row below
platform issue platform.outbox has no dispatcher/consumer anywhere in the codebase — a pre-existing gap, not introduced by the approvals build. Confirmed via grep: outbox has had zero real consumers since Remediation Phase 4 built the table (PROJECT_DECISIONS #40); this build's own approval_delivery/approval_event deliberately did not attempt to fill that gap (see the two rows above). See PROJECT_DECISIONS #44. open when the first real outbox consumer is built
approvals issue approval_step.condition lacks a concrete example JSONB shape in the schema doc. Section 4 self-audit GAP (minor, non-blocking): fix by adding one, e.g. {"amount_cents_gt": 500000}. See PROJECT_DECISIONS #44. open low priority, fix opportunistically — e.g. next time schema_docs/approvals.md is touched
approvals issue No index yet on approval_request.expires_at / approval_token.expires_at for a future escalation/expiry-sweep job. Section 4 self-audit GAP (minor, non-blocking) — matches this codebase's own established "add this index when the alerting query is defined" precedent. See PROJECT_DECISIONS #44. open when an escalation/expiry-sweep job is built
approvals issue No CHECK enforcing step_mode/resolution_mode semantic consistency (resolution_mode is only meaningful when step_mode='parallel') — nothing stops a sequential-step row from also carrying a resolution_mode value today. Section 4 self-audit GAP (minor, non-blocking). See PROJECT_DECISIONS #44. open when real step-creation logic is built and the production shape is known
billing issue billing.ar_adjustment can reach status='posted' while review_status is still 'pending' — no CHECK links the two state machines. Found live-reproduced by the original 81-mechanism sweep's own test suite (test G3), independent of the approvals build — flagged here because approvals is the natural place this eventually gets closed (an agent-proposed write-off routed through a real approval workflow would make review_status derive from the workflow's own resolution, rather than being a free-standing column with no enforced link to status). No prior open row named ar_adjustment existed before this one (confirmed via grep before adding). See PROJECT_DECISIONS #44. open independent of this module — flag to whoever owns billing next; consider wiring ar_adjustment's write-off approval through approvals at that time rather than inventing a bespoke fix
ai→approvals dependency ai.agent_execution has no reviewer-attribution column anywhere for a needs_approval action's resolution. Directly relevant to this module: once role 1 (pre-execution gate) or role 3 (agent-as-approver) is wired for any ai.agent_execution-originated action, approval_step/approval_event becomes the natural place this gets closed — this is a role-1/pre-execution-gate convergence specifically, NOT a license to migrate the 41 other autonomy-seam tables' own post-hoc review-seam pattern onto approvals generally. No prior open row named agent_execution existed for this gap (the one existing mention, row 50 above, is closed and covers the original schema build, not this gap). See PROJECT_DECISIONS #44. open when role 1 or role 3 is wired for any ai.agent_execution-originated action — route the resolution through approval_step/approval_event then, not before
identity issue identity_user.is_platform_user is NOT dropped — deliberately deferred. The 2026-07-09 operator identity build (PROJECT_DECISIONS #45) retargeted every real auth path (AdminAuthGuard, recordLogin, grantSupportAccess, listSupportAccessGrants, getUser, seed scripts) off this column onto identity.operator; the column itself is still present, still readable/writable, and no longer read by any code path. Left in place rather than dropped in the same pass, per this codebase's own "don't drop a column and its retarget in the same migration" discipline — a live column with zero readers is safer to remove separately, after confirming nothing external (a stale report query, an ad hoc script) still depends on it. open after a full-codebase confirmation sweep (grep + a brief production-log/query-log check, if available) finds zero remaining readers of is_platform_user — then DROP COLUMN in its own dedicated migration
identity issue identity.identity_session/identity.identity_access_event still carry actor-attribution rows for the OLD operator identity model — no tracked dependency on the eventual is_platform_user drop. Both tables FK to identity.actor generically (not identity_user specifically), so they already correctly attribute sessions/events to whichever actor (tenant user or, historically, an is_platform_user=true identity_user) performed them — no retarget was needed or done for these two tables this pass. Flagged here only so the eventual is_platform_user column drop (row above) doesn't overlook checking whether any reporting/analytics query joins identity_session/identity_access_event to identity_user.is_platform_user specifically (as opposed to the generic actor_id join) to distinguish "was this a Vrida-staff session" historically. open at the same time as the is_platform_user drop above — confirm no query joins these 2 tables to identity_user.is_platform_user before dropping it
identity issue OperatorService.createOperator() — the recommended production-safe operator creation path — is not built. create-admin-user.ts (dev/local convenience script, retargeted 2026-07-09) is the only creation path today; there is no HTTP-reachable, super_admin-gated way to provision a new Vrida operator in production. Design intent (per the approved design doc): a real service method, gated behind role_code='super_admin', callable only via getAdminDb() — never tenantDB()/authenticated. See PROJECT_DECISIONS #45. open before this codebase needs to onboard a real production operator without direct DB/script access — build OperatorService.createOperator() (or a method on IdentityService) + its HTTP route, gated behind super_admin
platform issue CLOSED 2026-07-10. platform.payment.invoice_id upgraded from a bare FK to composite payment_invoice_id_tenant_fkey (invoice_id, tenant_id) → platform.subscription_invoice(id, tenant_id), using the subscription_invoice_id_tenant_id_unique prerequisite fix #1 (PROJECT_DECISIONS #48) already added. Old bare constraint (payment_invoice_id_subscription_invoice_id_fk) confirmed DROPPED. Migration: packages/db/migrations/20260710070000_headerline_bare_fk_fixes.sql. Pre-migration audit: 3 live rows, 0 cross-tenant mismatches, 0 orphans. Tests: platform-billing.spec.ts Group K (2 tests). See PROJECT_DECISIONS #53 (this closes out the entire 2-batch Header/Line Remediation effort). closed resolved by PROJECT_DECISIONS #53
purchasing issue CLOSED 2026-07-10. purchasing.vendor_invoice_match.vendor_invoice_line_id upgraded from a bare FK to composite vendor_invoice_match_vendor_invoice_line_id_tenant_fkey (vendor_invoice_line_id, tenant_id) → purchasing.vendor_invoice_line(id, tenant_id), using the vendor_invoice_line_id_tenant_id_unique prerequisite added in the FIRST Header/Line Remediation batch's own Purchasing reopen (fix #4, PROJECT_DECISIONS #47) — verified this constraint's actual origin rather than assumed. Old bare constraint (vendor_invoice_match_vendor_invoice_line_id_fkey) confirmed DROPPED. Migration: packages/db/migrations/20260710070000_headerline_bare_fk_fixes.sql. Pre-migration audit: 0 live rows in vendor_invoice_match. Independent verification built a full 2-tenant fixture chain from scratch (vendor → PO → PO-line → receipt → receipt-line, plus vendor_invoice → vendor_invoice_line) to prove the cross-tenant rejection for real, since the table itself had zero rows at build time. Tests: purchasing-schema.spec.ts Section O (2 tests, O1/O2). See PROJECT_DECISIONS #53 (this closes out the entire 2-batch Header/Line Remediation effort). closed resolved by PROJECT_DECISIONS #53
purchasing issue CLOSED 2026-07-10. Superseded by the Receiving extraction, which moved and renamed the target table itself: purchasing.vendor_invoice_match.purchase_receipt_line_idgoods_receipt_line_id, upgraded from a bare FK to composite vendor_invoice_match_goods_receipt_line_tenant_fkey (goods_receipt_line_id, tenant_id) → receiving.goods_receipt_line(id, tenant_id) — confirmed live via \d purchasing.vendor_invoice_match (old bare constraint vendor_invoice_match_purchase_receipt_line_id_fkey gone). The same fix was applied to vendor_return_line.purchase_receipt_line_idgoods_receipt_line_id in the same migration (now composite vendor_return_line_goods_receipt_line_tenant_fkey, also confirmed live) even though that column never had its own dedicated OPEN_ITEMS row — closing it here too rather than leaving an untracked twin. Migration: packages/db/migrations/20260710090000_receiving_extraction.sql. See PROJECT_DECISIONS #53 (this row's origin) and the Receiving extraction build. closed
billing issue CLOSED 2026-07-10. billing.ar_charge.tax_calculation_id upgraded from a bare FK to composite ar_charge_tax_calculation_tenant_fkey (tax_calculation_id, tenant_id) → tax.tax_calculation(id, tenant_id) — a 3rd, addendum fix found bare during fix #2's own independent verification (PROJECT_DECISIONS #51), folded into this same migration as a cheap, zero-risk follow-up rather than a separate reopen. Both prerequisite UNIQUE(id, tenant_id) constraints (on ar_charge and tax.tax_calculation) already existed from fix #2 itself. Old bare constraint (ar_charge_tax_calculation_id_fkey) confirmed DROPPED. Migration: packages/db/migrations/20260710070000_headerline_bare_fk_fixes.sql. Pre-migration audit: 40 rows with populated tax_calculation_id at build time, 0 cross-tenant mismatches. Tests: billing-schema.spec.ts Group N (2 tests). See PROJECT_DECISIONS #53 (this closes out the entire 2-batch Header/Line Remediation effort). closed resolved by PROJECT_DECISIONS #53
purchasing↔inventory dependency CLOSED 2026-07-10 (fix #11 fully; fix #7 for its goods_receipt_line half only). The Receiving extraction (packages/db/migrations/20260710090000_receiving_extraction.sql; design vrida-header-line-remediation-design-2026-07-10.md §4b/§4d + ~/Downloads/vrida-cc-task-2026-07-10-orphan-fix-and-receiving-design.md Part B) moved purchase_receipt/purchase_receipt_line to receiving.goods_receipt/goods_receipt_line and built both fixes against the new schema. Fix #11 (over-receipt tolerance): new trigger trg_goods_receipt_line_check_over_receipt_tolerance (receiving.check_goods_receipt_line_over_receipt_tolerance()), confirmed live, fires BEFORE INSERT OR UPDATE OF accepted_qty (corrected post-independent-verification from an initial OF over_short_qty scoping — see below) — a deliberate, disclosed narrowing from the design doc's literal unscoped "BEFORE INSERT OR UPDATE" wording, so a later unrelated edit to an already-reviewed line can't re-trip the gate and clobber a human review decision. Reads admin.tenant_setting/setting_definition with site-scoped > tenant-wide > catalog-default precedence (all 3 levels live-reproduced, incl. a site override correctly beating a tenant-wide override, and a defensive EXCEPTION handler added after live-reproduction found an unguarded ::numeric cast could crash on a corrupted catalog value); 2 new catalog rows confirmed live (category='receiving', keys over_receipt_tolerance_percent/over_receipt_tolerance_action, the latter CHECK-constrained to 'flag'/'block' via chk_setting_definition_receiving_tolerance_action). action='block' rejects the line; action='flag' (default) proceeds and flips goods_receipt.review_status. Interacts with fix #10's rollup CHECK via a capped write-back (absorbed_qty = LEAST(accepted_qty, ordered_qty − received_qty − invoiced_qty − cancelled_qty); over_short_qty = accepted_qty − absorbed_qty) — live-reproduced both the capped case (a 120-vs-100 over-receipt writes back exactly 100, chk_purchase_order_line_quantity_rollup holds, the flag fires) and that an uncapped write-back attempt genuinely violates the rollup CHECK. STRENGTHENED same-day, post-independent-verification: the first built version left over_short_qty's derivation and the write-back as an external convention for the not-yet-built ReceivingService, not DB-enforced — 2 separate lock-gate verification passes independently found this gap (pasted, attributed, PROJECT_DECISIONS #55). Fixed: the trigger now derives over_short_qty itself and performs the write-back atomically, in the same invocation; the trigger's own UPDATE scope was corrected from OF over_short_qty to OF accepted_qty to match (the derived output isn't the right column to watch — its input is); chk_goods_receipt_line_qty_nonneg widened to also guard over_short_qty >= 0 (defense-in-depth). Fix #7 (movement-line linkage), goods_receipt_line half only: new goods_receipt_line.stock_movement_line_id, composite FK → inventory.stock_movement_line(id, tenant_id) (confirmed live), supersedes the header-grain link (renamed stock_movement_id from inventory_movement_id, kept per this codebase's deprecate-in-place convention, its own FK also upgraded bare→composite). The vendor_return_line.inventory_movement_id half of the original fix #7 was explicitly OUT of scope for this build (vendor returns stay in Purchasing) — genuinely still deferred, see the new row below; do not treat it as closed by this row. closed (fix #11 fully; fix #7 partially — receiving side only) — (residual vendor_return_line half of fix #7 tracked in the new row below)
inventory issue CLOSED 2026-07-10. inventory.stock_movement_line gained UNIQUE(id, tenant_id) (stock_movement_line_id_tenant_id_unique) as part of the Receiving extraction's prerequisite pass — confirmed live via pg_constraint — alongside the same treatment for item_variant/lot/stock_movement (all 4 added in the same migration, all 4 confirmed live). This was the exact prerequisite this row named for Purchasing's fix #7; fix #7 has now consumed it (receiving.goods_receipt_line.stock_movement_line_id's composite FK targets this constraint). Migration: packages/db/migrations/20260710090000_receiving_extraction.sql. See the (now-closed) purchasing↔inventory fix #7/#11 row above. closed
inventory issue [DISCOVERED 2026-07-10, while documenting the header/line remediation reopen — not part of the verifier's own report] inventory.stock_adjustment_batch.updated_at has no maintaining trigger. Confirmed live: SELECT tgname FROM pg_trigger WHERE tgrelid='inventory.stock_adjustment_batch'::regclass AND NOT tgisinternal returns zero rows. Every other soft-delete/mutable table in this module (stock_adjustment_reason, stock_count, stock_reservation, etc.) has a set_updated_at trigger wired via platform.set_updated_at(); this brand-new table's own migration (20260710030000_headerline_inventory_fixes.sql) never adds one, so updated_at is set once at INSERT and never refreshed on UPDATE. Not a deliberate design choice (unlike this module's 5 legitimately-exempt append-only/hard-delete tables) — a genuine, disclosed gap. See PROJECT_DECISIONS #49. open next time stock_adjustment_batch is touched — CREATE TRIGGER set_updated_at BEFORE UPDATE ON inventory.stock_adjustment_batch FOR EACH ROW EXECUTE FUNCTION platform.set_updated_at();, matching every sibling table's pattern
orders issue [DISCOVERED 2026-07-10, header/line remediation fix #6's independent verification] 5 bare (non-composite) FKs remain inside orders itself. order_line.order_id, order_payment.order_id, order_fulfillment.order_idorder_header.id; order_fulfillment_line.order_fulfillment_idorder_fulfillment.id; order_template_line.order_template_idorder_template.id are all plain (non-composite) FKs, and none of order_header/order_fulfillment/order_template currently carries the UNIQUE(id, tenant_id) a composite upgrade would require. This is the identical cross-tenant-write-exposure bug class this entire remediation effort exists to close (a bare FK lets a row point at a same-shaped row belonging to a different tenant; RLS hides it on SELECT but does not block the write) — found sitting inside Orders' own internals, immediately next to the column fix #6 (order_line.sale_line_id) just touched, and not named anywhere in the design doc or any prior PROJECT_DECISIONS entry. See PROJECT_DECISIONS #50 (Orders header/line remediation entry). open a future Orders reopen candidate — add UNIQUE(id, tenant_id) to order_header/order_fulfillment/order_template first, then upgrade all 5 listed FKs to composite (col, tenant_id) → (id, tenant_id) form
identity→multi_loc issue RESOLVED 2026-07-10 (same day, follow-up pass). The one live orphaned row flagged here (identity.user_site_assignment id 4cd177f7-6f3a-42aa-a977-6fe0b34e89e6, site_id 3587ced4-62ba-47a8-b5ba-4cb0ae43ce29) was investigated: its tenant (5312b5df-c9c3-4099-9733-a93de5fc8517) had exactly 5 rows total anywhere in the schema, all boilerplate/system rows, zero business data — isolated dev-seed fixture junk, independently confirmed via a DB-wide tenant-isolation scan (not just the original claim). The row was DELETED (site_id is NOT NULL, nulling wasn't an option). All 3 site_id-shaped columns named in the original bundle — user_site_assignment.site_id, tenant_user.default_site_id, invitation_site_assignment.site_id — are now wired to real composite FKs → multi_loc.site(id, tenant_id), independently verified live-reproduced (same-tenant success / cross-tenant reject / orphan reject, all 3 columns). See PROJECT_DECISIONS #54. This row can be considered closed; the 2 sibling columns NOT part of this specific bundle (platform.tenant.primary_site_id, identity.user_permission_override.scope_id) remain tracked in the row above, still genuinely open. resolved 2026-07-10
identity→multi_loc FK [DISCOVERED 2026-07-10, during PROJECT_DECISIONS #54's own docs pass — not caught by #52's audit, the FK-wiring build, or that build's independent verifier, since none were asked to look beyond the named 3+2 columns] identity.access_request.requested_scope_id (nullable, packages/db/src/schema/identity/events.ts) is a 4th site_id-shaped column pointing at multi_loc.site, still bare — gated only by CHECK access_request_scope_check (requires NOT NULL when requested_scope_type='site'), no FK at all. Same deferred-to-Phase-4 history as its 3 now-resolved siblings, just never named in any prior audit or design doc. Deliberately NOT wired in this pass — logged here rather than silently swept in. open next dedicated pass touching identity.access_request or multi_loc.site FK wiring — cheap to add alongside user_permission_override.scope_id (row above), same shape, same target
receiving issue Scale extension points named at design time, not built (design doc vrida-cc-task-2026-07-10-orphan-fix-and-receiving-design.md §B.10/§B.14 item 1). 5 named future capabilities, each an identified schema extension point, none stubbed: (a) ASN (advance ship notice) — header-level asn_reference/asn_expected_at on goods_receipt; (b) put-away/bin structuring — converging the free-text destination_zone column on goods_receipt_line into a structured inventory_location_id; (c) serial capture — a future goods_receipt_line_serial child table; (d) fuller inspection/QC workflow — the new inspection_status='quarantine' value (added this build) is the minimal seed, not a full QC workflow; (e) blind receiving (hide expected qty from the receiver) — a future tenant setting reusing the admin.tenant_setting/setting_definition pattern fix #11 established this same build, zero schema impact expected. None of the 5 have any schema representation today beyond the quarantine seed — confirmed live (goods_receipt has no asn_* columns; goods_receipt_line has no serial table or structured location FK). open per-item, independently: (a) when ASN/EDI integration is prioritized; (b) when a structured put-away/bin location model is designed; (c) when serial-number-tracked receiving is required; (d) when a full inspection/QC workflow beyond the quarantine flag is prioritized; (e) when blind-receiving is requested — resolve each on its own trigger, do not wait for all 5
purchasing dependency vendor_return_line.inventory_movement_id — the OTHER half of the original fix #7 (movement-line linkage) — explicitly OUT of scope for the Receiving extraction, still genuinely deferred. Fix #7 as originally scoped (vrida-header-line-remediation-design-2026-07-10.md §4b) covered upgrading BOTH purchase_receipt_line.inventory_movement_id and vendor_return_line.inventory_movement_id from header-grain to line-grain composite FKs → inventory.stock_movement_line(id, tenant_id). The Receiving extraction (2026-07-10) closed only the first half — goods_receipt_line (renamed from purchase_receipt_line) gained stock_movement_line_id (see the now-closed purchasing↔inventory fix #7/#11 row above). vendor_return_line.inventory_movement_id itself was deliberately left untouched — vendor returns stay in Purchasing, not moved to Receiving, and upgrading its own header-grain link to line-grain was named as explicitly out of scope (design doc §B.14 item 2). Confirmed live: vendor_return_line.inventory_movement_id is still a plain (non-composite, non-tenant-scoped) FK → inventory.stock_movement(id) (header grain), unchanged by this build. open when Purchasing's own vendor_return line-level movement linkage is prioritized — upgrade vendor_return_line.inventory_movement_id to a line-grain composite FK → inventory.stock_movement_line(id, tenant_id), mirroring goods_receipt_line.stock_movement_line_id's own precedent from this same build
receiving issue inventory.stock_movement.source_module stays 'purchasing' for receiving-originated movements — a disclosed naming asymmetry, not renamed to 'receiving' (design doc §B.14 item 3). chk_stock_movement_source_module's CHECK vocabulary ('pos'/'orders'/'purchasing'/'inventory'/'production'/'system') was NOT widened to add a 'receiving' value when the Receiving extraction moved purchase_receipt/purchase_receipt_line out of Purchasing — a stock movement produced via goods_receipt_line.stock_movement_line_id still tags itself source_module='purchasing', matching the attribution used before the module split existed. Confirmed live: SELECT DISTINCT source_module FROM inventory.stock_movement returns only purchasing/production; the CHECK constraint itself has no 'receiving' value. Deliberate, not an oversight. open when reporting/analytics needs to distinguish receiving-originated stock movements from other purchasing-sourced ones specifically — widen chk_stock_movement_source_module to add 'receiving' and repoint the write path at that time
purchasing issue vendor_invoice_match.match_status's own 3-way-match tolerance/variance mechanism is untouched by fix #11 — a different, still-separately-deferred item (design doc §B.14 item 4). Fix #11 (over-receipt tolerance, closed this build — see the purchasing↔inventory row above) governs ONLY the receiving-time accepted-vs-ordered quantity check on goods_receipt_line. It has no relationship to the separate 3-way-match discrepancy/variance workflow on vendor_invoice_match (match_status CHECK values include quantity_variance/price_variance/over_billed/under_billed/disputed/resolved, plus quantity_variance/price_variance_cents columns) — confirmed live unchanged by this build (no new column, no new CHECK on vendor_invoice_match). If a tolerance_override_reason-style column is ever wanted on the 3-way-match side (documenting WHY a variance was accepted rather than disputed), it is a distinct Purchasing schema change, not a byproduct of fix #11. open if/when a documented tolerance-override reason for 3-way-match variances becomes a real product requirement — add it to purchasing.vendor_invoice_match, not to receiving.goods_receipt_line
receiving issue [DISCOVERED 2026-07-10, this build's own Section 4 audit — independently confirmed live, not just repeated as claimed] goods_receipt_line.stock_movement_line_id and goods_receipt_line.reversal_of_goods_receipt_line_id have no supporting index. Verified directly via SELECT indexname, indexdef FROM pg_indexes WHERE schemaname='receiving' AND tablename='goods_receipt_line': the table carries exactly 8 indexes (goods_receipt_line_pkey; _goods_receipt_id_idx; _id_tenant_id_unique; _inspection_status_idx, partial; _purchase_order_line_id_idx; _receipt_line_number_unique, partial; _tenant_id_idx; _variant_id_idx) — none has stock_movement_line_id or reversal_of_goods_receipt_line_id as a leading column, so either lookup direction ("which goods-receipt line produced this stock-movement line" / "find the reversal of this line") requires a full per-tenant scan. Consistent with this codebase's own precedent of not indexing the header-grain stock_movement_id either (also confirmed unindexed on goods_receipt today) — a known, accepted gap class here, not a new inconsistency, but worth its own named row rather than silently repeating the pattern a 3rd time. open when a query pattern needing either lookup direction is actually built (e.g. a reversal-lookup UI, or a stock-movement-to-receiving-line traceability report) — add CREATE INDEX ON receiving.goods_receipt_line (stock_movement_line_id) / (reversal_of_goods_receipt_line_id) at that time, matching whichever query shape is real
purchasing issue [DISCOVERED 2026-07-10, adversarial lock-gate verification of the Receiving extraction, pasted/attributed in PROJECT_DECISIONS #55] vendor_return_line carries 3 bare (non-composite) FKs into tables that gained their UNIQUE(id, tenant_id) prerequisite in this very migration, but were never upgraded. Confirmed live via pg_constraint on purchasing.vendor_return_line: vendor_return_line_item_variant_id_fkey FOREIGN KEY (item_variant_id) REFERENCES inventory.item_variant(id), vendor_return_line_lot_id_fkey FOREIGN KEY (lot_id) REFERENCES inventory.lot(id), and vendor_return_line_purchase_order_line_id_fkey FOREIGN KEY (purchase_order_line_id) REFERENCES purchasing.purchase_order_line(id) are all still single-column, non-tenant-scoped FKs — even though inventory.item_variant, inventory.lot, and purchasing.purchase_order_line all gained UNIQUE(id, tenant_id) as direct prerequisites of this same Receiving-extraction migration (for receiving.goods_receipt_line's own composite FKs into the same 3 tables). Only the sibling inventory_movement_id bare FK on this same table already had its own tracked row (see the row above) — these 3 were pre-existing, never previously flagged in any OPEN_ITEMS/CROSS_MODULE_CONTRACTS row, and were out of this migration's own named scope (only goods_receipt_line_id's retarget was requested) — disclosed here rather than left silently inconsistent with 2 of this table's own composite-FK siblings (goods_receipt_line_id, vendor_return_id) that already are composite. open when purchasing.vendor_return_line's own cross-tenant FK hygiene is next reopened (natural to bundle with the still-deferred inventory_movement_id row above, since all 4 share the same root cause and the same table) — upgrade all 3 to composite (col, tenant_id) → parent(id, tenant_id), reusing the prerequisites this build already added
offers→crm FK [DISCOVERED 2026-07-11, consumer/rewards/offers build] offers.offer_targeting_rule.segment_definition_id stays a plain (non-composite) FK to crm.customer_segment_definition plus a cross-tenant-integrity trigger, instead of a composite (col, tenant_id) FK — crm.customer_segment_definition itself carries no UNIQUE(id, tenant_id) prerequisite today, so a composite upgrade isn't possible without first reopening crm. See the design doc for the consumer/rewards/offers build. open add the UNIQUE(id, tenant_id) prerequisite to crm.customer_segment_definition (and upgrade this FK to composite) if/when that table needs the constraint for other reasons — do not reopen crm solely for this row
tax↔offers/rewards issue [DISCOVERED 2026-07-11, consumer/rewards/offers build] The tax pre/post-discount basis for offers/rewards redemptions is undecided — tax.tax_calculation has zero concept of a discount, offer, or reward (no column, no seam onto either new module). Needs an explicit human call on whether tax is calculated before or after an offer/reward discount is applied. open HUMAN DECISION, no auto-trigger — resolve before Tax and Offers/Rewards genuinely need to interoperate (e.g. the first redemption that also needs a real tax calculation); no technical event surfaces this
consumer issue [DISCOVERED 2026-07-11, consumer/rewards/offers build] consumer.consumer.email/.phone are display-cache fields with no sync mechanism back from consumer_identifier changes (e.g. a consumer adding or re-verifying an email doesn't propagate to the cached display field) — same bug class as crm.customer.tax_exempt/.marketing_opt_in's already-logged maintained-cache-without-reconciliation gap (crm row above). open reconcile if/when this causes a real support ticket (a consumer or tenant-side user seeing a stale email/phone on a consumer profile)
pos→rewards/offers issue [DISCOVERED 2026-07-11, consumer/rewards/offers build] A stronger structural refund-clawback mechanism is deferred — a future pos reopen adding a write-back trigger on pos.sale_refund itself, modeled on receiving.goods_receipt_line's own write-back-trigger precedent, would make "a refund claws back the reward/offer it earned" DB-enforced rather than service-layer-dependent. The ledger-side math (once a clawback row is written) is already trigger-enforced this build; whether a clawback row actually GETS WRITTEN when a refund happens is still entirely up to whichever service calls it. UPDATE 2026-07-11 (PROJECT_DECISIONS #60): the ledger-side math is now PROPORTIONAL, not just all-or-nothing — a partial reversal (e.g. returning 2 of 5 units) correctly claws back/releases the exact fractional amount, cumulative-capped against the original via a new tracker table in each module, live-reproduced under concurrency. This row's own open point is unchanged by that fix: no automatic trigger from pos.sale_refund exists yet, so whether a (full or partial) clawback row actually gets written when a refund happens is still entirely up to whichever service calls it. open when pos is reopened to add the sale_refund write-back trigger, mirroring receiving.goods_receipt_line's precedent
consumer/rewards/offers issue [DISCOVERED 2026-07-11, consumer/rewards/offers build] ConsumerService/RewardsService/OffersService — HTTP controllers and service layer do not exist yet for any of the 3 new modules. This build is schema-only, same pattern as every other schema-only module (see crm row 119, inventory row 124, pos row 178, receiving's own equivalent, etc.). open when ConsumerService/RewardsService/OffersService are built
consumer issue [DISCOVERED 2026-07-11, consumer/rewards/offers build] consumer.consumer_identifier's superseded_at release/dispute flow — when a recycled identifier (e.g. a reused email address) needs to be released from its first claimant and reassigned to a new consumer — has no built service-layer flow yet; the schema supports marking a row superseded but nothing implements the release/dispute decision logic. open when ConsumerService implements identifier release/dispute handling
offers issue [DISCOVERED 2026-07-11, consumer/rewards/offers build's own lock-gate verification fix pass, PROJECT_DECISIONS #57] offers.offer.max_per_consumer is declared but still unenforced. The same-day fix pass closed offer.max_redemptions and offer_code.max_redemptions (both were live-reproduced as bypassable, now fixed via a maintained redemption_count counter in the same atomic trigger) but deliberately left max_per_consumer untouched — enforcing a per-(offer, consumer) cap correctly under concurrency requires a new counter mechanism (e.g. a dedicated per-consumer redemption-count table, or a SELECT ... FOR UPDATE-locked row) beyond this fix's own evidenced scope; a live COUNT(*) query without such a lock would reintroduce the exact concurrency race Finding 3 (#56) and this same fix pass's MAJOR finding both closed elsewhere. closed 2026-07-19 — re-discovered independently by that day's gap-validation pass (B7) and fixed the same day (gap-fix pass, PROJECT_DECISIONS entry pending): a plain COUNT(*) IS now used, but positioned AFTER the existing row-locking UPDATE offers.offer in check_and_sync_offer_budget() — that lock already serializes every concurrent redemption of one offer, so no new counter table was needed after all. Live-reproduced with a genuine 3-way concurrent race (3 backgrounded psql processes, pg_sleep barrier, max_per_consumer=2) — exactly 2 of 3 committed, 1 correctly rejected, confirming the count-after-lock placement is race-safe and this row's original "do not add a live COUNT query" caution was about an UNLOCKED count, not this one. Slot-freeing rule: a FULLY-reversed redemption frees its slot (via the reversal tracker's own total_reversed_cents = original_discount_amount_cents signal); a partial reversal does not.
files FK The Files FK-wiring bundle — 8 confirmed forward-ref columns across 6 modules, still plain columns, no FK. admin.tenant_branding.logo_ref, admin.compliance_document.document_ref, inventory.stock_movement.photo_ref, inventory.item_image.file_id, receiving.goods_receipt.shipment_photo_ref, pos.sale.signature_ref, crm.customer_tax_certificate.document_ref, ai.import_file.file_id — all deferred, never wired at the Files module's own build (2026-07-11, PROJECT_DECISIONS #58/#59), deliberately: bundling a 6-module coordinated reopen into Files' own first lock would be scope creep, mirroring this codebase's own "header/line remediation" 2-batch precedent (PROJECT_DECISIONS #46-53) and the "multi_loc site_id FK-wiring bundle" follow-up (#54). files.file carries UNIQUE(id, tenant_id) from this build's own day one — the prerequisite every one of these 6 reopens will need is already satisfied. open a dedicated Files FK-wiring bundle reopen (6 modules: admin, inventory ×2, receiving, pos, crm, ai)
files→platform dependency platform.tenant_entitlement needs 2 new keys (storage_soft_limit_bytes, storage_hard_limit_bytes) for the storage-quota tier ceiling, AND limit_value widened from int4 to bigint. tenant_storage_usage (Files) tracks usage only, per DR6 — the limit belongs in Platform's own entitlement table, which doesn't have a storage key yet. Independently confirmed live: tenant_entitlement.limit_value is integer (max ~2.1B), which cannot hold a byte-scale limit (a 10GB tier alone is already 5× over that ceiling) — both the 2 new keys and the width fix must land together in the same Platform reopen, or the deferred fix lands broken. open when the storage-quota tier ceiling is actually enforced — reopen platform for both the keys and the width fix together
files issue file_access_grant.grantee_user_id/.granted_by_user_id retarget candidate to identity.actor, deliberately deferred. Same reasoning as file.uploaded_by_actor_id's own retarget (this build's own scope discipline kept file_access_grant otherwise untouched from v1 verbatim) — not applied this pass. open if/when file_access_grant is next reopened for an unrelated reason, bundle this retarget in
files issue The orphan-reconciliation sweep's "R2 object with no matching files.file row" direction has zero schema representation — a required service-layer job, not yet built. The pendinguploadedready/failed/deleted lifecycle + pending_expires_at cover the schema half (a file row that never got confirmed); nothing today sweeps R2 itself for orphaned objects with no row at all. open when FilesService (or a dedicated reconciliation worker) is built
files issue tenant_storage_usage quota-recalculation frequency is undecided — an ops/service-layer decision, not a schema gap. The cached counter is expected to drift between recalculation runs (an R2 upload's confirmation is an external event, not a same-transaction Postgres one, so no trigger can keep it exact) — module_spec/files.md §8 discloses this as inherent, not a bug. open when the recalculation job is actually scheduled — pick a cadence and document it here
files issue Semantic search activation (the embedding-backfill pass) — no trigger date set. document_chunk.embedding (vector(1024), Amazon Titan Text Embeddings V2) is built and left NULL; FTS via search_vector works today with zero embeddings. Activating semantic search means backfilling embeddings from already-stored document_chunk.text — never a re-OCR of the source file, even if the embedding model changes later. open when semantic/similarity search over documents becomes a prioritized product feature
files issue [CLOSED 2026-07-17, agents-v2/v3 build Phase 6] agent_reader (NOLOGIN NOINHERIT, matching consumer_authenticated's own shape) now exists, with GRANT USAGE/SELECT on files.document_chunk/document_index plus 2 NEW, dedicated agent_reader-scoped RLS policies (document_chunk_agent_reader_select/document_index_agent_reader_select) — a real finding from this build: the pre-existing authenticated-scoped policies on those 2 tables do NOT extend to a separate role in Postgres (roles = {authenticated}, confirmed live via pg_policies), so GRANT alone would have left every agent_reader query silently returning zero rows. All 4 guards (tenant-scoped read, cross-tenant isolation, write rejection, EXECUTE on the signals functions) live-reproduced. See packages/db/migrations/20260717000001_agent_reader_role.sql, PROJECT_DECISIONS #67. closed
ai issue [CLOSED 2026-07-17, agents-v2/v3 build Phase 6] An ESLint no-restricted-imports + no-restricted-syntax rule now bans adminDb/getAdminDb/tenantDB imports and raw set_config(...) calls under apps/api/src/agents/** (excluding __tests__) — verified firing via a temporary probe file (4/4 violations caught; the __tests__-exempted probe and an unrelated existing file both passed clean). See apps/api/eslint.config.mjs, PROJECT_DECISIONS #67. closed
ai issue agent_reader/agentReaderDB() (packages/db/src/client.ts) have ZERO real call sites today — no AgentsService/FilesService exists yet to adopt them (mirrors tenantDB()'s own pre-Remediation-Phase-1 history: built, verified, zero call sites until a real consumer arrived). The role's narrower GRANT boundary is provably correct in isolation but structurally inert until agent-execution read code actually exists and uses agentReaderDB() instead of tenantDB()/adminDb — the ESLint rule above only fires once files exist under src/agents/** to lint. open when AgentsService (or any real agent-execution read path touching files.document_chunk/document_index or the signals.*_as_of() functions) is actually built — confirm it uses agentReaderDB(), not tenantDB()/adminDb
files issue Whether some consumer_id-tagged uploads (e.g. an ID/age-verification photo) need vault-style handling instead of standard files.file row storage — an open, undecided question, not silently assumed resolved. This codebase already has a real precedent for exactly this class of sensitive-PII storage (crm.customer.pii_vault_ref, platform.tenant_profile.ein_ref) that this build does not invoke or rule out for consumer-tagged files. open HUMAN DECISION, no auto-trigger — resolve if/when a consumer-facing upload flow handling sensitive personal documents is actually designed
files issue FilesService does not exist yet — schema-only this pass, same pattern as every other schema-only module (see crm row 119, inventory row 124, pos row 178, consumer/rewards/offers row 308). open when FilesService is built
files issue File versioning was explicitly resisted this build, not overlooked — no version-history table exists for any files.* object. Revisit only if a real compliance-document/contract version-history requirement appears; do not build speculatively. open if/when a genuine version-history requirement for a specific document type is confirmed
returns issue [DISCOVERED 2026-07-11, returns module build, PROJECT_DECISIONS #61] billing should eventually own a store_credit_account/store_credit_transaction pair — return_resolution.store_credit_reference is a loose text/uuid reference today, no FK, since no spendable balance table exists anywhere in this codebase yet (v1 had 4 fully-specified deferred tables for this with an explicit rationale that gift-card and store-credit must never be merged). closed — built 2026-07-18 exactly as this row envisioned (Phase 3 stored-value build, PROJECT_DECISIONS #71): billing.store_credit_account/store_credit_transaction exist; return_resolution gained store_credit_transaction_id (real composite FK) with a presence CHECK for store_credit/warranty_credit resolutions, mirroring refund's own enforced pointer; store_credit_reference deprecated in place (0 non-NULL rows at deprecation). The never-merge rationale was honored — two instruments, two ledgers, one shared pattern.
returns issue [DISCOVERED 2026-07-11, returns module build, PROJECT_DECISIONS #61] A cached customer-return-velocity/fraud-signal counter is deferred — return_authorization.risk_score is computed from live raw joins each time, no dedicated crm/reporting fraud-analytics surface exists yet. open when crm/reporting owns a real fraud-analytics surface
returns issue [DISCOVERED 2026-07-11, returns module build, PROJECT_DECISIONS #61] warranty.signature_ref is a deferred forward-ref to files.file.id, folded into the existing Files FK-wiring bundle (the same bundle 8 other modules' forward-ref columns are already waiting on) — not a new gap. open when the Files FK-wiring bundle follow-up is executed
returns issue [DISCOVERED 2026-07-11, returns module build, PROJECT_DECISIONS #61] ReturnsService does not exist yet — schema-only this pass, same pattern as every other schema-only module. open when ReturnsService is built
returns issue [DISCOVERED 2026-07-11, returns module build, PROJECT_DECISIONS #61] return_source_line_tracker rows are never soft-deleted or cleaned up — if a pos.sale_line/orders.order_line were ever hard-purged (it won't be — both are append-only/soft-delete-only) the tracker would orphan. Theoretical, not practical, concern, disclosed by the design proposal itself. open N/A — purely theoretical given append-only/soft-delete-only source tables
returns issue [DISCOVERED 2026-07-11, returns lock-gate verification, PROJECT_DECISIONS #61 addendum] return_authorization_line's per-line money-derivation formulas (allocated_discount_cents, effective_unit_price_cents, eligible_refund_cents, restocking-fee arithmetic) are DB-unenforced beyond non-negativity and the aggregate ceiling against the source line — live-reproduced: a wrong-but-in-bounds combination (e.g. effective_unit_price_cents off by 3000x) is accepted today. Matches the same convention already on pos.sale_line/orders.order_line (neither has a derivation CHECK either) — not a returns-specific regression, but must be implemented and unit-tested correctly in the service layer since the DB will not catch a formula error. open when ReturnsService is built — must correctly implement and test the discount-allocation/restocking-fee formulas
ai FK [CLOSED 2026-07-15, agents-v2/v3 build Phase 5] ai.routing_policy.workload_class_id is now a real FK → agents.workload_class(id) — wired the same day Phase 5 landed the target table, pre-migration orphan audit found 0 violating rows. closed
semantics FK [DISCOVERED 2026-07-14, agents-v2/v3 build Phase 3, PROJECT_DECISIONS #64] semantics.tenant_goal_binding.module_id/.site_id and semantics.tenant_constraint_binding.module_id/.site_id are bare uuid, no FK — v3's own literal SQL declares them plain, with no REFERENCES clause specified. Disclosed as a genuine judgment call, not an oversight: platform.module_catalog already exists (unlike ai.routing_policy.workload_class_id's row above, whose target genuinely doesn't exist yet), but the design of record does not specify wiring module_id to it, and no multi_loc.site composite-FK prerequisite check was run for site_id either. Left exactly as specified rather than over-interpreted. open if a future reopen decides these SHOULD be real FKs — module_idplatform.module_catalog(id) is immediately wireable today; site_idmulti_loc.site(id, tenant_id) needs the same composite-FK treatment already applied to identity.user_site_assignment.site_id (PROJECT_DECISIONS #54)
semantics issue [DISCOVERED 2026-07-14, agents-v2/v3 build Phase 3, independent lock-gate verification, PROJECT_DECISIONS #64 addendum] semantics.dimension_definition and semantics.attribution_model_definition both carry a mutable lifecycle_status using the identical vocabulary as metric_definition/goal_definition/constraint_definition, but neither has created_at/updated_at or a maintaining trigger — the same asymmetry class the phase's own Section 4 self-audit found and fixed for tenant_goal_binding/tenant_constraint_binding vs. tenant_metric_binding, but not extended to this second, parallel instance. Traced to the design of record itself (v3 §I4's literal SQL for dimension_definition, and v3's attribution_model_definition spec, both omit these columns) — a design-level gap the build faithfully followed, not a build-introduced defect. No audit trail exists today for lifecycle transitions on these 2 catalog tables. open if a future reopen of semantics decides these need an audit trail — add created_at/updated_at + the shared platform.set_updated_at() trigger, matching every sibling *_definition table's own shape
signals FK [CLOSED 2026-07-15, agents-v2/v3 build Phase 5] signals.outcome_observation.agent_action_id and signals.outcome_authority.agent_action_id are now real composite FKs → agents.agent_action(id, tenant_id) — wired the same day Phase 5 landed the target table; pre-migration orphan audit found 0 violating rows in either table. Phase 4's own pre-existing signals-schema.spec.ts fixtures (5 placeholder agent_action_id literals) were retrofitted with a real minimal fixture chain (identity.actoragent_identitydecision_context_snapshotai.agent_executionagents.agent_decisionagents.agent_action) rather than left broken. closed
signals FK [CLOSED 2026-07-17, agents-v2/v3 build Phase 6] GRANT EXECUTE on signals.get_feature_as_of()/get_forecast_as_of()/get_anomaly_score_as_of() now wired to agent_reader (all 3, (uuid, uuid, timestamptz, timestamptz) signature). Live-reproduced: a call via agentReaderDB() succeeds (no permission error); no GRANT USAGE ON SCHEMA platform needed for the caller since all 3 functions are SECURITY DEFINER owned by signals_function_owner, which already holds that grant to call platform.current_tenant_id() internally. See packages/db/migrations/20260717000001_agent_reader_role.sql, PROJECT_DECISIONS #67. closed
agents issue [DISCOVERED 2026-07-15, agents-v2/v3 build Phase 5, Section 4 self-audit item K] 12 tenant-scoped tables have no tenant_id-LEADING index: agent_eval_case, agent_event_log, agent_shadow_decision, agent_thread, decision_context_feature/_forecast/_knowledge_source/_metric/_policy, evidence_retention_policy, kill_switch_event, rollback_execution. RLS correctness is unaffected (policies work regardless of index presence); each table's natural query path is via its own FK-indexed parent (snapshot_id, eval_run_id, etc.), not a direct tenant-wide scan — a low-severity GAP, not fixed this pass. open if a real query pattern needing a direct tenant-wide scan on any of these 12 tables materializes, add the supporting index then
agents issue [DISCOVERED 2026-07-15, agents-v2/v3 build Phase 5, live guard-reproduction] validate_skill_activation()'s original draft used record-typed PL/pgSQL variables and IS NOT NULL checks directly as IF conditions — empirically confirmed to be UNRELIABLE (a genuinely non-null, fully-populated record evaluated as if NULL in that specific boolean context), silently swallowing the entire A8 Rule 4 spend-ceiling check. Fixed by rewriting to scalar variables, checking each scalar's own NULL-ness directly — not logged as an open item, already fixed in the migration and live DB, listed here only as a documented PL/pgSQL pitfall for future trigger-writing in this codebase. closed — (informational: avoid record-typed IS NOT NULL as a direct IF condition in any future PL/pgSQL function; check a specific NOT-NULL field, or use scalar variables)
agents issue [DISCOVERED 2026-07-15, agents-v2/v3 build Phase 5, independent lock-gate verification, Item M — RECLASSIFIED 2026-07-17 from NOTE to PROHIBITION, per this codebase's own Deferred-Safety Principle: when a safety control is deferred, the hazard it guards must be simultaneously prohibited] Autonomy level is advisory — PROHIBITION until structurally bound. agents.agent_autonomy_profile.current_mode — the column that declares how autonomous an agent is permitted to be — has no structural connection to any enforcement path. It is a label. Nothing in the database prevents an agent whose profile says draft_only from committing an autonomous write.

Not currently exploitable: no agent runtime, no AgentsService, no code path reads current_mode. Risk is zero until the first agent executes.

Not the only gate: identity.agent_duty_grant.authority_level (draft_only / needs_approval / may_act_alone) IS enforced via the approvals seam; agents.agent_tool_grant gates tool access; skill_certification gates deployment activation; allowed_mutating_module_id gates the saga boundary; the kill switch gates task claim. An agent with a lying current_mode still cannot call an ungranted tool, mutate two modules autonomously, or self-approve a financial action. This is a redundancy gap, not an unguarded crown jewel.

Why it was NOT fixed in the agents-v2/v3 build: the correct binding is genuinely undetermined. current_mode could plausibly derive, override, cap, or be cross-checked against agent_duty_grant.authority_level — and the grain is open (per-agent? per-agent-per-skill? per-domain?). Designing that binding against an imagined runtime risks enforcing the wrong invariant, correctly and permanently. It must be designed against a real execution path.

PROHIBITION — binding on whoever builds the agent runtime: No agent may execute in production until agent_autonomy_profile.current_mode is STRUCTURALLY bound to the enforcement path, with the binding LIVE-REPRODUCED: an agent whose profile says draft_only must be structurally UNABLE to commit an autonomous write. This is the FIRST task of the agent-runtime build, not a later hardening pass. Shipping an executor without it is prohibited.

Verification required at revival: live-reproduce, git-stash A/B — an agent at draft_only attempts an autonomous write and is REJECTED by a database mechanism (not a service-layer check). Name the mechanism and its layer. Apply the standard "can a caller with ordinary privileges do the wrong thing anyway?" — not "does the correct path work?"
open REVIVAL TRIGGER: the first task of the agent-runtime / AgentsService build — not a later hardening pass.
agents FK [DISCOVERED 2026-07-17, agents-v2/v3 build Phase 6, independent lock-gate verification — CLOSED 2026-07-18, Phase 1 Security & Integrity Remediation, Item 6] agents.agent_skill_assignment.agent_identity_id is a BARE (non-composite, non-tenant-scoped) FK into identity.agent_identity(id) — this codebase's own established convention elsewhere is a composite (col, tenant_id) FK for exactly this shape. Root cause: identity.agent_identity itself lacks a UNIQUE(id, tenant_id) prerequisite. This is Phase-5-origin (the table was built then), not introduced by Phase 6's own identity-reopen cutover work — Phase 6 only retargeted IdentityService's own application-layer queries onto this pre-existing table shape, it did not touch the FK itself. Not currently exploitable via any live application code path: the sole write path, IdentityService.assignSkillToAgent(), already re-fetches the target agent's own tenant_id and rejects a cross-tenant assignment BEFORE the insert (an application-layer guard, not a DB-structural one) — live-reproduced correct by the same verification pass that found this gap. Matches the same bug class this codebase's own 2-batch Header/Line Remediation effort (PROJECT_DECISIONS #46-#53) fixed repeatedly elsewhere; not folded into that already-closed effort since it was undiscovered until now. Fixed: identity.agent_identity gained UNIQUE(id, tenant_id); agent_skill_assignment.agent_identity_id retargeted to a real composite (agent_identity_id, tenant_id) FK — see PROJECT_DECISIONS #68. closed — (fixed 2026-07-18, migration 20260718000006_phase1_remediation_item6_fk_audit_fixes.sql)
agents issue [DISCOVERED 2026-07-15, agents-v2/v3 build Phase 5, independent lock-gate verification, Item P] tool_definition, toolset_definition, and skill_definition all have updated_at columns but NO maintaining trigger (confirmed live: zero triggers on all 3) — matches the same disclosed, accepted gap already documented for shared's own reference tables (rarely-mutated catalog data, updated_at set once at insert and never touched again), but was never itself logged as its own row for this module. open if these 3 catalog tables start seeing frequent post-insert edits, add the shared platform.set_updated_at() trigger, matching every other tenant-scoped table's own convention
agents FK [DISCOVERED 2026-07-15, agents-v2/v3 build Phase 5, independent lock-gate verification, Item P] agents.kill_switch_scope_state.last_event_id is a bare uuid with no FK to agents.kill_switch_event.id — lower risk than the (now-fixed) agent_eval_case.eval_run_id gap since authenticated has zero write grant on kill_switch_scope_state at all (writes are internal, SECURITY DEFINER-trigger-only via resolve_kill_switch_state()), but still worth a real FK for referential-integrity completeness. open if kill_switch_scope_state is next reopened for an unrelated reason, add the FK then (no partitioning complication on either side)
agents issue [DISCOVERED 2026-07-15, agents-v2/v3 build Phase 5, independent lock-gate verification, Item B] docs/database/schema_docs/agents.md omits the required explicit "No RLS" annotation line for 6 join/global tables (agent_catalog_entry_required_tool, agent_eval_suite_version, skill_version_module, skill_version_required_duty, skill_version_toolset, toolset_version_member) — the live DB config itself is correct (confirmed: all 6 genuinely have no RLS, appropriately, as pure join/catalog tables), this is a documentation-completeness gap only. open next time schema_docs/agents.md is touched, add the standard "Global, no RLS" line to these 6 table sections
(all) FK [DISCOVERED 2026-07-18, Phase 1 Security & Integrity Remediation, Item 6 — RECONCILED 2026-07-18, pre-Phase-2 confirmation pass] The original 115-row estimate was regenerated from scratch (the original scratchpad was ephemeral) and reconciled to 230 single-column FKs where both child and parent carry tenant_id (partition-child duplicates excluded, independently re-confirmed by a separate verification pass). Categorized: 12 into mixed-scope (global-or-tenant) parents where a composite FK would be a REGRESSION, not a fix (identity.role, ai.ai_request, agents.rollback_recipe, crm.customer_segment_definition — all 4 use tenant_id IS NULL as a documented built-in/global-row marker); 13 more agent_identity_id-shaped references into identity.agent_identity (same shape as the already-fixed agent_skill_assignment case, parent already has the UNIQUE(id,tenant_id) prerequisite — cheap fix); 27 site_id bare FKs into multi_loc.site (parent already has the prerequisite from the 2026-07-10 site_id FK-wiring reopen — a different, larger population than the pre-existing "site_id has zero FK at all" precedent); 61 genuinely-missing composite FKs where the parent already has UNIQUE(id,tenant_id) (cheap retarget); 117 genuinely-missing composite FKs where 39 distinct parent tables still need a new UNIQUE(id,tenant_id) added first (the expensive tier — comparable in scope to the entire 2-batch Header/Line Remediation effort). Of these, 2 are concretely live-code gaps — platform.subscription_invoice.subscription_id and platform.subscription.source_contract_id, both inserted via a caller-supplied id with zero tenant check in PlatformServiceboth were fixed immediately (app-layer tenant-ownership guard + regression tests, not deferred to the larger phase, since neither needs a schema migration) — see PROJECT_DECISIONS #69. The other ~228 rows sit in modules with no service layer yet (only Identity and Platform have one today) and are not currently reachable via any app code. Recommendation: this needs its own dedicated multi-batch remediation phase (not folded into the nursery-extraction Phase 2 migration) — see PROJECT_DECISIONS #69 for full reasoning. open a dedicated future phase, batched by schema (crm+billing+identity; inventory+pos+orders+purchasing; platform+payments+ai+approvals+signals per the pre-Phase-2 confirmation's own suggested grouping); the 40 cheap rows (site_id + agent_identity_id) can open the first batch since their prerequisite already exists
identity FK [DISCOVERED 2026-07-18, Phase 1 Security & Integrity Remediation, Item 6 — ROOT-CAUSED AND CLOSED 2026-07-18, pre-Phase-2 confirmation pass] identity.identity_access_event.session_id is a bare reference to identity.identity_session.id (plain, not composite — identity_session has no tenant_id at all, so a composite FK was never structurally possible here, correcting this row's own earlier imprecision). Root cause fully traced and independently verified: 100% test-fixture debris, caused by identity-session.spec.ts's own teardown hard-deleting identity_session rows after every test run — violating that table's own documented "permanent audit history, never hard-deleted" design (zero production code path ever deletes a session). Fixed: the test teardown no longer deletes identity_session rows (identity-session.spec.ts); docs/database/schema_docs/identity.md's DR-21 record and 3 column-doc locations, which incorrectly claimed this was an enforced FK backed by a partial index since the column's original 2026-06-28 build, corrected to describe the real, unenforced state. The FK itself stays deliberately unenforced going forward — not because deletion is expected, but because the accumulated orphaned rows already live inside the hard append-only identity_access_event ledger and cannot be backfilled/cleaned without bypassing that ledger's own append-only trigger — see PROJECT_DECISIONS #69 and DR-21 for the full reasoning. closed — (root cause fixed 2026-07-18; revisit enforcing the FK only if a deliberate one-time cleanup of the pre-existing orphaned rows is ever justified)
(all) issue [DISCOVERED 2026-07-18, Phase 1 Security & Integrity Remediation, Item 10] The original doc/source drift cleanup batch for this remediation phase's own Item 10 (an 11-sub-item list) was lost to an earlier context-compaction event in the same session and was not reconstructed from guesswork. This remediation's own docs fan-out (PROJECT_DECISIONS #68, this file, DOCS_INDEX.md, CLAUDE.md, MODULE_BUILD_STATUS.md) closes doc drift Items 1-9 themselves introduced, but the original, broader-scoped Item 10 sweep was never independently executed. open run a fresh, from-scratch doc/source drift sweep (grep every schema_docs/.md and module_spec/.md against live DB metadata) as its own dedicated pass
shared issue [DISCOVERED 2026-07-18, Phase 1 Security & Integrity Remediation — CLOSED 2026-07-18, Phase 2 (Nursery Vertical Extraction)] shared's own 12 tables were deliberately excluded from Phase 1's GRANT/REVOKE structural sweep (Item 2) — most lack a tenant_id column entirely, so the tenant-leak/append-only reasoning that phase applied elsewhere doesn't translate directly; shared needed its own dedicated write-permission design pass. Closed: Phase 2's Part C wrote and live-reproduced the design — REVOKE INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA shared FROM authenticated (all 8 remaining tables, post-extraction) plus ALTER DEFAULT PRIVILEGES (run as the original granting role) so future tables inherit read-only-by-default, closing the exact bug class Phase 1's own polymorphic_target_registry finding was, in reverse. nursery_ref (the new schema 4 of shared's 12 tables moved into during the same phase) got the identical treatment from its own first migration, not retrofitted later. See PROJECT_DECISIONS #70. closed — (resolved 2026-07-18)
(all) issue [DISCOVERED 2026-07-18, pre-Phase-2 confirmation pass] The db-drift-check.yml CI gate, disclosed as "unexercised" when built (PROJECT_DECISIONS #68), was live-investigated and found to have 3 STACKED, independent reasons it would not actually catch or block a real regression today, each requiring a human decision (not silently picked): (1) this repo lands 100% of changes via direct commits to main — zero PRs, zero merge commits across 88 commits, confirmed via gh pr list and git log --merges — so the workflow's on: pull_request trigger structurally never fires; (2) main has zero branch protection (gh api .../branches/main confirms protected: false, no required status checks) — even a firing, correctly-failing gate would block nothing; (3) the workflow's own postgres:17 service is a vanilla Docker image, but this repo's migrations assume Supabase-provisioned Postgres (the authenticated role, an extensions schema with pgcrypto/pgvector) — empirically, the real unmodified migration set fails on migration #1 of 90 against a vanilla image (role "authenticated" does not exist), so the job would never even reach the drift-check step. The underlying drift-check.ts script itself remains genuinely fail-closed (independently re-proven: crashes non-zero on a bad DATABASE_URL; reports 333 real failures, never a false PASS, against a genuinely empty database) — this is a CI-wiring gap, not a detection-logic gap. See PROJECT_DECISIONS #69 for full evidence and the 3 specific decisions needed (trigger convention: push vs. adopting real PRs; DB image: Supabase-flavored image vs. an explicit role/extension bootstrap step; branch protection: gated behind a GitHub paid-plan upgrade for this private repo). open Srini: 3 decisions needed before this gate can protect anything — trigger convention, CI Postgres image/bootstrap strategy, and branch-protection/plan upgrade — then execute the workflow once for real to get its first genuine pass/fail
(all) issue [DISCOVERED 2026-07-18, Phase 2 (Nursery Vertical Extraction) full-suite run] Running the entire apps/api test suite (53 spec files) as one combined jest --runInBand process now reliably fails partway through with PostgresError: remaining connection slots are reserved for roles with the SUPERUSER attribute — a genuine, worsening structural gap, not a one-off flake: no test file in this suite ever closes the postgres() connection pool object its own getAdminDb()/getDb()/tenantDB() import creates, so open connections accumulate monotonically across the single node process's lifetime as more files run, without ever being released mid-suite. This was previously only named as a narrower, single-file flake (database/__tests__/rls-cross-tenant.spec.ts, "fails only under parallel workers" — PROJECT_DECISIONS #66/#67); it now reproduces deterministically in a combined serial run too, worsening as the suite has grown to 53 files (2 more added by this same phase). Verified NOT connection-count-related in the idle sense — pg_stat_activity shows only ~30 rows both before and after a failing run, confirming the exhaustion is a transient in-process spike, not a persisted leak. Worked around for this phase's own verification, not fixed: the full suite was run in 4 batches, each its own fresh jest process, confirming 1236/1237 passing (the 1 failure being the separately-logged, unrelated admin-catalog.spec.ts pagination flake). open add a global jest teardown (globalTeardown in jest config) that calls .end() on every connection pool opened during the run, or split apps/api's test npm script into batched sub-suites as a standing convention, before the file count grows further
inventory issue [DISCOVERED 2026-07-18, Phase 2 (Nursery Vertical Extraction) docs pass] Reconciling inventory's column count while documenting this phase's own item.plant_id drop surfaced a pre-existing, unrelated drift: schema_docs/inventory.md's own stated baseline (25 tables / 351 cols, last updated at the 2026-07-10 Header/Line Remediation reopen) undercounts the live/Drizzle-confirmed total by 8 columns — live inventory was actually 359 columns immediately before this phase's own -1 (plant_id dropped), landing at 358 post-Phase-2, not the 350 the stale 351 baseline would predict. Confirmed NOT caused by Phase 2 (this phase's only column-level change to inventory was the single plant_id drop; drift-check.ts — which diffs live DB against the Drizzle source, not against this doc — passed clean both before and after this migration, so the DB and Drizzle schema already agreed with each other; only this doc's own stated total was stale). Root cause not investigated this pass (out of Phase 2's own scope, and matches the same disclosed-not-silently-corrected pattern already used for shared's 108-vs-120 and offers' 42-vs-43 column-count drifts). open run a full per-table column recount against live DB the next time inventory is reopened, and correct schema_docs/inventory.md's stated baseline with the reconciled number
platform issue [DISCOVERED 2026-07-18, Phase 2 (Nursery Vertical Extraction) full-suite run] platform/__tests__/admin-catalog.spec.ts test (C2) our fixture agreement version appears in the response fails when the full suite has run for a while, reproduces in total isolation too — confirmed pre-existing and entirely unrelated to Phase 2 (touches only platform.agreement_version, a table this phase never touched). Root cause not fully traced this pass, but the shape strongly suggests pagination-vs-data-growth: PlatformService.listAgreementVersions() orders by desc(effective_date), desc(id) with limit/offset, and the live table has grown to 55 rows (organically, across many sessions of dev/test activity) — if the test's own fixture uses an effective_date that sorts behind enough real rows, it falls off the first page the test's assertion checks. open root-cause and fix (or make the test query its own fixture by id/version directly instead of asserting page-1 membership) the next time platform or its test suite is touched
pos issue [Phase 3 stored-value build, 2026-07-18, PROJECT_DECISIONS #71 — the 'reward' half of the old fail-closed gate, carried forward as its own row.] chk_sale_payment_no_unbacked_tender_type now blocks ONLY payment_method='reward'. Part 0.3 of the stored-value build investigated whether the rewards module (locked since #56/#60) genuinely backs a reward TENDER and confirmed it does not, on 4 independent grounds: zero pos→rewards linkage column exists anywhere in pos; no points→money bridge exists (the only monetary mapping is a pre-configured reward_option.discount_amount_cents); chk_loyalty_point_ledger_redeem_requires_reward_option structurally forces every redemption through a reward_option — a free-form tender redemption would violate rewards' own constraint as designed; and loyalty_account is consumer-layer-only (consumer_id NOT NULL), with no crm-customer-keyed account. Semantically, rewards' own shape models redemption as a price adjustment, not a tender. open when a reward-tender backing is deliberately designed: (a) a sale_paymentrewards.loyalty_point_ledger (or new redemption-instrument) reference column with a real composite FK, (b) a point-valuation decision (points→minor_units), and (c) a rewards-side decision on whether tender-redemption bypasses or extends the reward_option requirement — then, and only then, narrow the gate away entirely
billing issue [Phase 3 stored-value build, 2026-07-18, PROJECT_DECISIONS #71] billing.ar_payment.payment_method already declares 'store_credit' as an A/R payment method, but ar_payment has no linkage column to billing.store_credit_transaction — the same unbacked-vocabulary shape pos.sale_payment had before this build, at much lower risk (no ArPaymentService/payment-recording flow exists at all, and no fail-open acceptance path is live). Deliberately NOT wired this pass (out of the POS-tender scope). open when BillingService's A/R payment recording is built — add a store_credit_transaction_id composite FK + a coherence CHECK mirroring pos.sale_payment's strengthened companion, and route the redemption through the same atomic ledger mechanism
billing issue [Phase 3 stored-value build, 2026-07-18, PROJECT_DECISIONS #71 — disclosed hardening gap, shared with rewards.] The cached balance_cents on billing.gift_card/store_credit_account is maintained exclusively by the sync triggers, but a caller with the authenticated role can still UPDATE the header column directly (inflating — not overdrawing, the >= 0 CHECK holds — a balance without a ledger entry). This matches rewards.loyalty_account.balance_points' identical, pre-existing posture. Same class: the reversal trackers' total_reversed_cents is directly UPDATE-able by authenticated (the trigger's own UPDATE runs as the invoking role, so UPDATE can't be revoked; DELETE WAS revoked this pass after the Section 4 audit live-reproduced the cap-reset exposure). open a future hardening pass: column-level REVOKE on the cached-balance columns + SECURITY DEFINER (search_path-pinned) sync functions, applied jointly to billing.gift_card/store_credit_account/both trackers AND rewards.loyalty_account/its tracker — one pattern, all five surfaces
pos issue [Phase 3 stored-value build, 2026-07-18, PROJECT_DECISIONS #71 — service-layer contract, same class as pos DR-2/DR-3.] A sale_payment row with payment_method IN ('gift_card','store_credit') is NOT DB-required to have a matching 'redeem' ledger entry — the tender row and its balance deduction are only atomically paired when the service writes both in one transaction. The DB enforces the reverse direction fully (a redeem entry requires a real payment row, one redeem per payment ever), and the strengthened companion CHECK requires a real instrument ref — but a payment row inserted WITHOUT its ledger entry would accept tender with no balance deduction. PosService (unbuilt) MUST insert the sale_payment row and its redeem ledger entry in the same transaction, deriving idempotency_key per DR-3. Same must-pair rule applies to cash_out (policy-gated by stored_value/cash_out_allowed, default false) and refund_to_instrument flows. open when PosService's tender path is built — pair payment row + ledger entry atomically; also enforce the stored_value/cash_out_allowed and gift_card_default_expiry_days tenant settings (catalog rows seeded this pass) at issuance/cash-out time
billing issue [Phase 3 stored-value build, 2026-07-18, PROJECT_DECISIONS #71] Escheatment (unclaimed-property remittance of dormant gift-card balances, state-law-specific) has NO schema surface and NO prior art anywhere in this codebase (grep-confirmed at design time). The schema deliberately supports the primitives (per-card issued_at, full ledger history, expires_at default NULL = never expires, expire entry type) without baking any jurisdictional assumption in. open when a compliance/reporting pass takes on dormancy reporting — a read-only report over gift_card/gift_card_transaction by last-activity age, per-state rules at the service/report layer, never a schema default
billing issue [Phase 3 stored-value build, 2026-07-18, PROJECT_DECISIONS #71 — documented live-reproduction fixture rows.] The Phase 3 guard live-reproduction committed a small set of deterministic, f3000000--prefixed fixture rows to the local dev DB under tenant 00000000-...-0001 (2 gift cards + 9 ledger entries + supporting pos register/session/sale/payment rows): ledger rows are append-only by design and structurally undeletable, so they were documented rather than force-deleted (deleting them would have required disabling the very trigger this build exists to prove — the exact lesson of Phase 2's own "Guard Test" debris investigation, applied in reverse: document at creation time, don't leave for a future archaeologist). The naive-concurrency demo rig (a deliberately-wrong function + one card) WAS fully removed. The independent lock-gate re-verification (same day) added a second, distinct a1000000--prefixed fixture set for its own from-scratch probes (1 gift card + 3 ledger entries from an independent 3-way concurrency race, 1 more from a void-guard probe, 1 sale_payment row pinned by its own ledger redeem entry) — 3 of 4 probe sale_payment rows were cleaned up; the rest follow the identical disclosed-not-deleted pattern. open if/when the local dev DB is reseeded from scratch these rows disappear naturally; no other action needed — this row exists as the documentation
billing issue [Phase 3 fix migration 20260718000009, 2026-07-18, PROJECT_DECISIONS #71 addendum — Finding 3, round 2 of a GENUINELY independent adversarial verification pass, dispatched after the account's spend-limit reset.] 'refund_to_instrument' had no same-instrument check and no aggregate-amount cap at all: pos.sale_refund carries no gift_card_id/store_credit_id of its own, only a nullable sale_payment_id, so the SAME sale_refund_id could be cited against MULTIPLE different cards — the verifying agent live-reproduced fabricating $30.00 of stored value from one $15.00 blind (no-receipt) refund record. Fixed same-day inside both sync trigger functions (no schema/CHECK change): a same-instrument check when the refund's sale_payment_id is known, plus a cross-table (gift_card_transaction + store_credit_transaction) SUM cap against the refund's own refunded_amount_minor_units, closing the gap even for a receiptless refund. Live-reproduced the agent's exact scenario plus the receipted-refund variant; regression tests I7–I9 added to billing-stored-value.spec.ts. closed — fixed, live-reproduced, tested same-day
billing issue [Phase 3 fix migration 20260718000009, 2026-07-18, PROJECT_DECISIONS #71 addendum — disclosed scope boundary of the Finding 3 fix above, NOT fixed.] store_credit_transaction's 'issue' entries may ALSO legitimately carry sale_refund_id (credit issued FROM a refund, v1's own shape, distinct from 'refund_to_instrument') — the Finding 3 fix's same-instrument check and aggregate-amount cap apply ONLY to 'refund_to_instrument' entries, since that is the exact path the verifying agent demonstrated exploitable. An 'issue' entry with sale_refund_id set is NOT covered: its amount is already disclosed as unconstrained by design (row above, Finding 2 fix), and the SAME same-instrument/aggregate-cap gap could theoretically apply there too (an 'issue'-with-refund citing an unrelated or already-exhausted sale_refund_id), but this was not proven exploitable and is deliberately out of this fix's own scope rather than silently assumed safe. open if a future verification pass specifically targets store_credit's 'issue'-with-sale_refund_id path, extend the same same-instrument + aggregate-cap check to it, OR make an explicit, disclosed decision that 'issue'-from-refund is a distinct business event that doesn't need the same guard (e.g. because it represents a NEW grant rather than a refund of spent value)
pricing issue [gap-validation report 2026-07-19, Part A, A1 — CONFIRMED GAP] pricing.price_rule.item_variant_id is NOT NULL and scope_type's CHECK (chk_price_rule_scope_fk_consistency) has exactly 4 values — default/price_level/customer/customer_group — none category/brand/collection-scoped. "20% off all perennials" is unrepresentable as one rule; it requires one price_rule row per variant in the category, with no shared parent to atomically update/expire. pricing.price_change_log.item_variant_id is also NOT NULL, inheriting the same per-variant granularity. closed — resolved 2026-07-20 (Gap-Fill Batch, PROJECT_DECISIONS #74): item_variant_id relaxed nullable; a NEW item_scope_type column (variant/category/brand/all, deliberately distinct from the pre-existing WHO-scope scope_type column) plus nullable category_id/brand_id composite FKs (the latter into a brand-new, minimal inventory.brand table — no brand catalog existed anywhere, architect-authorized to build one). chk_price_rule_item_scope_fk_consistency enforces exactly one target per scope. Resolution precedence (variant > category > brand > all) is documented as a PricingService rule, not yet enforced — that remains PricingService's own future build. collection-scoped stays out of scope (not requested).
orders issue [gap-validation report 2026-07-19, Part A, A3 — CONFIRMED GAP; Part B, B4 — PARTIAL] orders.order_header/order_line carry only a set_updated_at trigger — no amendment/change-log table exists anywhere in the orders schema (7 tables total: order_fulfillment, order_fulfillment_line, order_header, order_line, order_payment, order_template, order_template_line), unlike pricing.price_change_log, which exists for exactly this purpose on the pricing side. A quantity change, price override, or date-push on a live special order overwrites the row in place with zero forensic trail beyond the single updated_at timestamp. Related finding (B4, purchasing/receiving side): purchasing.purchase_order/purchase_order_line DO carry an expected_delivery_date (header AND line level) that can absorb a vendor's revised ETA, but it's mutable-in-place — no vendor-acknowledgement column and no original-vs-revised-ETA history exists there either; a later ETA push silently overwrites the original with no trail. Logged as ONE combined row, not two, since both are the same underlying architecture question: does this codebase want a general-purpose audit-trail/versioning mechanism (e.g. a shared platform-level change-log pattern reusable by any header/line table), or does each module keep inventing its own bespoke log table (price_change_log, and eventually an order_change_log, a PO-ETA-revision-log, etc.) one at a time as each module happens to need it? This is a genuine architecture fork, not a per-module deferral. open before the 2nd or 3rd module independently reinvents a change-log table, raise this as its own architecture decision (general mechanism vs. per-module bespoke logs) — whichever way it's decided, orders' own amendment history and purchasing's PO-ETA-revision history should both be built against the SAME chosen pattern, not two more independent one-offs
pos issue [gap-validation report 2026-07-19, Part A, A4 — CONFIRMED GAP] No gift-receipt representation exists anywhere in the schema. SELECT table_schema, table_name, column_name FROM information_schema.columns WHERE column_name ILIKE '%gift%' returns exactly 2 hits, both FK-shaped columns pointing at billing.gift_card (billing.gift_card_transaction.gift_card_id, pos.sale_payment.gift_card_id) — nothing gift-receipt-related. No flag exists on pos.sale, pos.sale_line, or pos.sale_refund to mark a line as gift-wrapped/price-hidden, and no mechanism identifies the gift recipient as the returning party at refund time without a price disclosure. A gift return (hide price on the receipt, credit the bearer rather than the original payer) has no schema anchor to build against today. partially closed — resolved 2026-07-20 (Gap-Fill Batch, PROJECT_DECISIONS #74): pos.sale_line.is_gift boolean NOT NULL DEFAULT false persists line-level gift intent (covers mixed baskets). This is the flag only — rendering (hiding price on a printed/emailed receipt) and the gift-return bearer-credit mechanism at refund time are NOT built; both remain the receipt-rendering/notifications build's own work, per this row's original trigger. when the receipt-rendering/notifications build consumes is_gift
receiving issue [gap-validation report 2026-07-19, Part B, B3 — CONFIRMED GAP] receiving.goods_receipt.purchase_order_id is NOT NULL — no non-PO receipt row can exist under the current schema. A walk-in/off-PO vendor delivery (a real SMB retail scenario — a supplier drops off an unplanned restock, or a nursery grower delivers plants with no PO on file) has no receiving path at all. closed — resolved 2026-07-20 (Gap-Fill Batch, PROJECT_DECISIONS #74): purchase_order_id relaxed nullable + new receipt_source (purchase_order/direct) CHECK-coherent with it; goods_receipt_line.purchase_order_line_id ALSO relaxed nullable (a scope correction found mid-build — it was NOT NULL, which would have made a direct receipt's header creatable but line-less) with a cross-table trigger enforcing the same source/line-ref coherence at the line level. vendor_id was already NOT NULL, so "SOME anchor" was already structurally guaranteed — no receipt_reason column was needed. Full stock-effect walk (a real inventory.stock_movement/stock_movement_line pair) live-reproduced end to end for a direct receipt, proving it completes an inventory effect exactly like a PO-sourced one.
orders issue [gap-validation report 2026-07-19, Part B, B10 — CONFIRMED GAP] orders.order_payment.status CHECK (chk_order_payment_status) is ('scheduled','due','paid','failed','refunded','cancelled') — no forfeited value. A customer-forfeited deposit (special order cancelled by the customer, tenant keeps the deposit per stated policy) cannot be distinguished from cancelled (which reads as "the payment itself never completed / was voided, no money changed hands") or refunded (money returned) — there is no status meaning "money was kept, but the underlying order did not proceed." partially closed — resolved 2026-07-20 (Gap-Fill Batch, PROJECT_DECISIONS #74): CHECK widened to add 'forfeited', plus a new terminal-state guard trigger (trg_order_payment_guard_status, mirroring notifications.delivery_attempt's own no-op-plus-reject shape) so a forfeited row can never transition to any other status. The DEEPER semantics this row asked about — does a forfeited deposit still count toward balance_due_cents? does it post anywhere in billing? — are explicitly NOT decided by this pass; that remains OrderService's own future build. when OrderService's cancellation/forfeiture path is built and needs to decide the balance/billing semantics
offers issue [gap-validation report 2026-07-19, Part B, B6 — PARTIAL] offers.offer.discount_type CHECK (chk_offer_discount_type) includes bogo and free_item, both tied via chk_offer_discount_type_coherence to a single free_item_variant_ref with no quantity-threshold column. A generalized "buy 3 get 1 free" (quantity-gated BOGO) or a multi-item bundle price (e.g. "any 3 for $10") is not representable — only classic 1-for-1 BOGO/free-item swaps. partially closed — resolved 2026-07-20 (Gap-Fill Batch, PROJECT_DECISIONS #74): a new, additive discount_type='buy_x_get_y' closes the quantity-gated-BOGO half (bxgy_qualifying_qty/bxgy_reward_qty/bxgy_reward_variant_id/bxgy_reward_discount_pct, own coherence CHECK; the pre-existing bogo value is untouched). A same-pass live-reproduction pass ALSO found and fixed a real bug: the pre-existing chk_offer_discount_type_coherence had no branch at all for the new value, so every buy_x_get_y insert would have unconditionally failed it regardless of the new columns — fixed in a follow-up migration the same day. Multi-item bundle pricing (the "any 3 for $10" half) remains genuinely deferred, not built — its own OPEN_ITEMS trigger stands as originally written. trigger: offers v1.1, when bundle pricing (not quantity-gated BOGO, which is now built) is prioritized
tax dependency [gap-validation report 2026-07-19, Part A, A5 — facts confirmed, PRE-SELL-PATH ARCHITECT DECISION, not a deferral] POS is offline-first, but tax.tax_calculation/tax_calculation_jurisdiction are the only 2 tenant-facing tables in the tax schema (plus 1 global jurisdiction_level_catalog) — no local/cached tax-rate table exists, no tenant-level tax-fallback setting exists (SELECT key FROM admin.setting_definition WHERE key ILIKE '%tax%' returns 0 rows), and pos.sale_line.tax_amount_cents/.tax_rate are plain, freely-settable columns with zero FK/trigger requiring correlation to any tax.tax_calculation row. tax_calculation.provider is a FK to platform.processor_catalog(code), which does seed a manual/exempt value structurally, but no computation logic exists behind either. An offline sale can be rung and synced today with self-reported tax values, fully uncorrelated to any real tax computation, with no schema-level guardrail governing what those values should be. This is disclosed as facts only, per the original gap-validation task's own explicit instruction not to resolve it — it is a genuine pre-sell-path go/no-go decision (what tax logic applies to an offline sale: a cached last-known rate? a tenant-configured flat fallback rate? defer tax entirely and true it up on sync?), not something to schema-default away. UNDECIDED — requires an explicit architect decision before POS's offline tax behavior can be considered production-ready, not merely deferred before general availability of offline POS — an architect must decide the offline-tax model (cached rate table + a sync-time reconciliation job, vs. a tenant-level flat fallback rate setting, vs. accepting self-reported values with a mandatory post-sync review flag) and only then should schema follow
offers issue [gap-fix pass, 2026-07-19 — disclosed non-reachable branch in the max_per_consumer fix] check_and_sync_offer_budget()'s new max_per_consumer enforcement includes a defensive AND NEW.consumer_id IS NOT NULL guard for a hypothetical anonymous redemption. Confirmed live via \d offers.offer_redemption: consumer_id is NOT NULL at the table level today — no anonymous redemption row can exist under the current schema, so this guard is forward-compatible dead code, not a live gap, and was disclosed as such in the migration's own header comment rather than silently assumed reachable. Independently re-confirmed by the same-day lock-gate verification pass (its own fresh fixtures, not a re-run of this build's own probes) — flagged as the sole finding of its adversarial sweep, no other bypass path found. open purely informational unless a future reopen relaxes offer_redemption.consumer_id to nullable (e.g. to support a fully anonymous kiosk-code redemption flow) — if that ever happens, OffersService.redeem() must be the enforcement point requiring consumer identification for any offer that carries a max_per_consumer cap, since the DB guard alone cannot bind an anonymous row to a per-consumer count
offers issue [gap-fix pass, 2026-07-19 — documented independent-verification fixture debris, same disclosed pattern as prior phases.] The independent lock-gate verification's own from-scratch concurrency-race fixtures (UUID prefix a2000000-...) could not be fully cleaned up: offers.offer_redemption is genuinely append-only (platform.reject_append_only_mutation()), which transitively blocks deleting the 5 offers, 20 pos.sale rows, 4 consumers, and 1 tenant that its 10 offer_redemption rows (+2 reversal-tracker rows) reference. Documented rather than force-deleted — same reasoning as the Phase 3 stored-value build's own f3000000-/a1000000--prefixed debris (see the billing rows above). open if/when the local dev DB is reseeded from scratch these rows disappear naturally; no other action needed — this row exists as the documentation
notifications issue [notifications build, 2026-07-19, PROJECT_DECISIONS #73] platform_suppression.channel/suppression.channel both accept 'push' alongside email/sms, but push notifications don't have hard-bounce/spam-complaint semantics the same way email/SMS do — a push token becomes invalid (uninstalled app, revoked permission, expired token) rather than "bouncing" or triggering a spam complaint in the RFC-5321/carrier sense. The reason CHECK (hard_bounce/spam_complaint/manual/provider_block on suppression; the same minus manual on platform_suppression) was designed against email/SMS provider vocabulary and was never specifically validated against a push provider's (e.g. FCM/APNs) actual invalid-token event shape. Disclosed as a genuine wrinkle found during docs fan-out, not fixed — the schema is structurally ready (channel already includes push) but the reason vocabulary may need a push-specific value (e.g. token_invalid) once a push provider is actually integrated. open when a push notification provider (FCM/APNs) is actually integrated — confirm whether provider_block already covers "token invalid" or whether a dedicated reason value is needed
notifications issue [notifications build, 2026-07-19, RULED design D11, PROJECT_DECISIONS #73] A malicious or careless manual suppression of a legitimate address (suppression.reason='manual') is partially mitigated by created_by_actor_id + the audit trail it implies, but not fully preventable at the schema layer — there is no approval gate, no rate limit, and no reconciliation check on manual suppression inserts. Manual suppressions deliberately never propagate to platform_suppression (only automatic hard_bounce/spam_complaint/provider_block do, per the platform_suppression CHECK's own deliberate exclusion of 'manual'), so this residual risk stays scoped to the single tenant that created it, not cross-tenant — but within that tenant it is a real, disclosed gap. open HUMAN DECISION / HOUSEKEEPING — no auto-trigger; revisit if abuse is ever observed in practice (e.g. an approval gate on manual suppression inserts, or a periodic review report of recently-added manual suppressions)
notifications issue [notifications build, 2026-07-19, RULED design Ruling 16, PROJECT_DECISIONS #73] Retention horizon undecided by design (a stated posture, not a decision): notification/delivery_attempt/in_app_notification (transactional-record tables) likely warrant a longer retention horizon than provider_event_log/provider_event_dead_letter (raw provider events, shorter-value once ingested into delivery_attempt's cache and past the CAN-SPAM/TCPA evidence window). No horizon is picked in this build; all 7 ledger-shaped tables (notification, delivery_attempt, campaign_recipient, notification_frequency_tracker, provider_event_log, provider_event_dead_letter, suppression) are UUIDv7-keyed, keeping time-range partitioning open at near-zero cost whenever a horizon is actually decided. open when a concrete data-retention/compliance requirement or a real storage-cost pressure forces the decision — pick horizons per table-class and implement via partition-drop, not a bulk DELETE, given the UUIDv7 keys
notifications issue [notifications build, 2026-07-19, RULED design D3/D12/D9/D14, PROJECT_DECISIONS #73] Deferred capabilities carried forward from the RULED design, not built this pass: (1) source_module CHECK's exact producer list and provider_event_log.event_type's exact Resend/Twilio vocabulary both need Build-time finalization against real producers/webhooks; (2) delivery_attempt.max_attempts's actual default/tenant-override mechanism is a config-value decision, not a schema-shape one; (3) POS↔Notifications offline-print integration shape — notification.printed_at exists as the durable signal, but no offline-sync quintet (client_uuid/sync_status/etc., matching pos.sale's own pattern) is proposed on notification itself; (4) notification_quota_usage.sent_count/campaign.recipient_count reconciliation view — mirrors inventory.stock_reconciliation_shell's "minimal shell now, real aggregation later" precedent, not built; (5) sender_identity's 'byo' verification-lifecycle machinery (DNS check, actual provider verification call) — the column exists (domain_mode), the workflow does not; (6) 4 *_user_id-shaped columns flagged in the RULED design as needing the standard retarget were in fact retargeted during THIS build (notification_preference.actor_id, in_app_notification.recipient_actor_id, inbound_message.handled_by_actor_id, campaign.created_by_actor_id, journey_enrollment.recipient_actor_id) — listed here only for completeness against the RULED doc's own Deferred list, not because any retarget remains outstanding. open service-build time (NotificationsService) for items 1–5; item 6 is informational only, already resolved
notifications issue [notifications build, 2026-07-19, mandatory independent lock-gate verification, MAJOR finding #4, PROJECT_DECISIONS #73] ALTER DEFAULT PRIVILEGES IN SCHEMA notifications GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO authenticated (applied by the schema-wide GRANT fix, 20260719000010) still grants authenticated full read/write to any FUTURE table created in this schema — the identical latent-risk class independently found and fixed for platform.polymorphic_target_registry (PROJECT_DECISIONS #68 Item 1). notifications.platform_suppression ITSELF is correctly locked down TODAY (confirmed live via information_schema.role_table_grants: zero grants of any kind for authenticated) — the risk only materializes if this specific table (or an equally sensitive future one) were ever dropped and recreated, since default privileges apply at CREATE TABLE time and cannot selectively exempt one table name in advance. Not fixed this pass — revoking the schema-wide default would incorrectly block every other (legitimately tenant-writable) table's own future siblings. open if platform_suppression is ever dropped/recreated, immediately re-apply its own REVOKE ALL ... FROM authenticated in the same migration that recreates it — don't rely on the schema-wide default to protect it
inventory issue [Gap-Fill Batch, 2026-07-20, PROJECT_DECISIONS #74] docs/modules/MODULE_INDEX.md's inventory row's stated column count (358, as of the Phase 2/Nursery-Extraction pass) does not reconcile against a live re-measurement: querying information_schema.columns for every inventory schema BASE TABLE (excluding the stock_reconciliation_shell VIEW, which the row itself already says doesn't count) returns exactly 350 cols across 25 tables immediately BEFORE this batch's own inventory.brand addition (confirmed by subtracting brand's own 7 cols from this batch's live post-migration total of 357/26). This is an 8-column pre-existing drift, unrelated to this batch's own work (which only adds brand's 7 cols + 1 table) — found incidentally while reconciling MODULE_INDEX for this reopen, not chased to a root cause. This batch's own docs pass corrects the row's stated figure to the true live count (357, not 358+7=365) but does not investigate why the prior 358 was itself already wrong. open whoever next reopens inventory should re-derive the full column list from \d output table-by-table (as this row's own discovery did) and compare against the Drizzle schema file to find which specific column(s) account for the 8-column gap, rather than trusting either source's running tally
inventory issue [Gap-Fill Batch, 2026-07-20, independent lock-gate verification, PROJECT_DECISIONS #74] inventory.stock_movement.chk_stock_movement_source_module only permits ('pos','orders','purchasing','inventory','production','system','returns') — it does NOT include 'receiving', despite goods_receipt/goods_receipt_line living in a dedicated receiving schema since the 2026-07-10 extraction (PROJECT_DECISIONS #55). A pre-existing drift, not introduced or touched by this batch — found only because this batch's own independent verifier needed to insert a real stock_movement row to prove B3's direct-receipt stock-effect walk, and had to use source_module='purchasing' as a workaround since 'receiving' was rejected outright. open before ReceivingService is built — widen the CHECK to add 'receiving' (a pure enum-widen, zero column impact, matching this codebase's own established precedent for this exact class of fix) before any service code naively tags its own stock movements with source_module='receiving' and hard-fails
(all) issue [Gap-Fill Batch, 2026-07-20, PROJECT_DECISIONS #74] The 8 hand-written SQL migrations for this batch (item_scope_type/category_id/brand_id on pricing.price_rule; inventory.brand; pos.parked_cart/parked_cart_line; pos.sale_line.is_gift; receiving.goods_receipt.receipt_source + relaxed FKs; offers.offer's 4 bxgy_* columns + the discount_type_coherence fix; orders.order_payment's forfeited status) were applied directly and independently verified live, but the corresponding Drizzle TypeScript schema files (packages/db/src/schema/pricing/rule.ts, inventory/catalog.ts or a new file for brand, pos/sale.ts/a new pos/parked_cart.ts, receiving/receipt.ts, offers/offer.ts, orders/payment.ts) were NOT updated to match — a real, disclosed Drizzle↔live-DB drift, not caught by this batch's own pipeline since it never named a Drizzle-sync step. The existing drift-check CI gate (packages/db/scripts/drift-check.ts) would flag this if it ran, but per PROJECT_DECISIONS #69 that gate doesn't actually fire in CI today (no PRs, no branch protection, and the CI Postgres service can't get past migration #1) — so this drift is currently invisible to any automated check. closed — resolved 2026-07-20 (Drizzle Sync Follow-Up, same-day). All 7 touched Drizzle schema files updated to mirror the 8 migrations exactly (pricing/rule.ts, inventory/catalog.ts — extended in place for category's UNIQUE + the new brand table, no separate file — pos/register.ts, pos/sale.ts, a new pos/parked_cart.ts, receiving/receipt.ts, offers/offer.ts, orders/payment.ts), plus the new parked_cart/parked_cart_line tables added to pos/index.ts's barrel export. Parity proved column-by-column (not eyeballed): a one-off script using getTableConfig() diffed every touched table's Drizzle-declared columns/nullability/unique-constraints against live information_schema/pg_constraint — 11/11 tables PASS on the second pass (the first pass caught a genuine miss: pricing/rule.ts itself had been read but never actually edited, an error the parity script itself surfaced before this row was closed, not found by eyeballing). drift-check.ts re-run clean before and after. A broader, report-only sweep of all 362 Drizzle-exported tables across every schema (not just this batch's own 11) found zero additional drift anywhere else in the codebase. Full apps/api suite re-confirmed at the same 1379 total, zero deltas (10 suites that failed under connection pressure during the full run — including pricing-schema.spec.ts, since pricing/rule.ts was touched — all passed cleanly in isolation, confirming zero runtime-behavior change from the Drizzle-only edits). packages/db typechecks clean (tsc --noEmit, exit 0); the 2 unrelated apps/api TS-strictness findings in pricing-schema.spec.ts/inventory-schema.spec.ts test files (ruleId/stockLotId "used before assigned") predate this entire session (confirmed present at commit f0868fe) and are out of this row's own scope.
inventory issue The historical Inventory documentation count drift was fully re-derived during the Inventory Core Write Protection reopen. closed 2026-07-15 task run Live base-table columns were counted table-by-table and reconciled with Drizzle and the exact +5 migration delta: 26 base tables / 362 columns; the eight-column view is excluded. This supersedes the earlier open count-drift rows.
inventory→transfer dependency Transfer-specific protected wrappers were deliberately deferred to the Transfer migration. closed 2026-07-16 — the Transfer tables and narrow wrappers now exist in the same migration set; they derive tenant, actor, sites, locations, variants, lot, quantities, timestamps, source identity, deterministic keys, disposition, and cost provenance from locked authoritative rows. Runtime EXECUTE remains deliberately absent pending a future credential gateway.
inventory→transfer service-phase dependency The built Transfer wrappers are intentionally dormant: no runtime role has consequential EXECUTE and the current postgres/GUC path is schema-test-only. open Before any Transfer service/API wiring, separately design and approve a non-spoofable tenant/actor credential or signed session gateway, then grant only the exact narrow wrapper paths. Do not expose caller-selected tenant, actor, source type, quantity, cost, provenance, or idempotency facts.
Last modified: Jul 16, 2026, 9:05 AM PT
On this page
Esc