medOS ultra

Inventory Homogenization

One movement spine for every consumable: streaming, device cache, AI rules, exploration, consume-to-charge billing; 10 invariants.

18 min read diagramsUpdated 2026-06-14docs/architecture/inventory-ai-billing-homogenization.md

⚠️ Correction (2026-06-14, audit): Any statement here that billable_ledger “has zero writers” or that createFromLedger “doesn’t exist” is stale. Both writeGhostLedgerLines and salesOrders/createFromLedger are shipped (salesOrder.controller.mixin.ts:725-857, commits 5ecb96fe7/a79b451a9, 2026-06-09). The accurate state: the writers exist but are inertbillable_ledger is empty in prod because the order producer is mis-keyed (medicationCode ≠ sku) and the stock producer is unstamped (no encounterId). See revenue-loop-flow-audit.md §1 + broken-links #1/#2. Original content below is unchanged.

Status: Design + verified live-state audit; P0–P3 SHIPPED + live-verified 2026-06-10 (see §9 table) Date: 2026-06-10 Companion docs: realtime-inventory-supply-chain.md (the streaming substrate master plan, shipped 2026-05-25), menu-price-ledger.md (pricing substrate), universal-facility-stay-billing.md (facility-stay rules engine), emr-cowork-substrate.md (AI coworker invariants), operational-facts-erp-hrm-activation.md (ERP connectors)

Read this before any inventory / stock / supply-chain / consume-to-charge / supply-AI work. This doc is the homogenization layer: it does not redesign the streaming substrate — it ties the five currently-disconnected planes (stream → device cache → rules/AI → exploration → billing) into one contract.


1. Verified Live State (audited 2026-06-10 against the shared PH Supabase + EC2)

The question “are we streaming inventory to a cache on devices, medical supplies included?” has a precise answer: the pipeline is built and deployed, but it is dry. Every layer exists; two small operational faults stop the flow.

1.1 What is SHIPPED and LIVE

Layer Component State (verified)
Write source foundation service: stockItem, stockBatch, stockBalance, stockTransaction (13 movement types incl. GOOD_RECEIVE / STOCK_ISSUE / STOCK_TRANSFER / STOCK_CONSUMPTION / DISPOSAL / STOCK_RESERVE), stockRequest, stockReturn, pharmacyPR/pharmacyPO ✅ live on EC2, full WAC costing per transaction
Event emit stockEvent.emitter.ts → Moleculer stock.* events; StockEventBridgeMixin wired into foundationService.ts:398 → inserts hospital_events rows (STOCK_BALANCE_CHANGED, STOCK_TRANSACTION_CREATED, STOCK_ITEM_*, STOCK_BATCH_EXPIRING) ✅ code deployed — ❌ silently no-ops (see 1.2)
Read model stock_balance_cache, stock_alerts, stock_movement_log, stock_item_catalog (migration 20260525b) — all four in supabase_realtime publication ✅ tables exist live — ❌ 0 rows each
Orchestration inventory-orchestrator edge fn (ACTIVE, v2), sync-inventory-cache edge fn (ACTIVE, v2, reconcile/full/archive modes) ✅ deployed — ❌ never fed / never scheduled
Frontend web/packages/inventory-kit: /inventory-dashboard, /inventory/stock-monitor, /inventory/hub, /inventory (18 deep-link tabs), /medical-supplies-work-list, /inventory/ai-receive, /inventory/ai-dispense; hooks useStockBalanceRealtime, useStockAlerts, useStockMovementFeed, useStockAvailability, useStockItemSearch ✅ shipped, realtime subscriptions correct — render empty because tables are empty
Pricing substrate menu_items (34,205 rows, projected 2026-06-10), resolve_menu_price(p_sku, …) RPC (answers, returns unresolved for unknown SKU), billable_ledger table ✅ live in DB — ❌ billable_ledger has ZERO writers (0 rows); ⚠️ no repo migration file creates these objects (schema drift — applied via SQL editor; 20260605b only references them in comments)
Billing close close-encounter-billing edge fn (ACTIVE, v3, redeployed 2026-06-10) ⚠️ skeleton — reads billable_ledger correctly, POST createFromLedger + billClosed stamping are explicit TODOs; NestJS endpoint doesn’t exist

Medical supplies are covered by the same spine — not a separate system. Product.type distinguishes DRUG | MEDICAL_SUPPLY | CONSU | DEVICE | BLOOD_PRODUCT | … (25+ types); stock_item_catalog.category projects drug | medical_supply | consumable | equipment | reagent | other. One pipeline, one classification axis.

1.2 Why the stream is dry — two root causes (both trivial)

  1. EC2 env: env-files/ever/.env on the PH box defines only ONE of SUPABASE_URL / SUPABASE_KEY (verified: grep -c = 1). StockEventBridgeMixin.getSupabaseClient() requires both and returns null otherwise → every stock event is dropped at debug level (“Supabase not configured”). Zero STOCK_* rows have ever landed in hospital_events.
  2. Cron never seeded: migration 20260525c_inventory_cron_seed.sql was never applied — inventory_cache_config doesn’t exist live and cron_jobs has no inventory entry. So the daily sync-inventory-cache reconcile (which would have backfilled the cache from MongoDB even without events) never ran once.

The P0 fix is operational, not code: add the missing env var + restart foundation, apply 20260525c, invoke sync-inventory-cache once with {"mode":"full"}. The four dashboards light up with no frontend change.

1.3 What is genuinely MISSING (design gaps, not faults)

Gap Detail
No persistent device cache Frontend realtime state is in-memory only. No IndexedDB/localStorage persistence for catalog or balances; service-worker background-sync funcs are unimplemented TODOs. Cold boot = full refetch; offline = blank.
Consume ≠ charge Stock decrement and charge capture are fully decoupled. Charges are created at order-acknowledgement from frontend-supplied OrderRequestItem.unitPrice (defaults to 0!). Dispense (rx.dispensed) only flips queue status. Nothing writes billable_ledger. Supplies issued to wards via STOCK_ISSUE produce no charge at all.
No rules engine Reorder min/max fields exist on StockItem (Odoo-synced) but nothing evaluates them; STOCK_REORDER_TRIGGERED event type exists with no producer. No FEFO enforcement, no par levels, no ABC classification, no substitution rules.
Parallel silos off the spine Blood bank (blood_bag_locations, blood_inventory_movements, blood_dispense_events — no realtime publication, no charge hook), nutrition (nutritionPurchaseOrder/nutritionReceiving), compounding bulk stock, OR implant/supply lines (or_case_costing.supply_line_items[], manual) each track movement in their own shape.
No exploration surface stock_movement_log is the perfect audit substrate (actor, WAC before/after, patient HN, ref no) but has no explorer UI, no anomaly detection, no link from HDAP.
No demand intelligence No forecast, no usage-rate projection, no stockout prediction.
menu_items.financial_product_id is NULL on sampled rows — the projection carries SKU+price but not the Mongo product linkage, so joining ledger↔chargemaster↔stock item is not yet possible.

2. The Homogenization Contract — one spine, five planes

                        ┌─ PLANE 4: EXPLORATION ─────────────────────────┐
                        │  HDAP → Inventory Explorer (movement log,      │
                        │  anomaly feed, shrinkage/diversion detection)  │
                        └────────────────▲───────────────────────────────┘
                                         │ reads
┌─ WRITE SOURCES ─────────┐   ┌─ PLANE 1: ONE MOVEMENT SPINE ─────────────────┐
│ pharmacy dispense        │   │ hospital_events (STOCK_*)                     │
│ ward supply issue        ├──▶│   → inventory-orchestrator                    │
│ blood dispense (adapter) │   │   → stock_balance_cache / stock_movement_log │
│ nutrition (adapter)      │   │     stock_alerts / stock_item_catalog        │
│ compounding (adapter)    │   └──────┬──────────────────┬─────────────────────┘
│ OR implants (adapter)    │          │ realtime         │ change feed
└──────────────────────────┘          ▼                  ▼
                     ┌─ PLANE 2: DEVICE CACHE ─┐  ┌─ PLANE 3: RULES + AI ──────────┐
                     │ inventory-kit hooks      │  │ inventory_rules (deterministic │
                     │ + IndexedDB persistence  │  │ policy_gates-shape JSONB)      │
                     │ + offline op queue       │  │ + supply-chain AI coworker     │
                     └──────────────────────────┘  │   (recommender-only,           │
                                                   │    cowork_proposals)           │
                                                   └──────────────┬─────────────────┘
                                                                  │ approved actions
                              ┌─ PLANE 5: BILLING ────────────────▼─────────────────┐
                              │ consume = charge:                                    │
                              │ STOCK_ISSUE/dispense → resolve_menu_price()          │
                              │   → billable_ledger (append-only)                    │
                              │   → encounter_journey_cache.financial_summary        │
                              │   → close-encounter-billing → SalesOrder             │
                              │ + leakage view: movements WITHOUT charges            │
                              └──────────────────────────────────────────────────────┘

The homogenizing idea: every consumable domain — drugs, medical supplies, blood products, nutrition, compounding ingredients, OR implants — speaks the SAME event vocabulary (STOCK_*), lands in the SAME four read-model tables, is priced by the SAME resolve_menu_price(), charges into the SAME billable_ledger, and is governed by the SAME rule-row pattern (scope_json / predicate_json / action_json / priority) already proven by policy_gates, cds_rules, facility_billing_rule, and detection_rules. Domain differences are data rows and thin adapters, never forks.


3. Plane 1 — Movement Spine (mostly shipped; adapters needed)

3.1 Canonical movement event (already defined, keep as-is)

STOCK_TRANSACTION_CREATED payload (from stockEvent.emitter.ts) is the canonical shape. Every adapter must produce it — including the fields that make billing and audit possible downstream: itemId/itemCode/itemName, locationId, toLocationId?, quantity, beforeQty, stockOnHand, unitCost, totalCost, wacBefore, wacAfter, batchId?, batchNumber?, userId, userDisplayName, departmentName?, patientHn?, refNo?.

Additive field (new, optional): encounterId + chargeIntent. Today stock events carry patientHn but no encounter linkage, which is why consume→charge can’t be wired. Adapters that know the encounter MUST stamp it:

// addition to StockTransactionEventData (additive, optional)
encounterId?: string;        // Mongo encounter _id when movement is encounter-scoped
chargeIntent?: 'billable' | 'non_billable' | 'estimate';  // default: derived from product binding

3.2 Domain adapters (thin, data-preserving — never rewrite the domain)

Domain Adapter Effort
Pharmacy dispense orderRequestProductDispense HANDED_OVER → emit STOCK_TRANSACTION_CREATED (movement_type: stock_issue, with encounterId). The cart flow already calls foundation.stockTransaction.createWithStockAdjustment (medicationCart.controller.mixin.ts:82) — the emitter fires there for free once the bridge env is fixed. The non-cart dispense path needs the same call added. S
Ward supply issue Already STOCK_ISSUE through stockTransaction — nothing to do beyond P0.
Blood bank New thin projector: blood_dispense_events INSERT trigger → synthesize STOCK_TRANSACTION_CREATED row in hospital_events (category: blood_product, bag IDs in refNo). Blood keeps its own tables (chain-of-custody invariants stay); the spine gets a copy of the movement, same as specimen-transport keeps its own context. S
Nutrition nutritionReceivinggood_receive; kitchen consumption → stock_consumption. S
Compounding Ingredient consumption per worksheet → stock_consumption with refNo = worksheet id (the compounding doc already names inventory-orchestrator for bulk stock — this is that hook). M
OR implants/supplies When or_case_costing.status → posted, emit one stock_consumption per supply_line_items[]/implant_line_items[] row (alongside the already-designed ledger trigger trg_or_case_costing_sync_ledger). M

Invariant: one funnel. No adapter writes stock_balance_cache/stock_movement_log directly — only inventory-orchestrator writes the read model (mirrors the “single dispatch funnel” rule of department_queues).


4. Plane 2 — Device Cache (the actual “streaming to devices” answer)

Today: realtime → in-memory React state. Good for connected desktops; useless for cold boot, tablets on ward Wi-Fi dead zones, and the (designed) edge-AI boxes.

4.1 Three-tier cache, additive to existing hooks

  1. Tier 1 — in-memory realtime (exists): keep useStockBalanceRealtime map-based upsert as-is.
  2. Tier 2 — persistent snapshot (new): IndexedDB persistence inside inventory-kit (NOT the dead service worker — service-worker.ts is generateSW dead code per the Vercel cache fix; don’t rewire it).
    • stock_item_catalog (34k-row chargemaster-adjacent catalog) → full snapshot, refreshed by updated_at > last_snapshot delta query on boot, then realtime deltas. This makes item search/autocomplete instant and offline-capable.
    • stock_balance_cache for the user’s userStoreInventory-authorized locations only (scoped, small).
    • Implementation: a createPersistedRealtimeStore(table, scopeFilter) helper in web/packages/inventory-kit/src/cache/ wrapping idb-keyval/localforage — boot: hydrate from IndexedDB → render → delta-fetch → subscribe. Stale data renders with the existing isLive-style badge (Patient 360 pattern).
  3. Tier 3 — offline op queue (new, later): stock receipts/issues/adjustments queued in IndexedDB when offline, flushed through the normal REST endpoints on reconnect with client-generated adjustNo for idempotency. Conflict policy: server WAC always wins; queued op replays as a new transaction (append-only), never a balance overwrite.

4.2 REPLICA IDENTITY

Set REPLICA IDENTITY FULL on the four cache tables (one-line migration) so realtime UPDATE payloads carry full rows — required for the map-upsert pattern to stay correct on partial updates.


5. Plane 3 — Rules + AI (deterministic gates, recommender AI)

5.1 inventory_rules — same shape as every other rule engine in the platform

One table, the proven five-column pattern:

CREATE TABLE inventory_rules (
  id            uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  rule_kind     text NOT NULL,        -- 'reorder' | 'par_level' | 'expiry_window' | 'fefo'
                                      -- | 'substitution' | 'abc_class' | 'consumption_anomaly'
                                      -- | 'controlled_substance_watch' | 'auto_charge'
  scope_json    jsonb NOT NULL DEFAULT '{}',   -- {item_id?|category?|location_id?|store_type?|country?}
  predicate_json jsonb NOT NULL DEFAULT '{}',  -- templated tokens, cds_rules-safe grammar — NOT raw SQL
  action_json   jsonb NOT NULL DEFAULT '{}',   -- {alert_severity, suggest_reorder_qty?, create_ack?,
                                               --  draft_pr?, block_dispense?:false, charge_sku?}
  priority      int NOT NULL DEFAULT 100,
  is_active     boolean NOT NULL DEFAULT false,  -- OFF by default, like everything else
  country_code  text DEFAULT '*',
  created_by    uuid, notes text,
  created_at timestamptz DEFAULT now(), updated_at timestamptz DEFAULT now()
);
  • Evaluator lives in inventory-orchestrator (it already computes stock_status and writes deduped stock_alerts — the rules table replaces its hardcoded thresholds, exactly how policy_gates replaced hardcoded payment gates). Effective-rule resolution: most-specific scope wins, then priority — same resolution semantics as ward_admission_policies/facility_billing_rule.
  • Rule kinds are templated, not freeform (the encounter_watch_rules lesson): each kind has a typed predicate schema, so the admin UI is a form, not a JSONB editor.
  • Admin UI: /admin/inventory-rules cloned from CdsRulesPage; seed library ships draft/inactive (reorder-at-min for all categories, 90/30/7-day expiry ladder, FEFO suggestion on batch-tracked items, controlled-substance variance watch).
  • Country/market differences = seed rows in infrastructure/market-packs/medos-{country}/seed-inventory-rules.sql (TH NHSO consignment patterns, JP lot-trace strictness, PH multi-branch min/max) — zero TypeScript per country.
  • Hard rule: rules never block clinical dispense by default. block_dispense exists for controlled substances only and requires an explicit policy_gates row (hard-stop + override-with-reason per hospital-movement-architecture.md) — the inventory engine suggests, the policy-gate engine gates.

5.2 AI layer — supply-chain coworker on the shipped cowork substrate

All AI is recommender-first per emr-cowork-substrate.md (off by default, kill switch, accept/edit/reject = the training label, llm_audit_log provenance). Register one cowork_agents row: supply-chain-coworker, plane = Operational (no PHI needed — item/location/quantity series only; strip patientHn from anything sent to a model).

Capability Mechanism Writes
Reorder proposals Nightly run over stock_balance_cache + 90-day stock_movement_log usage rate → propose PR drafts (item, qty, vendor from StockItem.vendors) cowork_proposals → Inbox Accept creates pharmacyPR via existing REST. Never auto-submits a PO.
Demand forecast Deterministic first: moving-average + day-of-week seasonality as a twin_metric_supply_* RPC in the existing METRIC_SOURCES registry (HORUS twin calibration, per operational-facts-erp-hrm-activation.md). LLM narrative on top, display-only. stock_alerts row alert_type='reorder_suggested' with suggested_reorder_qty (column already exists)
Consumption anomaly / shrinkage detection Deterministic consumption_anomaly rules (z-score vs item-location baseline) catch diversion patterns — controlled substances especially (RUDS-adjacent: high-value items walking out, after-hours adjustments, repeated small negative adjustments by one user). LLM second-opinion ranks, never suppresses (the e-Kardex AI stance: re-rank only). stock_alerts severity≤warning + AcknowledgementRequest to pharmacy lead
AI receive / AI dispense (routes exist) Document-intelligence path: photo/PDF of delivery note → SpreadsheetIngestPanel-style extraction → prefilled GOOD_RECEIVE draft; human confirms every line (draft-until-signed, scribe pattern). Nothing until human confirm
Charge-leakage recommender See §7.3 — flags movements with no matching ledger row. proposal rows

Invariants (inherit, don’t reinvent): AI cannot create stock transactions, cannot auto-approve PRs/POs, cannot change prices, cannot suppress alerts; every inference audited; per-tenant feature flag.


6. Plane 4 — Logs & Data Exploration

stock_movement_log is already the right substrate (append-only, actor, costing, before/after, patient link). Three additions:

  1. Inventory Explorer at HDAP (/admin/hdap → Monitor group, beside Connector Activity): movement stream with the Supabase-Logs-style panel pattern (ConnectorActivityPanel precedent) — filter by item/location/type/user/date, drill to balance history, export CSV. Saved views per user.
  2. Retention split: hot log stays 14 days in stock_movement_log (existing archive logic); add stock_movement_archive (monthly-partitioned, or fold into the medallion Bronze layer per ai-training-corpus.md) so exploration and forecasting have ≥13 months of history. The sync fn’s archive_only mode gets a destination instead of deleting.
  3. Ask-the-data: wire the existing DataAnalysisPanel (local Ollama RAG, cited sources) to an inventory corpus — read-only RPCs over movement archive + balances (top movers, expiry exposure by location, usage vs forecast). Same safety contract as the unified-clinical-assistant READ tool class: no gate, no writes, no raw SQL from the model — only whitelisted RPCs.

7. Plane 5 — Billing Linkage (consume = charge)

7.1 The chain to wire (most pieces verified live)

movement (STOCK_ISSUE / dispense HANDED_OVER / blood dispense / OR posted / compounding consume)
   → inventory-orchestrator handler                       [exists, extend]
   → if chargeIntent='billable' AND encounterId present:
       resolve_menu_price(sku, payor, scheme, contract)   [LIVE — verified responding]
       → INSERT billable_ledger (append-only)             [table LIVE — zero writers today]
       → encounter_journey_cache.financial_summary        [trg_sync_billing_queue exists]
   → close-encounter-billing → POST /v2/financial/salesOrders/createFromLedger   [skeleton + missing endpoint]

This completes menu-price-ledger.md M4 for the consumable categories — it does not replace order-time charging for services/procedures. Drug/supply charges move from “charge at acknowledgement with frontend-supplied unitPrice (often 0)” to “charge at consumption with backend-resolved price.” During transition, both write; the ledger row carries metadata.source='consume' and the reconciliation view (7.3) shows divergence before cutover (the M2 shadow-mode discipline).

7.2 Catalog homogenization — the one mapping table that makes it work

Three product identities exist today: foundation.StockItem (logistics), financial.Product (chargemaster, stockItemRef link exists), menu_items (Supabase pricing projection, financial_product_id currently NULL). The keystone fix:

  • Backfill menu_items.financial_product_id from the projection source, and add stock_item_catalog.menu_item_sku (projected via Product.stockItemRef join) so the orchestrator can go mongo_item_id → sku → resolve_menu_price() in one lookup.
  • Items with no product binding (pure logistics stock, e.g. cleaning supplies) resolve to non_billableno-silent-zero invariant: a billable-intent movement that resolves unresolved writes a ledger row with qty×0 + needs_pricing=true flag and raises a stock_alerts row, rather than silently dropping the charge.
  • ⚠️ menu_items/billable_ledger/resolve_menu_price have no repo migration (live-only objects, created 2026-06-10, likely by a concurrent session). Before building on them: snapshot the live DDL into a checked-in migration (pg_dump --schema-only of the three objects) so the repo regains source-of-truth. Do not drop or recreate — they may be another session’s in-flight work.

7.3 Leakage reconciliation — the immediate revenue win

One view, huge demo value:

CREATE VIEW inventory_charge_leakage_v AS
SELECT m.* FROM stock_movement_log m
LEFT JOIN billable_ledger b ON b.metadata->>'movement_id' = m.mongo_transaction_id
WHERE m.movement_type IN ('stock_issue','stock_consumption')
  AND m.patient_hn IS NOT NULL
  AND b.id IS NULL;

Surfaced as a cashier/finance panel (“consumed but never charged”) + the AI coworker proposes the missing charges as cowork_proposals. This is the homogenized answer to “supplies charged at 0฿” — detection first, auto-capture after trust is established.


8. Invariants

  1. One funnel — only inventory-orchestrator writes the stock read model; adapters emit events.
  2. Append-onlystock_movement_log and billable_ledger never UPDATE quantities; corrections are new rows (void+repost).
  3. Consume = charge, but never block care — pricing failures alert, they never stop a dispense.
  4. No-silent-zero — unresolvable price ⇒ flagged ledger row + alert, never a dropped charge.
  5. Rules are data — country/site differences are rule rows + market-pack seeds; zero per-country code.
  6. AI is recommender-only — no autonomous stock writes, PO submissions, price changes, or alert suppression; accept/edit/reject is the training label.
  7. PHI-safe AI — item/location/quantity series only; patientHn stripped from any model payload.
  8. Fail-soft frontend — every surface renders from device cache with staleness badge when the stream is down.
  9. Off by default — rules ship is_active=false; AI behind per-tenant flag; default config = today’s behavior.
  10. Schema drift is debt — every live-DB object must have a checked-in migration before code depends on it.

9. Rollout

Phase Scope Effort Outcome
P0 — turn the stream onSHIPPED 2026-06-10 StockEventBridgeMixin accepts SUPABASE_SERVICE_ROLE_KEY (the var actually on EC2); 20260525c fixed (nested $$ bug — why it never applied) + applied, cron reads Vault secrets (project_url/service_role_key — hosted Supabase denies ALTER DATABASE SET); 20260610b REPLICA IDENTITY applied; fixed stockBalance.repository empty-$and Mongo BadValue (unfiltered lists 500’d); sync rewritten page-streamed + batch upserts + background mode (was per-row → WORKER_RESOURCE_LIMIT; sync HTTP cap 150s → 502); 20260610g plain unique index on mongo_stock_balance_id + fn_inventory_upsert_alerts RPC (PostgREST can’t target the COALESCE expression index nor the partial dedup index — every balance/alert upsert had been failing). Live-verified: 3,892 balances / 20,237 catalog items / 3,309 alerts; synthetic STOCK_BALANCE_CHANGED → orchestrator → cache+alert E2E pass. done All four inventory dashboards live with real data, drugs AND supplies
P1 — catalog homogenizationSHIPPED 2026-06-10 20260610c snapshots live menu_items/billable_ledger/resolve_menu_price DDL into the repo (drift closed); financial_product_id was already backfilled by the concurrent menu-items-sync session (34,194/34,205); item→SKU join = menu_items.metadata->>'stockItemRef' stamped by toMenuItemRow (re-ran POST /v2/financial/products/syncMenuItems: 45,000 projected, 9,712 stock-backed items carry the join key); encounterId/chargeIntent added to stock event payload + orchestrator passthrough done Ledger writers possible; repo = source of truth again
P2 — consume→chargeSHIPPED 2026-06-10 20260610e trg_stock_movement_post_ledger (fail-soft, idempotent on origin_ref, no-silent-zero, HN-fallback encounter lookup with 14-day recency bound + metadata.encounter_source audit flag) + inventory_charge_leakage_v. Scheme/payor enrichment from insurance_context + the finance panel UI = follow-up done (panel UI open) Supplies stop leaking revenue; auditable charge trail
P3 — rules engineSHIPPED 2026-06-10 20260610f inventory_rules (+resolve_inventory_rule() SQL, realtime, open-write RLS like cds_rules) + 7-rule draft seed library; shared TS evaluator functions/_shared/inventory-rules.ts wired into BOTH orchestrator (60s-TTL cache) and sync (fail-open: no active rules = today’s behavior); admin UI /admin/inventory-rules (InventoryRulesPage + inventoryRules.service). Market-pack seeds + typed per-kind predicate forms = follow-up done (seeds/forms open) Reorder/expiry/FEFO/par-level as live-editable data
P4 — device cache createPersistedRealtimeStore (IndexedDB) for catalog + scoped balances; staleness badge; offline op queue last M True cache-on-device, instant search, offline-tolerant
P5 — AI coworker + exploration supply-chain-coworker agent (reorder proposals, anomaly re-rank, AI-receive extraction); HDAP Inventory Explorer + movement archive + ask-the-data RPCs L AI-integrated supply chain, fully audited
P6 — silo adapters Blood / nutrition / compounding / OR-implant event adapters onto the spine M Everything consumable on one movement + charge spine

P0 is independent and should happen immediately. P1–P2 complete the menu-price-ledger M4 path for consumables. P3–P6 are independently shippable after P1.


10. Open questions

  1. Who owns the live menu_items projection? Created 2026-06-10 03:54 UTC with no repo migration — confirm whether a concurrent session/branch carries the migration before snapshotting DDL (consolidate-all-pockets rule).
  2. Cutover policy for drug charges — at HANDED_OVER (consumption) vs current acknowledgement-time SalesOrder: needs finance sign-off per market (NHSO claims reference order-time in some flows).
  3. Controlled-substance hard gates — which jurisdictions require blocking dispense on count variance (vs alert-only)? Determines which policy_gates rows ship per market pack.
  4. Movement archive home — dedicated partitioned table vs the medallion Bronze layer (ai-training-corpus.md). Leaning Bronze if the forecast work lands in the twin; dedicated table if finance needs 7-year retention first.
Ask Anything