design+state: Convex-Signal spike full design (40h/3-phase plan, 6 modular seams, Repository interface, auth seam decision) [Pi]
This commit is contained in:
parent
bd0009ea10
commit
04d204e965
3 changed files with 539 additions and 3 deletions
|
|
@ -2,12 +2,13 @@ v:1
|
||||||
|
|
||||||
# current-state
|
# current-state
|
||||||
|
|
||||||
ACTIVE: Claude SHIPPED + verified the Claude-side delegation trigger (caught + fixed Pi's $1->stdin bug and the Finding-4 dispatch-rule conflict; verified firing live). Diagnosed the NocoDB leads failure: the Signal base's data source is corrupted and creates PK-less tables (capture still works, delete/update don't). Ran a full Convex platform evaluation (Professional $25/dev/mo includes the BAA + HIPAA/SOC2 + daily backups, NOT Enterprise-gated; leads are PII-not-PHI so they stay outside the PHI boundary). Wrote the Convex-Signal spike brief and created a committed ship-status ledger (docs/ship-status-ledger.md).
|
ACTIVE: Pi designed the modular Convex-Signal spike from the brief (docs/convex-signal-spike-design-2026-06-24.md). Design covers module map with swappable seams, data-access interface (Repository portability hedge), Convex schema matching the readiness model, auth seam (Convex default, Clerk swappable), phased 40h spike plan (Phase 1 proves core readiness loop). Claude builds Phase 1 once repo exists.
|
||||||
|
|
||||||
NEXT (in order): (1) Kisa: dedup decision (same patient + device + DOS under two order numbers -> one worklist line or two?) -> unblocks Claude's readiness-model build. (2) Pi: design the modular Convex-Signal spike from docs/convex-signal-instance-brief-2026-06-24.md (separate repo signal-convex; swappable auth + data layer; reuse the readiness model). (3) Kisa: send the Convex BAA email + /pilot-prep labABLE Fri 2026-06-26 1:30 PM. (4) Claude: build the readiness model once dedup is decided. (5) Claude: research-versioning Layer-1 + add signal/docs/ to its scope (Audit Finding 5 — the reason design docs need manual commits). (6) After Pi's Convex design + Kisa approval: create signal-convex (Claude=GitHub, Forgejo needs Kisa/token), Claude builds the spike. FULL ship-status + reasons/dependencies: docs/ship-status-ledger.md.
|
NEXT (in order): (1) Kisa: dedup decision (same patient + device + DOS under two order numbers -> one worklist line or two?) -> unblocks Claude's readiness-model build. (2) Kisa: send the Convex BAA email + /pilot-prep labABLE Fri 2026-06-26 1:30 PM. (3) Claude: build the readiness model once dedup is decided from its brief (rename coverage->readiness, rules-to-config, patient/line/shipment grouping+verdict, device-keyed override, require-plan-type, frontend nesting). (4) Claude: create signal-convex repo (GitHub via `gh`, Forgejo needs Kisa's token) + build Phase 1 of the Convex-Signal spike from Pi's design (docs/convex-signal-spike-design-2026-06-24.md). (5) Claude: research-versioning Layer-1 + add signal/docs/ to its scope (Audit Finding 5 — the reason design docs need manual commits).
|
||||||
|
|
||||||
## Decisions (last 8)
|
## Decisions (last 8)
|
||||||
|
|
||||||
|
- 2026-06-24 [Pi]: Convex-Signal spike full design persisted to docs/convex-signal-spike-design-2026-06-24.md. 12 sections covering module map with 6 swappable seams (auth, data, readiness, storage, automations, notifications), Repository interface for backend portability, Convex schema (8 tables matching the readiness model), auth seam decision (Convex first, Clerk swappable), phased 40h/5-day spike plan (Phase 1 proves core readiness loop on Convex). Claude builds Phase 1 from this.
|
||||||
- 2026-06-24 [CC]: SHIPPED the Claude-side delegation trigger — dispatch-trigger.sh reads stdin JSON (fixed Pi's $1 bug), settings.json UserPromptSubmit hook wired, dispatch SKILL.md thresholds added, Finding-4 clarifying line so old+new dispatch rules don't conflict. Verified live. Audit Findings 1/2/4 (this build) closed; 3/8 Pi-side, 5/6/7/9 research-versioning/phase-2 and non-blocking.
|
- 2026-06-24 [CC]: SHIPPED the Claude-side delegation trigger — dispatch-trigger.sh reads stdin JSON (fixed Pi's $1 bug), settings.json UserPromptSubmit hook wired, dispatch SKILL.md thresholds added, Finding-4 clarifying line so old+new dispatch rules don't conflict. Verified live. Audit Findings 1/2/4 (this build) closed; 3/8 Pi-side, 5/6/7/9 research-versioning/phase-2 and non-blocking.
|
||||||
- 2026-06-24 [CC]: NocoDB leads diagnosed — Signal-base data source bdmhi02nks1pg6g is corrupted, creates PK-less tables (old leads + a fresh test table both no-PK; content base healthy). SQLite can't add a PK to a populated table (Route 1 failed). Capture still works (n8n webhook signal-cgm-lead inserts ok); 2 test rows un-deletable. Folded into the platform decision.
|
- 2026-06-24 [CC]: NocoDB leads diagnosed — Signal-base data source bdmhi02nks1pg6g is corrupted, creates PK-less tables (old leads + a fresh test table both no-PK; content base healthy). SQLite can't add a PK to a populated table (Route 1 failed). Capture still works (n8n webhook signal-cgm-lead inserts ok); 2 test rows un-deletable. Folded into the platform decision.
|
||||||
- 2026-06-24 [CC]: Convex evaluation (verified). Professional $25/dev/mo includes BAA + HIPAA/SOC2 reports + daily backups (NOT Enterprise). Leads = PII-not-PHI -> outside the PHI/Validated-Cloud boundary. AWS (BAA + RDS/S3 provisioned, parked) = proven fallback; Neon = cheaper managed Postgres. Decision: build a SEPARATE 'Signal on Convex' spike (repo signal-convex), modular + backend-swappable to limit lock-in. Brief: docs/convex-signal-instance-brief-2026-06-24.md.
|
- 2026-06-24 [CC]: Convex evaluation (verified). Professional $25/dev/mo includes BAA + HIPAA/SOC2 reports + daily backups (NOT Enterprise). Leads = PII-not-PHI -> outside the PHI/Validated-Cloud boundary. AWS (BAA + RDS/S3 provisioned, parked) = proven fallback; Neon = cheaper managed Postgres. Decision: build a SEPARATE 'Signal on Convex' spike (repo signal-convex), modular + backend-swappable to limit lock-in. Brief: docs/convex-signal-instance-brief-2026-06-24.md.
|
||||||
|
|
|
||||||
533
docs/convex-signal-spike-design-2026-06-24.md
Normal file
533
docs/convex-signal-spike-design-2026-06-24.md
Normal file
|
|
@ -0,0 +1,533 @@
|
||||||
|
# Convex-Signal Spike — Build Design
|
||||||
|
|
||||||
|
**Date:** 2026-06-24
|
||||||
|
**Design by:** Pi
|
||||||
|
**Builds by:** Claude
|
||||||
|
**Approves:** Kisa
|
||||||
|
**Source:** docs/convex-signal-instance-brief-2026-06-24.md
|
||||||
|
**Status:** Design input. This is a SEPARATE, parallel spike in repo `signal-convex`. The current Signal build keeps running.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. What This Spike Proves
|
||||||
|
|
||||||
|
One question answered with a build, not a whiteboard:
|
||||||
|
|
||||||
|
> Can Convex's reactive platform serve as Signal's data + backend layer (at $25/mo with a BAA) without losing the readiness model's integrity, the auth flexibility we need, or the ability to walk away if Convex doesn't work out?
|
||||||
|
|
||||||
|
The spike proves three things:
|
||||||
|
|
||||||
|
1. **Core readiness loop** — import CSV → grade → display worklist — on Convex
|
||||||
|
2. **Auth seam works** — the spike runs on Convex auth; swapping to Clerk is a config change, not a rewrite
|
||||||
|
3. **Data-access interface holds** — the readiness engine doesn't call Convex directly; it calls an interface
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Module Map (The Seams)
|
||||||
|
|
||||||
|
Six modular seams per the brief, but Koaned to ensure they're actually buildable, not over-granular.
|
||||||
|
|
||||||
|
```
|
||||||
|
convex/ ← Convex server-functions directory
|
||||||
|
├── schema.ts ← Convex schema (tables + indexes)
|
||||||
|
├── auth/
|
||||||
|
│ ├── AuthProvider.ts ← Interface: login, getUser, hasPermission
|
||||||
|
│ └── convexAuth.ts ← Default: Convex auth
|
||||||
|
│ └── clerkAuth.ts ← Seam: Clerk JWT validation (future)
|
||||||
|
│
|
||||||
|
├── data/ ← DATA-ACCESS LAYER (the keystone)
|
||||||
|
│ ├── Repository.ts ← Generic interface: get, list, create, update, delete
|
||||||
|
│ ├── PatientRepository.ts ← Patient + CoverageLine + Shipment queries
|
||||||
|
│ ├── DocItemRepository.ts ← DocItem CRUD
|
||||||
|
│ ├── OverrideRepository.ts ← Override CRUD
|
||||||
|
│ └── README.md ← How to swap the backend
|
||||||
|
│
|
||||||
|
├── readiness/ ← PORTABLE READINESS ENGINE
|
||||||
|
│ ├── engine.ts ← gradeLine(), rollupPatient(), computeVerdict()
|
||||||
|
│ ├── rules.ts ← load payer_rules.json, resolve rule by ID
|
||||||
|
│ ├── citations.ts ← build CascadeEntry records
|
||||||
|
│ ├── overrides.ts ← apply override, recompute, re-rollup
|
||||||
|
│ ├── payer_rules.json ← COPIED from signal/python-backend/config/
|
||||||
|
│ └── README.md ← This is portable; review before every copy
|
||||||
|
│
|
||||||
|
├── storage/
|
||||||
|
│ ├── FileStorage.ts ← Interface: upload, getUrl, delete
|
||||||
|
│ └── convexStorage.ts ← Default: Convex file storage
|
||||||
|
│ └── s3Storage.ts ← Seam: S3 (future)
|
||||||
|
│
|
||||||
|
├── automations/
|
||||||
|
│ ├── ScheduledChecks.ts ← Cron jobs: resupply reminders, expiry alerts
|
||||||
|
│ └── crons.ts ← Convex cron definitions
|
||||||
|
│
|
||||||
|
└── notifications/
|
||||||
|
├── NotificationService.ts ← Interface: sendAlert, notifyWorklistChange
|
||||||
|
└── navBotNotify.ts ← Default: NavBot pattern
|
||||||
|
└── emailNotify.ts ← Seam: SES/SendGrid (future)
|
||||||
|
|
||||||
|
src/ ← React app
|
||||||
|
├── App.tsx ← Auth provider + routing
|
||||||
|
├── convexClient.ts ← Convex client config
|
||||||
|
├── components/
|
||||||
|
│ ├── worklist/
|
||||||
|
│ │ ├── WorklistTable.tsx ← Reimagined on Convex reactive queries
|
||||||
|
│ │ └── PatientDetail.tsx ← Nested patient → device → doc view
|
||||||
|
│ ├── import/
|
||||||
|
│ │ └── CSVImport.tsx ← CSV upload → Convex mutation
|
||||||
|
│ └── auth/
|
||||||
|
│ └── AuthGate.tsx ← Auth seam adapter
|
||||||
|
│
|
||||||
|
└── lib/
|
||||||
|
└── readinessQueries.ts ← Convex-specific reactive query hooks
|
||||||
|
```
|
||||||
|
|
||||||
|
### Analogy
|
||||||
|
|
||||||
|
Think of it like a **modular synthesizer**:
|
||||||
|
|
||||||
|
- **Convex** = the powered Eurorack case (provides power, bus, physical space)
|
||||||
|
- **Readiness engine** = the oscillator module (does the core work, same module in any case)
|
||||||
|
- **Auth** = the MIDI interface (different models, same CV/gate output)
|
||||||
|
- **Data layer** = the patch cables (the interface is the jack standard; swap the module behind it)
|
||||||
|
- **Storage/notifications/automations** = utility modules (envelope generators, mixers)
|
||||||
|
- **`convexClient.ts` + reactive queries** = the speakers (Convex-specific output; swap the whole signal chain)
|
||||||
|
|
||||||
|
The readiness engine (oscillator) is the valuable module. Everything else exists to make it produce sound. Designing for swappable patch cables means you can move the oscillator to a different case without rewiring the whole rig.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Data-Access Interface (The Lock-In Hedge)
|
||||||
|
|
||||||
|
This is the real design tension. Convex's core value is **reactivity** — `useQuery()` auto-re-fetches when data changes. A naive DAO interface that returns plain data loses that.
|
||||||
|
|
||||||
|
**Compromise:** Two-layer design.
|
||||||
|
|
||||||
|
### Layer A — Convex-specific reactive queries (UI-facing)
|
||||||
|
|
||||||
|
These ARE Convex queries — they subscribe to data, auto-update, and ARE NOT portable. This is the Cost Of Doing Business with Convex.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// convex/readiness/queries.ts — Convex-specific, reactive
|
||||||
|
export const getWorklist = query({
|
||||||
|
args: { orgId: v.id("organizations") },
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
const patients = await ctx.db
|
||||||
|
.query("patients")
|
||||||
|
.withIndex("by_org", (q) => q.eq("orgId", args.orgId))
|
||||||
|
.collect();
|
||||||
|
const readinessEngine = new ReadinessEngine(ctx); // ctx.db → repository
|
||||||
|
return patients.map((p) => readinessEngine.rollupPatient(p));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Layer B — Repository interface (readiness-engine-facing)
|
||||||
|
|
||||||
|
The readiness engine never calls `ctx.db` directly. It calls a repository interface, which is implemented by a ConcreteRepository that wraps `ctx.db`. If the backend changes, implement a PostgresRepository that implements the same interface.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// convex/data/Repository.ts — portable interface
|
||||||
|
export interface Repository {
|
||||||
|
getPatient(patientId: Id<"patients">): Promise<Patient | null>;
|
||||||
|
listPatientsByOrg(orgId: Id<"organizations">): Promise<Patient[]>;
|
||||||
|
getCoverageLines(patientId: Id<"patients">): Promise<CoverageLine[]>;
|
||||||
|
createCoverageLine(line: NewCoverageLine): Promise<Id<"coverageLines">>;
|
||||||
|
updateCoverageLine(id: Id<"coverageLines">, fields: Partial<CoverageLine>): Promise<void>;
|
||||||
|
listDocItems(lineId: Id<"coverageLines">): Promise<DocItem[]>;
|
||||||
|
upsertDocItem(item: NewDocItem): Promise<Id<"docItems">>;
|
||||||
|
getOverride(patientHash: string, deviceType: string, docType: string): Promise<Override | null>;
|
||||||
|
setOverride(override: NewOverride): Promise<void>;
|
||||||
|
// ... etc
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// convex/data/convexRepository.ts — Convex-specific implementation
|
||||||
|
export class ConvexRepository implements Repository {
|
||||||
|
constructor(private ctx: QueryCtx | MutationCtx) {}
|
||||||
|
|
||||||
|
async getPatient(patientId: Id<"patients">) {
|
||||||
|
return this.ctx.db.get(patientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async listPatientsByOrg(orgId: Id<"organizations">) {
|
||||||
|
return this.ctx.db
|
||||||
|
.query("patients")
|
||||||
|
.withIndex("by_org", (q) => q.eq("orgId", orgId))
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The readiness engine:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// convex/readiness/engine.ts — portable, takes a Repository
|
||||||
|
export class ReadinessEngine {
|
||||||
|
constructor(private repo: Repository) {}
|
||||||
|
|
||||||
|
async gradeLine(patientId: Id<"patients">, deviceType: string, planId: string) {
|
||||||
|
const lines = await this.repo.getCoverageLines(patientId);
|
||||||
|
// ... grading logic, calls this.repo for all data access
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**What portability costs:** The reactive queries (`getWorklist`, `getPatientDetail`) ARE Convex-specific and would be rewritten if we move off Convex. The grading logic in `engine.ts` is fully portable. The cost of swappability is roughly 20% more code (the interface + implementation split) in exchange for 100% portable business logic. Worth it for a data-critical compliance engine.
|
||||||
|
|
||||||
|
**BYOD Postgres note:** Convex supports connecting an external Postgres database (bring-your-own-database). If we eventually want to own the data layer separately from Convex's runtime, this preserves that path without changing the repository interface at all — just swap which database the interface talks to.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Convex Schema + Readiness Model
|
||||||
|
|
||||||
|
### Schema
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// convex/schema.ts
|
||||||
|
import { defineSchema, defineTable } from "convex/server";
|
||||||
|
import { v } from "convex/values";
|
||||||
|
|
||||||
|
export default defineSchema({
|
||||||
|
organizations: defineTable({
|
||||||
|
name: v.string(),
|
||||||
|
clerkOrgId: v.optional(v.string()), // seam: Clerk org ID, null for Convex auth
|
||||||
|
createdAt: v.number(),
|
||||||
|
}),
|
||||||
|
|
||||||
|
patients: defineTable({
|
||||||
|
orgId: v.id("organizations"),
|
||||||
|
patientId: v.string(), // client's UUID
|
||||||
|
status: v.string(), // rolled-up status (derived)
|
||||||
|
}).index("by_org", ["orgId"]),
|
||||||
|
|
||||||
|
coverageLines: defineTable({
|
||||||
|
patientId: v.id("patients"),
|
||||||
|
deviceType: v.string(), // "dexcom_g7"
|
||||||
|
planType: v.string(), // REQUIRED: medicare | medicare_advantage | medicaid | commercial | unknown
|
||||||
|
planId: v.string(), // plan identifier from CSV mapping
|
||||||
|
gradeable: v.boolean(), // true only for CGM this phase
|
||||||
|
lineStatus: v.optional(v.string()), // derived
|
||||||
|
timingFlag: v.optional(v.string()),
|
||||||
|
}).index("by_patient", ["patientId"]),
|
||||||
|
|
||||||
|
shipments: defineTable({
|
||||||
|
lineId: v.id("coverageLines"),
|
||||||
|
dateOfService: v.string(), // YYYY-MM-DD
|
||||||
|
quantity: v.number(),
|
||||||
|
components: v.array(v.string()),
|
||||||
|
orderNumber: v.string(),
|
||||||
|
hcpcs: v.string(),
|
||||||
|
isCurrent: v.boolean(),
|
||||||
|
}).index("by_line", ["lineId"])
|
||||||
|
.index("by_line_dos", ["lineId", "dateOfService"]),
|
||||||
|
|
||||||
|
docItems: defineTable({
|
||||||
|
lineId: v.id("coverageLines"),
|
||||||
|
docType: v.string(), // swo | visit | pecos | pa | diagnosis
|
||||||
|
requiredState: v.string(), // REQUIRED | NOT_REQUIRED | NOT_EVALUATED
|
||||||
|
inputState: v.string(), // SUPPLIED | ABSENT
|
||||||
|
value: v.optional(v.string()),
|
||||||
|
source: v.string(), // csv | staff_override | computed
|
||||||
|
citation: v.optional(v.string()),
|
||||||
|
}).index("by_line", ["lineId"]),
|
||||||
|
|
||||||
|
overrides: defineTable({
|
||||||
|
orgId: v.id("organizations"),
|
||||||
|
patientId: v.id("patients"),
|
||||||
|
deviceType: v.string(),
|
||||||
|
docType: v.string(),
|
||||||
|
status: v.string(),
|
||||||
|
statusDate: v.optional(v.number()),
|
||||||
|
expiryDate: v.optional(v.number()),
|
||||||
|
confirmedBy: v.optional(v.string()),
|
||||||
|
confirmedAt: v.optional(v.number()),
|
||||||
|
reason: v.optional(v.string()),
|
||||||
|
}).index("by_override_key", ["orgId", "patientId", "deviceType", "docType"]),
|
||||||
|
|
||||||
|
cascadeEntries: defineTable({
|
||||||
|
patientId: v.id("patients"),
|
||||||
|
lineId: v.optional(v.id("coverageLines")),
|
||||||
|
docType: v.string(),
|
||||||
|
severity: v.string(), // blocker | alert
|
||||||
|
reason: v.string(),
|
||||||
|
ruleId: v.string(), // points to payer_rules.json key
|
||||||
|
clientInput: v.string(),
|
||||||
|
requirement: v.string(),
|
||||||
|
basis: v.string(),
|
||||||
|
rulesVersion: v.string(),
|
||||||
|
createdAt: v.number(),
|
||||||
|
}).index("by_patient", ["patientId"]),
|
||||||
|
|
||||||
|
uploads: defineTable({
|
||||||
|
orgId: v.id("organizations"),
|
||||||
|
fileName: v.string(),
|
||||||
|
uploadedAt: v.number(),
|
||||||
|
recordCount: v.number(),
|
||||||
|
status: v.string(), // processing | done | error
|
||||||
|
storageId: v.optional(v.id("_storage")),
|
||||||
|
}).index("by_org", ["orgId"]),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Function Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
convex/
|
||||||
|
├── auth.ts ← Auth helpers (login, register, session)
|
||||||
|
├── crons.ts ← Cron job definitions
|
||||||
|
│
|
||||||
|
├── patient/
|
||||||
|
│ ├── queries.ts ← getWorklist, getPatientDetail
|
||||||
|
│ └── mutations.ts ← importCSV, deduplicate
|
||||||
|
│
|
||||||
|
├── coverageLine/
|
||||||
|
│ ├── queries.ts ← getLinesByPatient
|
||||||
|
│ └── mutations.ts ← updatePlanType, recomputeLine
|
||||||
|
│
|
||||||
|
├── docItem/
|
||||||
|
│ ├── queries.ts ← getDocItemsByLine
|
||||||
|
│ └── mutations.ts ← setDocItemOverride
|
||||||
|
│
|
||||||
|
└── readiness/
|
||||||
|
├── queries.ts ← gradeLine (read-only grade), rollupPatient
|
||||||
|
└── mutations.ts ← recomputeAll, applyOverride
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Auth Seam
|
||||||
|
|
||||||
|
### Recommended Default: Convex auth for the spike
|
||||||
|
|
||||||
|
Rationale:
|
||||||
|
|
||||||
|
- **One less moving part.** The spike is proving Convex on Signal. Adding Clerk adds auth complexity without proving anything new.
|
||||||
|
- **Convex auth supports the same flows:** email/password, Google OAuth, magic link.
|
||||||
|
- **The seam stays clean** — the spike's components call `AuthProvider.getUser()` and `AuthProvider.hasPermission(orgId)`. The default implementation uses Convex auth. A Clerk implementation would validate Clerk JWT tokens.
|
||||||
|
- **Swappable later.** When/if the spike graduates, the auth backend can change without touching the readiness engine, worklist display, or any business logic.
|
||||||
|
|
||||||
|
### Seam interface
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// convex/auth/AuthProvider.ts
|
||||||
|
export interface AuthProvider {
|
||||||
|
getUserId(ctx: AuthCtx): Promise<string | null>;
|
||||||
|
getOrgId(ctx: AuthCtx): Promise<string | null>;
|
||||||
|
hasPermission(ctx: AuthCtx, orgId: string, permission: string): Promise<boolean>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Migration path to Clerk
|
||||||
|
|
||||||
|
1. Clerk issues JWT tokens → Convex validates Clerk's JWKS endpoint (already at `clerk.sttilsolutions.com/.well-known/jwks.json`)
|
||||||
|
2. Convex's `auth` middleware checks the Clerk JWT and extracts user + org
|
||||||
|
3. `AuthProvider.getUserId()` reads from Clerk's session, not Convex's
|
||||||
|
4. UI gate swaps from `<ConvexAuthGate>` to `<ClerkAuthGate>` — same props, same behavior
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Readiness Model Reuse Strategy
|
||||||
|
|
||||||
|
### Copy, not share (for the spike)
|
||||||
|
|
||||||
|
The readiness engine + `payer_rules.json` are **copied** into `signal-convex/convex/readiness/`. Rationale:
|
||||||
|
|
||||||
|
- **Isolation.** The spike is parallel. A shared package creates coupling between a production app and an experiment.
|
||||||
|
- **Speed.** Copying is faster than extracting a package. The spike needs to ship fast.
|
||||||
|
- **The readiness model is stable.** It was locked on 2026-06-23 and won't change during the spike.
|
||||||
|
|
||||||
|
### If both instances graduate
|
||||||
|
|
||||||
|
Extract a shared npm package (`@sttil/readiness-engine`) that both repos import. The engine is already designed to be portable (Repository interface, no I/O dependency) — this extraction is a mechanical step, not a redesign.
|
||||||
|
|
||||||
|
### What changes for Convex
|
||||||
|
|
||||||
|
The _logic_ is identical. What changes is how it's called:
|
||||||
|
|
||||||
|
- **Python → TypeScript.** The engine is rewritten from Python to TypeScript (Convex is TS-native). The logic is the same.
|
||||||
|
- **Convex types for IDs.** Instead of string patient_ids, Convex uses typed `Id<"patients">`.
|
||||||
|
- **Repository implementation.** Instead of Supabase SQL queries, the ConvexRepository uses `ctx.db.get/query/patch/insert/delete`.
|
||||||
|
- **No SQL migrations.** Convex schema changes are instant (no explicit migrations).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Repo Structure + Workflow Integration
|
||||||
|
|
||||||
|
### Repository
|
||||||
|
|
||||||
|
```
|
||||||
|
signal-convex/ ← NEW repo, separate from signal/
|
||||||
|
├── convex/ ← Convex server-functions directory
|
||||||
|
│ ├── schema.ts
|
||||||
|
│ ├── auth.ts
|
||||||
|
│ ├── crons.ts
|
||||||
|
│ ├── data/
|
||||||
|
│ ├── readiness/
|
||||||
|
│ ├── storage/
|
||||||
|
│ ├── patient/
|
||||||
|
│ ├── coverageLine/
|
||||||
|
│ ├── docItem/
|
||||||
|
│ └── notifications/
|
||||||
|
│
|
||||||
|
├── src/ ← React app (Vite + TypeScript)
|
||||||
|
│ ├── main.tsx
|
||||||
|
│ ├── convexClient.ts
|
||||||
|
│ ├── App.tsx
|
||||||
|
│ └── components/
|
||||||
|
│
|
||||||
|
├── package.json
|
||||||
|
├── convex.json ← Convex project config
|
||||||
|
├── tsconfig.json
|
||||||
|
├── vite.config.ts
|
||||||
|
├── CLAUDE.md ← Project instructions (like signal/CLAUDE.md)
|
||||||
|
├── context/
|
||||||
|
│ └── current-state.md ← Optional: state-sync for the spike
|
||||||
|
├── docs/
|
||||||
|
│ └── convex-signal-design-2026-06-24.md ← This doc
|
||||||
|
│
|
||||||
|
├── .github/
|
||||||
|
│ └── workflows/ ← CI: typecheck, lint, test on push
|
||||||
|
│ └── ci.yml
|
||||||
|
│
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
### Git workflow
|
||||||
|
|
||||||
|
- **Source of truth:** Forgejo (`git.forgejo.sttilsolutions.com:sttil/signal-convex`)
|
||||||
|
- **Mirror:** GitHub (via `git-push` skill convention)
|
||||||
|
- **Branch strategy:** `main` is the active spike. Feature branches for each phase.
|
||||||
|
- **Commit discipline:** research-versioning patterns apply. The readiness engine and payer_rules.json have an explicit COPY-FROM marker in commit messages.
|
||||||
|
|
||||||
|
### Operating model
|
||||||
|
|
||||||
|
- **Pi designs → Claude builds** (same as the current workflow)
|
||||||
|
- **state-sync** applies: `signal-convex/context/current-state.md` for spike state
|
||||||
|
- **ship-status-ledger** extended: add spike items alongside current-build items
|
||||||
|
- **research-versioning** skill: covers the new repo once the Stop hook is added
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Compliance Touchpoints + BAA Gate
|
||||||
|
|
||||||
|
### PHI boundary (copied from the existing model, same rules)
|
||||||
|
|
||||||
|
| Data | Status | Reason |
|
||||||
|
|------|--------|--------|
|
||||||
|
| patient_id (client UUID) | PII-level identifier | Same as current build — hashed in logs, only crosswalk key |
|
||||||
|
| Patient names, SSNs, DOBs, contacts | NEVER INGESTED | Architecture contract, same as current build |
|
||||||
|
| Device type, DOS, quantity | Non-PHI | Standard supply chain data |
|
||||||
|
| Payer name, plan type | Non-PHI | Payer policy data |
|
||||||
|
|
||||||
|
### Gate: BAA required BEFORE real PHI
|
||||||
|
|
||||||
|
The spike currently runs on mock/synthetic data. Before ANY real patient data touches it:
|
||||||
|
|
||||||
|
1. **Convex BAA must be signed** by Kisa (email drafted, costs known: $25/mo Professional)
|
||||||
|
2. **sttil-compliance-check** gate: run the compliance check on the Convex setup
|
||||||
|
3. **Attorney review** of the Convex BAA terms vs AWS BAA terms
|
||||||
|
4. **Data-handling doc update:** add Convex to the compliance docs in `signal/docs/compliance/`
|
||||||
|
|
||||||
|
### Built-in audit trail
|
||||||
|
|
||||||
|
Convex's Professional plan includes an audit log. No extra build needed for basic audit. The cascade entries (citations table) serve as the business-level audit trail.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Phased Spike Plan (Timeboxed)
|
||||||
|
|
||||||
|
Total estimate: **~40 engineer-hours** (Claude with Kisa review). Timeboxed to **5 calendar days** of focused work — can be parallelized with the readiness-model build on the current Signal repo.
|
||||||
|
|
||||||
|
### Phase 1 — Core Readiness Loop (prove the thesis, ~20h)
|
||||||
|
|
||||||
|
**What ships:** CSV in → grade → worklist display. Running on Convex auth.
|
||||||
|
|
||||||
|
| Step | What | Hours |
|
||||||
|
|------|------|-------|
|
||||||
|
| 1 | Create `signal-convex` repo, scaffold Convex + Vite project | 1 |
|
||||||
|
| 2 | Define schema (organizations, patients, coverageLines, shipments, docItems) | 2 |
|
||||||
|
| 3 | Implement Repository interface + ConvexRepository | 3 |
|
||||||
|
| 4 | Port readiness engine from Python to TypeScript (module 1: gradeLine + rollupPatient) | 6 |
|
||||||
|
| 5 | Copy `payer_rules.json`, build rules loader | 1 |
|
||||||
|
| 6 | Implement CSV upload mutation (parse → store → grade → cascade) | 3 |
|
||||||
|
| 7 | Build worklist display with reactive queries (patient list + status) | 3 |
|
||||||
|
| 8 | Implement auth: Convex auth gate, org-aware queries | 1 |
|
||||||
|
|
||||||
|
**Gate:** Kisa reviews Phase 1 before Phase 2 starts.
|
||||||
|
|
||||||
|
### Phase 2 — Auth Seam + Device-Keyed Override (~12h)
|
||||||
|
|
||||||
|
**What ships:** Override + recompute + re-rollup. Auth seam proven swappable.
|
||||||
|
|
||||||
|
| Step | What | Hours |
|
||||||
|
|------|------|-------|
|
||||||
|
| 1 | Implement Override schema + repository | 1 |
|
||||||
|
| 2 | Add override mutation + recomputeLine + re-rollupPatient | 3 |
|
||||||
|
| 3 | Build override UI in patient detail view | 2 |
|
||||||
|
| 4 | Implement AuthProvider interface + ClerkAuthProvider stub | 2 |
|
||||||
|
| 5 | Write tests: gradeLine with known inputs, override+recompute, dedup | 3 |
|
||||||
|
| 6 | Verification: swap to ClerkAuthProvider in config, confirm everything still works | 1 |
|
||||||
|
|
||||||
|
### Phase 3 — Hardening + Remaining Seams (~8h)
|
||||||
|
|
||||||
|
**What ships:** File storage (Convex), automations (cron reminders), notifications (NavBot), documentation.
|
||||||
|
|
||||||
|
| Step | What | Hours |
|
||||||
|
|------|------|-------|
|
||||||
|
| 1 | File storage seam: upload documents, attach to patients | 2 |
|
||||||
|
| 2 | Automation: schedule resupply-reminder checks | 2 |
|
||||||
|
| 3 | Notification: NavBot-compatible alert on new-lead / override-action-needed | 2 |
|
||||||
|
| 4 | README, CLAUDE.md, compliance notes, spike retrospective | 2 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. What Claude Builds First
|
||||||
|
|
||||||
|
### Claude builds Phase 1, Steps 1-8, in order
|
||||||
|
|
||||||
|
The priority is proving the core thesis: CSV-in → grade → worklist-display on Convex. Nothing else matters if this doesn't work.
|
||||||
|
|
||||||
|
**Explicitly NOT in Phase 1:**
|
||||||
|
|
||||||
|
- File storage (Phase 3)
|
||||||
|
- Automations (Phase 3)
|
||||||
|
- Notifications (Phase 3)
|
||||||
|
- Override + recompute (Phase 2 — need the core loop first to verify baseline correctness)
|
||||||
|
- Clerk auth (Phase 2 — prove the spike on Convex auth first, swap later)
|
||||||
|
- Export, multi-device display, or non-CGM grading
|
||||||
|
|
||||||
|
### Before Phase 1 starts
|
||||||
|
|
||||||
|
1. Kisa: send the Convex BAA email (item 8 in ship-status ledger)
|
||||||
|
2. Claude + Kisa: verify Convex Professional plan includes the BAA (already verified 2026-06-24 — reconfirm at sign-up)
|
||||||
|
3. Create `signal-convex` repo on Forgejo + GitHub
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Open Questions Resolved
|
||||||
|
|
||||||
|
### Convex auth vs Clerk for the spike
|
||||||
|
|
||||||
|
**Decision:** Convex auth as the default. The auth seam means Clerk is a config swap, not a rewrite. Reduces spike complexity without compromising future flexibility.
|
||||||
|
|
||||||
|
### Dedup key (carried from readiness-model decision)
|
||||||
|
|
||||||
|
**Pending Kisa's decision** — same patient + device + DOS under two order numbers → one worklist line or two? The spike design supports both. The `shipments` table has `orderNumber` as a displayed field (not a dedup key), and the dedup logic lives in `readiness/engine.ts`. When Kisa decides, one line changes in the engine.
|
||||||
|
|
||||||
|
### Data-access interface boundary
|
||||||
|
|
||||||
|
**Decision:** Repository interface between the readiness engine and the database. The reactive query layer (Convex-specific `convex/patient/queries.ts`) is the cost of Convex and is NOT portable. The tradeoff is documented in Section 3.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Effort Estimate Summary
|
||||||
|
|
||||||
|
| Phase | Hours | Builds | Key Risk |
|
||||||
|
|-------|-------|--------|----------|
|
||||||
|
| Phase 1 | ~20h | Claude | Porting readiness logic from Python to TS |
|
||||||
|
| Phase 2 | ~12h | Claude | Override + recompute complexity |
|
||||||
|
| Phase 3 | ~8h | Claude | Notification integration |
|
||||||
|
| **Total** | **~40h** | Claude | Timeboxed to 5 calendar days |
|
||||||
|
|
||||||
|
**Key risk:** The readiness engine rewrite from Python to TypeScript. The logic is stable and documented (readiness-model-brief-2026-06-23.md), but porting requires attention to edge cases (enum mapping, date handling, the dedup decision). Mitigation: code-auditor review after each Phase-1 step.
|
||||||
|
|
@ -15,7 +15,7 @@ Canonical, git-tracked record of **what shipped, what didn't, and the reason or
|
||||||
| 3 | Audit Finding 2 — `$1` bug → read stdin JSON | Claude | **SHIPPED** | caught + fixed during build | done |
|
| 3 | Audit Finding 2 — `$1` bug → read stdin JSON | Claude | **SHIPPED** | caught + fixed during build | done |
|
||||||
| 4 | Audit Finding 4 — old+new dispatch rules conflict | Claude | **SHIPPED** | added clarifying line (kept judgment fallback, marker = evaluate not auto-dispatch) | done |
|
| 4 | Audit Finding 4 — old+new dispatch rules conflict | Claude | **SHIPPED** | added clarifying line (kept judgment fallback, marker = evaluate not auto-dispatch) | done |
|
||||||
| 5 | Convex-Signal spike **brief** | Claude | **SHIPPED** (committed this wrap) | — | Pi designs from it |
|
| 5 | Convex-Signal spike **brief** | Claude | **SHIPPED** (committed this wrap) | — | Pi designs from it |
|
||||||
| 6 | Convex-Signal spike **design** | Pi | **NOT STARTED** | Dependency: brief (item 5, ready). Safe to start now. | hand brief to Pi |
|
| 6 | Convex-Signal spike **design** | Pi | **SHIPPED** | docs/convex-signal-spike-design-2026-06-24.md. 40h/3-phase plan; Module map + Repository interface + Convex schema + Auth seam (Convex default, Clerk swappable) + Phase 1 (core readiness loop) = what Claude builds first. | Claude builds Phase 1 from it |
|
||||||
| 7 | `signal-convex` repo creation | Claude (GitHub) + Kisa/token (Forgejo) | **PENDING** | Dependency: Pi design + Kisa approval (create once, correctly). Claude can do GitHub via `gh`; Forgejo has no API token here. | after design approved |
|
| 7 | `signal-convex` repo creation | Claude (GitHub) + Kisa/token (Forgejo) | **PENDING** | Dependency: Pi design + Kisa approval (create once, correctly). Claude can do GitHub via `gh`; Forgejo has no API token here. | after design approved |
|
||||||
| 8 | Convex BAA email (plan/cost/signing) | Kisa | **IN PROGRESS** | Kisa handling | Kisa sends |
|
| 8 | Convex BAA email (plan/cost/signing) | Kisa | **IN PROGRESS** | Kisa handling | Kisa sends |
|
||||||
| 9 | Readiness-model build (rename coverage→readiness, rules-to-config, grouping+verdict, device override, require-plan-type, frontend nesting) | Claude | **BLOCKED** | Dependency: Kisa's dedup decision (same patient + device + DOS under two order numbers → one worklist line or two?). Design ready: `docs/readiness-model-brief-2026-06-23.md` | Kisa decides → Claude builds |
|
| 9 | Readiness-model build (rename coverage→readiness, rules-to-config, grouping+verdict, device override, require-plan-type, frontend nesting) | Claude | **BLOCKED** | Dependency: Kisa's dedup decision (same patient + device + DOS under two order numbers → one worklist line or two?). Design ready: `docs/readiness-model-brief-2026-06-23.md` | Kisa decides → Claude builds |
|
||||||
|
|
@ -42,10 +42,12 @@ Canonical, git-tracked record of **what shipped, what didn't, and the reason or
|
||||||
5. **Fixed date:** labABLE `/pilot-prep` Fri 2026-06-26 1:30 PM.
|
5. **Fixed date:** labABLE `/pilot-prep` Fri 2026-06-26 1:30 PM.
|
||||||
|
|
||||||
## Dependencies summary
|
## Dependencies summary
|
||||||
|
|
||||||
- Readiness build ⟸ Kisa dedup decision.
|
- Readiness build ⟸ Kisa dedup decision.
|
||||||
- Convex spike build ⟸ Pi design ⟸ committed brief (ready) ; and ⟸ Kisa approval + repo creation.
|
- Convex spike build ⟸ Pi design ⟸ committed brief (ready) ; and ⟸ Kisa approval + repo creation.
|
||||||
- NocoDB/leads fix ⟸ data-platform decision ⟸ Convex spike evidence + BAA terms.
|
- NocoDB/leads fix ⟸ data-platform decision ⟸ Convex spike evidence + BAA terms.
|
||||||
- Design-doc durability ⟸ `signal/docs/` auto-versioning (until built, commit docs manually).
|
- Design-doc durability ⟸ `signal/docs/` auto-versioning (until built, commit docs manually).
|
||||||
|
|
||||||
## Maintenance
|
## Maintenance
|
||||||
|
|
||||||
Update this whenever an item ships or a blocker changes. Move shipped items to a `## Shipped (archive)` section when the table grows. Both agents reason from this; Kisa validates and redirects.
|
Update this whenever an item ships or a blocker changes. Move shipped items to a `## Shipped (archive)` section when the table grows. Both agents reason from this; Kisa validates and redirects.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue