{
  "id": "studio-e2244f07-0c89-460b-a26f-024b2af61fcd",
  "scope": "studio_project",
  "source_of_truth": "supabase.studio_artifacts",
  "source_path": "studio_artifacts/e2244f07-0c89-460b-a26f-024b2af61fcd",
  "source_kind": "supabase_json",
  "visibility": "internal",
  "renderer_id": "design_doc.dreamborn-forge.generated.v1",
  "design_system": "dreamborn-design-system:forge",
  "generated_at": "2026-05-09T13:00:56.413Z",
  "artifact_type": "policy",
  "schema_version": "studio_artifact.generated.v1",
  "title": "KnowledgeVault AI policy",
  "summary": "policy artifact · for KnowledgeVault AI · status approved",
  "status": "approved",
  "version": 1,
  "phase_id": null,
  "project_id": "572d75d0-3088-4055-be75-601d25442c61",
  "project_slug": "bezeliq-knowledgevault-ai-572d75d0",
  "project_title": "KnowledgeVault AI",
  "client_id": "bezeliq",
  "platform_project_id": "0.0.8773774",
  "format_source": "supabase_json_legacy_markdown_wrapped",
  "content_shape": [
    "body",
    "format",
    "generated_at",
    "generated_by"
  ],
  "sections": [
    {
      "title": "Artifact Shape",
      "level": 2,
      "body": "- body: # KnowledgeVault AI — Code & Merge Policy\n**project_id:** 572d75d0-3088-4055-be75-601d25442c61  \n**artifact_type:** policy  \n**generated_by:** vikram  \n**generated_at:** 2026-04-30  \n**status:** draft\n\n> **Quinn:** This policy is your pre-merge checklist. Hard rules block merges. Warn rules require human sign-off. Run each applicable check before closing any module PR.\n\n---\n\n## Rules\n\n### POL-SEC-001 — Row-Level Security on All User Tables\n| Field | Value |\n|---|---|\n| **Category** | security |\n| **Severity** | **HARD** |\n| **Applies to** | `supabase/migrations`, `apps/web/src/lib`, `apps/web/src/app/api` |\n\n**Description:** RLS must be ENABLED on every Supabase table storing user data (experts, sessions, transcripts, knowledge_items, assets, payouts, companies). No user-facing query may bypass RLS via service_role except in explicitly named admin server actions.\n\n**Check:**\n```sql\n-- Must return empty result set (all user tables have at least one policy)\nSELECT tablename FROM pg_tables\nWHERE schemaname = 'public'\n  AND tablename NOT IN (\n    SELECT tablename FROM pg_catalog.pg_policies\n    WHERE schemaname = 'public'\n  );\n```\nAlso:\n```bash\ngrep -r 'supabaseAdmin\\|service_role' apps/web/src \\\n  | grep -v '/api/admin/\\|/lib/server/admin.ts'\n# Must return zero matches\n```\n\n**Rationale:** KnowledgeVault stores proprietary trade knowledge and PII. A missing RLS policy is a full data breach — any authenticated user can read all rows.\n\n---\n\n### POL-SEC-002 — JWT Role Validation on Every API Route\n| Field | Value |\n|---|---|\n| **Category** | security |\n| **Severity** | **HARD** |\n| **Applies to** | `apps/web/src/app/api`, `apps/web/src/app/actions` |\n\n**Description:** Every Next.js API route and server action that returns data must call `supabase.auth.getUser()` and confirm the role claim before touching any DB row. Silent fail (returning empty data instead of 401/403) is not acceptable.\n\n**Check:**\n```bash\n# For each API route file, confirm getUser call is present\ngrep -rn 'export.*async.*POST\\|export.*async.*GET' apps/web/src/app/api \\\n  --include='*.ts' | while IFS=: read file line rest; do\n  grep -q 'getUser\\|auth.api.getUser' \"$file\" \\\n    || echo \"MISSING getUser: $file\"\ndone\n# Any output = hard block\n```\nAlso: any route returning 200 without `getUser` must be listed in `/src/lib/public-routes.ts`.\n\n**Rationale:** Without JWT verification, unauthenticated clients can POST directly to API routes. Supabase RLS alone is insufficient when service_role is used in server routes.\n\n---\n\n### POL-SEC-003 — Input Sanitization Before DB Writes and AI Calls\n| Field | Value |\n|---|---|\n| **Category** | security |\n| **Severity** | **HARD** |\n| **Applies to** | `apps/web/src/lib/sanitize.ts`, `apps/web/src/app/api`, `apps/web/src/app/actions` |\n\n**Description:** All user-supplied free-text (voice transcript output, expert profile fields, company descriptions, question answers) must pass through `sanitizeInput()` before being written to Supabase or passed to Claude/Deepgram. The utility must strip HTML tags, truncate to max field length defined in schema, and reject SQL injection patterns.\n\n**Check:**\n```bash\n# For each insert/update call site, confirm sanitizeInput appears within 30 lines above\ngrep -rn '\\.insert\\|\\.update' apps/web/src --include='*.ts' --include='*.tsx' \\\n  | while IFS=: read file line rest; do\n  context=$(sed -n \"$((line-30)),$((line))p\" \"$file\")\n  echo \"$context\" | grep -q 'sanitizeInput' \\\n    || echo \"MISSING sanitizeInput before DB write: $file:$line\"\ndone\n# Any output = hard block\n```\n\n**Rationale:** Transcript data originates from external audio and may contain adversarial content. Unsanitized strings fed to Claude or stored in pgvector embeddings can corrupt the knowledge base.\n\n---\n\n### POL-SEC-004 — No Secrets in Source Control\n| Field | Value |\n|---|---|\n| **Category** | security |\n| **Severity** | **HARD** |\n| **Applies to** | all source files, `.gitignore` |\n\n**Description:** No secret, API key, token, or credential may appear in committed source. Secrets permitted only in `.env.local` (gitignored), Vercel/CI environment variables, Supabase vault. All references in code must use `process.env.VAR_NAME`.\n\n**Check:**\n```bash\n# Scan staged diff for live secrets\ngit diff --cached -- '*.ts' '*.tsx' '*.js' '*.json' \\\n  | grep -E '(sk_live|sk_test|rk_live|DEEPGRAM_API_KEY=|whsec_)[A-Za-z0-9_\\-]{10,}'\n# Must return zero matches\n\n# Confirm .env is gitignored\ngrep -q '.env' .gitignore || echo \"FAIL: .env not in .gitignore\"\n```\n\n**Rationale:** Leaked Stripe live keys allow unauthorized payouts. Leaked Supabase service_role key bypasses all RLS.\n\n---\n\n### POL-ARCH-001 — Sequential, Immutable Migrations\n| Field | Value |\n|---|---|\n| **Category** | architecture |\n| **Severity** | **HARD** |\n| **Applies to** | `supabase/migrations` |\n\n**Description:** Migration files must be named `NNN_<slug>.sql` with strictly sequential NNN. Existing committed migrations must never be modified — new behaviour requires a new file. Migrations must be idempotent (`CREATE TABLE IF NOT EXISTS`, `CREATE INDEX IF NOT EXISTS`).\n\n**Check:**\n```bash\n# Check for gaps in sequence\nls supabase/migrations/ | sort | awk -F_ '{print $1}' \\\n  | awk 'NR>1 && $1 != prev+1 {print \"Gap at \" $1; exit 1} {prev=$1}'\n# Must exit 0\n\n# No deletions from existing migration files\ngit diff HEAD~1 -- 'supabase/migrations/*.sql' | grep '^-[^-]'\n# Must be empty\n```\n\n**Rationale:** Out-of-order or modified migrations corrupt production DB state. Supabase tracks applied migrations by filename — a modified file diverges from local dev state.\n\n---\n\n### POL-ARCH-002 — Acyclic Module Dependency Graph\n| Field | Value |\n|---|---|\n| **Category** | architecture |\n| **Severity** | **HARD** |\n| **Applies to** | `apps/web/src/modules`, `apps/mobile/src/modules` |\n\n**Description:** Module dependency graph must be acyclic and match the decomp artifact. M-00 (Foundation) has no dependencies. Each module M-NN may only import from modules with a lower index. Cross-module imports outside the declared dependency list in PLAN.json are forbidden.\n\n**Check:**\n```bash\nnode scripts/check-module-deps.js\n# Exit code 1 = hard block\n# Script must exist in repo before Phase 2 modules ship\n```\n\n**Rationale:** Circular module dependencies cause Next.js build failures and make phased rollouts impossible.\n\n---\n\n### POL-ARCH-003 — Service Role Key Must Never Reach Client Bundle\n| Field | Value |\n|---|---|\n| **Category** | architecture |\n| **Severity** | **HARD** |\n| **Applies to** | `apps/web/src/lib/server`, `apps/web/src/app/api` |\n\n**Description:** `SUPABASE_SERVICE_ROLE_KEY` must never be imported in any file that could be bundled into client-side JS. It must only appear in files under `apps/web/src/lib/server/` or `apps/web/src/app/api/`. Server components using the key must not pass it as a prop or embed it in serialised payloads.\n\n**Check:**\n```bash\ngrep -rn 'SUPABASE_SERVICE_ROLE_KEY\\|supabaseAdmin' \\\n  apps/web/src/components/ apps/web/src/app \\\n  --include='*.tsx' --include='*.ts' \\\n  | grep -v '/api/' | grep -v 'server.ts'\n# Must return zero matches\n```\n\n**Rationale:** Next.js 15 can accidentally bundle server-only env vars into client chunks if imported transitively. Service role exposure is a complete RLS bypass.\n\n---\n\n### POL-STYLE-001 — TypeScript Strict Mode, No Unguarded `any`\n| Field | Value |\n|---|---|\n| **Category** | style |\n| **Severity** | **HARD** |\n| **Applies to** | `apps/web`, `apps/mobile` |\n\n**Description:** `\"strict\": true` must be set in `tsconfig.json`. The `any` type is forbidden without an explicit `// vikram-allow-any: <reason>` comment on the same line. `@ts-ignore` and `@ts-expect-error` are forbidden in production code paths.\n\n**Check:**\n```bash\n# Confirm strict mode\ncat apps/web/tsconfig.json | jq '.compilerOptions.strict'\n# Must equal true\n\n# No bare any or ts-ignore in non-test files\ngrep -rn '@ts-ignore\\|as any' apps/web/src --include='*.ts' --include='*.tsx' \\\n  | grep -v '__tests__\\|\\.test\\.' \\\n  | grep -v 'vikram-allow-any'\n# Must return zero matches\n```\n\n**Rationale:** Implicit `any` types in payment or auth code are a class of runtime error that strict mode eliminates at compile time.\n\n---\n\n### POL-STYLE-002 — Typed Error Handling on All Async I/O\n| Field | Value |\n|---|---|\n| **Category** | style |\n| **Severity** | **WARN** |\n| **Applies to** | `apps/web/src`, `apps/mobile/src` |\n\n**Description:** All async functions calling Supabase, Claude, Deepgram, Stripe, or Resend must wrap calls in `try/catch` and return a typed `Result<T, AppError>` or throw a typed `AppError`. Empty catch blocks are forbidden.\n\n**Check:**\n```bash\n# No empty catch blocks\ngrep -rn 'catch\\s*{\\s*}\\|catch(e)\\s*{}\\|catch(_)\\s*{}' \\\n  apps/web/src apps/mobile/src\n# Must return zero matches (WARN: if violations found, list for Vikram review)\n```\n\n**Rationale:** Unhandled promise rejections in audio processing or payment flows fail silently in production. Typed errors allow the UI to show actionable messages rather than blank states.\n\n---\n\n### POL-PERF-001 — HNSW/IVFFlat Index Required on All Embedding Columns\n| Field | Value |\n|---|---|\n| **Category** | performance |\n| **Severity** | **HARD** |\n| **Applies to** | `supabase/migrations`, `apps/web/src/lib/search.ts` |\n\n**Description:** Every pgvector `embedding` column (e.g., `knowledge_items.embedding`, `assets.embedding`) must have an HNSW or IVFFlat index created in its migration before the module ships. Similarity search queries (`<=>`) must use the index. **p95 target: <800ms at 10k knowledge_items.**\n\n**Check:**\n```sql\n-- For each embedding column, confirm an index exists\nSELECT t.tablename, a.attname\nFROM pg_attribute a\nJOIN pg_class c ON a.attrelid = c.oid\nJOIN pg_tables t ON t.tablename = c.relname\nWHERE a.atttypid = 'vector'::regtype::oid\n  AND t.schemaname = 'public'\n  AND NOT EXISTS (\n    SELECT 1 FROM pg_indexes i\n    WHERE i.tablename = t.tablename\n      AND (i.indexdef ILIKE '%hnsw%' OR i.indexdef ILIKE '%ivfflat%')\n  );\n-- Must return zero rows\n```\n```sql\n-- Confirm query uses index (not seqscan)\nEXPLAIN (ANALYZE, BUFFERS)\nSELECT id FROM knowledge_items\nORDER BY embedding <=> $1 LIMIT 10;\n-- Plan must contain 'Index Scan', not 'Seq Scan'\n```\n\n**Rationale:** pgvector seqscans on un-indexed columns degrade to O(n). At 10k knowledge items an unindexed search takes 4-8 seconds — beyond acceptable UX for distributor semantic search.\n\n---\n\n### POL-DATA-001 — Foreign Key Constraints on All Relational Columns\n| Field | Value |\n|---|---|\n| **Category** | data_integrity |\n| **Severity** | **HARD** |\n| **Applies to** | `supabase/migrations`, `docs/schema-fk-manifest.md` |\n\n**Description:** All columns referencing another table's primary key must declare a `FOREIGN KEY` constraint with explicit `ON DELETE` behaviour (RESTRICT, CASCADE, or SET NULL). Junction tables (e.g., `session_experts`) must cascade delete when either parent is deleted. Implicit application-level joins with no FK are forbidden.\n\n**Check:**\n```bash\n# Diff actual FK constraints against manifest\nnode scripts/check-fk-manifest.js\n# Exit code 1 = hard block (script must be in repo by M-01 merge)\n```\n```sql\n-- Detect _id columns with no FK (candidate violations)\nSELECT column_name, table_name\nFROM information_schema.columns\nWHERE table_schema = 'public'\n  AND column_name LIKE '%_id'\n  AND (table_name, column_name) NOT IN (\n    SELECT c.relname, a.attname\n    FROM pg_constraint con\n    JOIN pg_class c ON con.conrelid = c.oid\n    JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum = ANY(con.conkey)\n    WHERE con.contype = 'f'\n  );\n-- Each row must be explained in docs/schema-fk-manifest.md\n```\n\n**Rationale:** Without FK constraints, orphaned transcript rows accumulate when experts are deleted, wasting pgvector storage and surfacing stale knowledge in search results.\n\n---\n\n### POL-DATA-002 — Nullable Column Policy\n| Field | Value |\n|---|---|\n| **Category** | data_integrity |\n| **Severity** | **WARN** |\n| **Applies to** | `supabase/migrations` |\n\n**Description:** New columns must be `NOT NULL` with a `DEFAULT` unless absence of a value is semantically meaningful. Nullable columns must have a migration comment `-- nullable: <reason>`. Boolean columns must **never** be nullable (`NOT NULL DEFAULT false`). `created_at` timestamps must be `NOT NULL`; state-transition timestamps (e.g., `completed_at`) may be nullable.\n\n**Check:**\n```sql\n-- Boolean nullable columns (hard within warn category)\nSELECT column_name, table_name\nFROM information_schema.columns\nWHERE table_schema = 'public'\n  AND data_type = 'boolean'\n  AND is_nullable = 'YES';\n-- Must return zero rows\n```\n```bash\n# New nullable columns in PR diff must have -- nullable: comment in migration\ngit diff HEAD~1 -- 'supabase/migrations/*.sql' \\\n  | grep '^\\+.*NULL' | grep -v 'NOT NULL' | grep -v '-- nullable:'\n# WARN: each result reviewed by Vikram before merge\n```\n\n**Rationale:** Nullable booleans introduce three-valued logic bugs in RLS policies. Undocumented nullable columns scatter NULL checks across the codebase.\n\n---\n\n### POL-PAY-001 — Payment Hold Release Conditions\n| Field | Value |\n|---|---|\n| **Category** | payment_compliance |\n| **Severity** | **HARD** |\n| **Applies to** | `apps/web/src/lib/payments.ts`, `apps/web/src/app/api/webhooks/stripe` |\n\n**Description:** A `payment_hold` record may only be released (status → `'released'`) and a Stripe Connect payout triggered if **ALL** of the following are true:\n1. `knowledge_score >= platform_config.payout_min_score`\n2. `session.status = 'completed'`\n3. No open dispute record exists for the session\n4. Expert's Stripe Connect account is in `charges_enabled` state\n\nAny code path calling `stripe.transfers.create` or updating hold status to `'released'` without all four checks is a **hard block**.\n\n**Check:**\n```bash\n# Every payout trigger must call checkPayoutEligibility() or contain all 4 guards\ngrep -rn 'stripe.transfers.create\\|status.*released\\|released.*status' \\\n  apps/web/src --include='*.ts' | while IFS=: read file line rest; do\n  grep -q 'checkPayoutEligibility\\|payout_min_score\\|charges_enabled' \"$file\" \\\n    || echo \"MISSING eligibility check: $file:$line\"\ndone\n# Any output = hard block\n\n# Unit test coverage required\nls tests/payout-eligibility.test.ts || echo \"FAIL: payout eligibility test file missing\"\n```\n\n**Rationale:** Premature payout release exposes the platform to chargeback losses. Stripe Connect payouts to un-enabled accounts fail silently and strand funds.\n\n---\n\n### POL-PAY-002 — Stripe Webhook Signature Verification and Idempotency\n| Field | Value |\n|---|---|\n| **Category** | payment_compliance |\n| **Severity** | **HARD** |\n| **Applies to** | `apps/web/src/app/api/webhooks/stripe`, `supabase/migrations` |\n\n**Description:** All Stripe webhook handlers must verify the `stripe-signature` header using `stripe.webhooks.constructEvent()` with the **raw** request body before processing. Handlers must be idempotent — processing the same `event.id` twice must not double-release a hold or double-trigger a payout. Event IDs must be recorded in `stripe_webhook_events` with a `UNIQUE` constraint on `event_id`.\n\n**Check:**\n```bash\n# All webhook handler files must call constructEvent\ngrep -rn 'constructEvent\\|stripe.webhooks' apps/web/src/app/api/webhooks/stripe \\\n  --include='*.ts'\n# Must have at least one match per handler file\n\n# Confirm UNIQUE constraint on stripe_webhook_events.event_id\n```\n```sql\nSELECT indexname FROM pg_indexes\nWHERE tablename = 'stripe_webhook_events'\n  AND indexdef ILIKE '%unique%';\n-- Must return >= 1 row\n```\n```bash\ngrep -rn 'stripe_webhook_events' supabase/migrations | grep -i 'unique'\n# Must match\n```\n\n**Rationale:** Stripe retries webhook delivery on failure. Without signature verification, any HTTP client can spoof a payment event. Without idempotency, a retry triggers a duplicate payout.\n\n---\n\n## Quinn's Pre-Merge Checklist\n\nRun these checks for every module PR before requesting merge:\n\n1. [ ] **POL-SEC-001** — RLS SQL check against branch migration state\n2. [ ] **POL-SEC-002** — getUser grep on all API route files in PR diff\n3. [ ] **POL-SEC-003** — sanitizeInput grep on all insert/update call sites in diff\n4. [ ] **POL-SEC-004** — Secret leak scan on `git diff --cached`\n5. [ ] **POL-ARCH-001** — Migration sequence check (no gaps, no modifications)\n6. [ ] **POL-ARCH-002** — `node scripts/check-module-deps.js` exits 0\n7. [ ] **POL-ARCH-003** — Service key client bundle grep returns zero matches\n8. [ ] **POL-STYLE-001** — tsconfig strict=true + no bare `any` grep\n9. [ ] **POL-STYLE-002** (warn) — Empty catch grep; escalate to Vikram if >3 violations\n10. [ ] **POL-PERF-001** — EXPLAIN ANALYZE on affected embedding queries shows Index Scan\n11. [ ] **POL-DATA-001** — `node scripts/check-fk-manifest.js` exits 0\n12. [ ] **POL-DATA-002** (warn) — New nullable columns have `-- nullable:` comments\n13. [ ] **POL-PAY-001** — Payout eligibility grep + `tests/payout-eligibility.test.ts` exists\n14. [ ] **POL-PAY-002** — constructEvent grep + stripe_webhook_events UNIQUE index confirmed\n\n**Hard rule fail action:** Block merge. Post `task.blocked` to Vikram with rule ID and `file:line` reference.  \n**Warn rule fail action:** Flag in PR comment. Require explicit approval (Justin or Vikram) before merge.\n\n---\n\n## Supplementary Notes\n\n**NOTE — Unapplied Migration:** `supabase/migrations/030_studio_artifacts_unique_constraint.sql` is untracked in the repo. This migration must be applied before `write_studio_artifact` upserts will succeed for any agent. Quinn should flag this to Quinn/developer for application in the next deploy window.\n\n- generated at: 2026-04-30\n- generated by: vikram"
    }
  ],
  "html_path": "projects/bezeliq-knowledgevault-ai-572d75d0/artifacts/policy-e2244f07.html",
  "json_path": "projects/bezeliq-knowledgevault-ai-572d75d0/artifacts/policy-e2244f07.json"
}