{"slug":"architecture/ADAPTIVE_SPINE_VERIFICATION","title":"Adaptive Spine Model Verification Guide","section":"architecture","sectionLabel":"Architecture","status":"unclassified","type":"document","path":"architecture/ADAPTIVE_SPINE_VERIFICATION.mdx","bytes":17338,"description":null,"updated":null,"source":"integratewise/Integratewise-docs-wiki","content":"\n\n> **Tier C — Historical.** This document is a point-in-time snapshot preserved for reference. For current architecture, see [CANONICAL_PLATFORM_ARCHITECTURE.md](../architecture/CANONICAL_PLATFORM_ARCHITECTURE.md).\n\n**Purpose:** Verify that the Spine deepens as tools are connected, without breaking UX or metrics  \n**Principle:** System gracefully degrades (shallow → deep); never degrades  \n**Status:** Verification Checklist & Test Scenarios  \n\n---\n\n## The Adaptive Model in One Diagram\n\n```\nDay 1 (Minimal):\n  Account { id, name, domain, industry }\n  ↓ metrics: [ ] (no data to compute)\n  ↓ UI: \"Connect Salesforce to unlock forecasting\"\n\nDay 2 (Connected Salesforce):\n  Account { id, name, domain, industry, revenue, deal_count, arr_current, logo_status }\n  ↓ metrics: [revenue_health, logo_risk, expansion_opportunity]\n  ↓ UI: 3 new cards populated\n\nDay 5 (Connected LinkedIn):\n  Account { +employee_count, +funding_stage, +industry_peers }\n  ↓ metrics: [+headcount_to_revenue_ratio, +growth_trajectory]\n  ↓ UI: 2 new insights surfaced\n\nDay 10 (Connected Slack + Support):\n  Account { +slack_channel, +support_tickets, +nps_score, +csat }\n  ↓ metrics: [+engagement_velocity, +support_health, +churn_risk]\n  ↓ UI: 3 more intelligence cards\n\nNO BREAKING CHANGES. No metrics broke. No UI crashed. Just depth.\n```\n\n---\n\n## Adaptive Model Core Rules\n\n### Rule 1: Never Break on Missing Data\n\n**Principle:** If a field is null, the system doesn't error — it signals.\n\n```typescript\n// BAD (breaks on null)\nconst ratio = account.employee_count / account.revenue; // NaN if either null\n\n// GOOD (graceful)\nconst ratio = account.employee_count && account.revenue \n  ? account.employee_count / account.revenue \n  : null; // metric inactive, not errored\n```\n\n**Implementation:**\n- Metrics have `requires: ['employee_count', 'revenue']` array\n- At compute time: if ANY required field is null, metric returns `{ status: 'inactive', reason: 'missing_fields', missing: ['employee_count'] }`\n- Workspace shows: \"Connect LinkedIn to enable headcount analysis\"\n\n---\n\n### Rule 2: Metrics Activate Automatically\n\n**Principle:** When a field hydrates, ALL metrics using that field activate automatically.\n\n```typescript\n// Example: Revenue/Headcount ratio metric\n\nconst METRIC_RHR = {\n  id: 'M-SALES-02',\n  name: 'Revenue per Employee',\n  requires: ['revenue', 'employee_count'],\n  formula: (account) => account.revenue / account.employee_count,\n  active: (account) => !!account.revenue && !!account.employee_count\n};\n\n// Day 2: Active? NO (employee_count = null)\n// Day 5: Active? YES (LinkedIn connected, field hydrated)\n// NO CODE CHANGE NEEDED\n```\n\n**Implementation:**\n- Metrics have `active()` predicate\n- At query time, only active metrics computed\n- Workspace auto-discovers new active metrics\n\n---\n\n### Rule 3: Shallow Spine is Valid\n\n**Principle:** A tenant with 5 fields is not broken — just less intelligent.\n\n```typescript\n// Shallow account (valid)\nconst account = {\n  id: 'acc-123',\n  name: 'Acme Inc',\n  domain: 'acme.com',\n  industry: 'SaaS',\n  created_date: '2024-01-01'\n};\n\n// Can query Spine? YES ✅\n// Can compute health_score? NO — requires 8 fields\n// Can render UI? YES — account card shows 5 fields, rest are \"Connect tool\" prompts\n// Is Spine broken? NO — it's just shallow\n```\n\n**Implementation:**\n- Workspace renders what exists\n- For each null SIGNAL field, shows: \"Connect [tool] to unlock [feature]\"\n\n---\n\n### Rule 4: Adding Tools Doesn't Re-Normalize Old Data\n\n**Principle:** Historical records stay as-is. New tools enrich forward.\n\n```\nTimeline:\n  2024-01-01: Account created via HubSpot (5 fields hydrated)\n  2024-01-15: Salesforce connected\n              → New deals sync WITH hydrated fields\n              → Old account record NOT updated (stays 5 fields)\n              → Next sync cycle: Account enriched with Salesforce data\n  2024-01-16: Account now has 12 fields (original 5 + 7 from Salesforce)\n```\n\n**Implementation:**\n- Loader checks: \"Does entity already exist in Spine?\"\n  - If YES: Merge new fields (nulls don't overwrite, new values do)\n  - If NO: Insert full row\n- Upsert logic: `INSERT OR UPDATE, SET field = COALESCE(new_value, old_value)`\n- Result: Spine row grows over time, never shrinks\n\n---\n\n### Rule 5: SIGNAL Fields Guide the User\n\n**Principle:** Null SIGNAL fields are not errors — they're prompts.\n\n```typescript\n// Example: slack_channel_linked = null\n\n// Data layer:\n{ slack_channel_linked: null }\n\n// Workspace rendering:\n<div className=\"prompt\">\n  \n  <p>Connect Slack to enable real-time collaboration</p>\n  <Button onClick={openIntegration}>Connect Slack</Button>\n</div>\n```\n\n**Implementation:**\n- Workspace queries all SIGNAL fields\n- For each null SIGNAL, check if corresponding tool available\n- Render prompt with:\n  - Icon of tool\n  - Value proposition\n  - \"Connect [tool]\" button\n\n---\n\n## Verification Scenarios\n\n### Scenario 1: New User (Day 1)\n\n**Given:** User signs up, manually creates account\n\n**Account State:**\n```json\n{\n  \"id\": \"acc-abc123\",\n  \"name\": \"Acme Inc\",\n  \"domain\": \"acme.com\",\n  \"industry\": \"SaaS\",\n  \"created_date\": \"2024-06-01\",\n  \n  \"deal_count\": null,\n  \"arr_current\": null,\n  \"employee_count\": null,\n  \"logo_status\": null,\n  \"health_score\": null\n}\n```\n\n**Verify:**\n- ✅ Account queryable from Spine (SELECT * FROM spine_accounts WHERE id = 'acc-abc123')\n- ✅ UI renders: Account name, domain, industry (3 cards)\n- ✅ UI shows prompts: \"Connect Salesforce\", \"Connect LinkedIn\", etc.\n- ✅ Metrics query returns: All metrics `{ status: 'inactive', missing: [...] }`\n- ✅ Workspace doesn't crash\n\n**Test Code:**\n```typescript\ntest('Shallow account renders without breaking', async () => {\n  const account = await spine.query('acc-abc123');\n  expect(account).toBeDefined();\n  expect(account.name).toBe('Acme Inc');\n  expect(account.employee_count).toBeNull();\n  \n  const metrics = await intelligence.computeMetrics(account);\n  const activeMetrics = metrics.filter(m => m.status === 'active');\n  expect(activeMetrics.length).toBe(0); // All inactive\n  \n  const ui = await workspace.render(account);\n  expect(ui).toContain('Connect Salesforce');\n  expect(ui).not.toThrow();\n});\n```\n\n---\n\n### Scenario 2: User Connects Salesforce (Day 2)\n\n**Given:** Day 1 account; user connects Salesforce\n\n**Loader Action:**\n1. Fetch account from Salesforce\n2. Resolve CANONICAL_FIELDS: name, domain, employees, industry, revenue\n3. Upsert spine_accounts: `UPDATE WHERE id = 'acc-abc123', SET revenue = 5000000, ...`\n\n**New Account State:**\n```json\n{\n  \"id\": \"acc-abc123\",\n  \"name\": \"Acme Inc\",\n  \"domain\": \"acme.com\",\n  \"industry\": \"SaaS\",\n  \"created_date\": \"2024-06-01\",\n  \n  \"revenue\": 5000000,\n  \"deal_count\": 3,\n  \"arr_current\": 4500000,\n  \"logo_status\": \"customer\",\n  \"health_score\": null,\n  \"employee_count\": null\n}\n```\n\n**Verify:**\n- ✅ Existing fields preserved\n- ✅ New fields hydrated\n- ✅ Metrics that require revenue are NOW ACTIVE\n- ✅ Metrics that require employee_count still inactive\n- ✅ UI auto-discovers new active metrics (no page reload needed)\n- ✅ No data loss or errors\n\n**Test Code:**\n```typescript\ntest('Salesforce connection hydrates fields without breaking', async () => {\n  // Day 1 state\n  let account = await spine.query('acc-abc123');\n  expect(account.revenue).toBeNull();\n  \n  // Simulate Salesforce connector\n  const sfAccount = { \n    name: 'Acme Inc', \n    revenue: 5000000, \n    deals: 3 \n  };\n  await loader.sync(sfAccount, 'SALESFORCE', 'account');\n  \n  // Day 2 state\n  account = await spine.query('acc-abc123');\n  expect(account.name).toBe('Acme Inc'); // Original preserved\n  expect(account.revenue).toBe(5000000); // New hydrated\n  \n  // Metrics\n  const metrics = await intelligence.computeMetrics(account);\n  const activeMetrics = metrics.filter(m => m.status === 'active');\n  expect(activeMetrics.length).toBeGreaterThan(0); // Now has active metrics\n  \n  // Specific metrics\n  expect(metrics.find(m => m.id === 'M-SALES-01')?.status).toBe('active'); // Revenue-based metric\n  expect(metrics.find(m => m.id === 'M-SALES-02')?.status).toBe('inactive'); // Needs employee_count\n});\n```\n\n---\n\n### Scenario 3: User Connects LinkedIn (Day 5)\n\n**Given:** Day 2 account + Salesforce; user connects LinkedIn\n\n**Loader Action:**\n1. Fetch company from LinkedIn\n2. Enrich with: employee_count, funding_stage, industry_peers\n3. Upsert spine_accounts: `UPDATE WHERE id = 'acc-abc123', SET employee_count = 500, ...`\n\n**New Account State:**\n```json\n{\n  \"id\": \"acc-abc123\",\n  \"name\": \"Acme Inc\",\n  \"domain\": \"acme.com\",\n  \"industry\": \"SaaS\",\n  \"created_date\": \"2024-06-01\",\n  \n  \"revenue\": 5000000,\n  \"deal_count\": 3,\n  \"arr_current\": 4500000,\n  \"logo_status\": \"customer\",\n  \"employee_count\": 500,\n  \"funding_stage\": \"Series B\",\n  \"health_score\": null\n}\n```\n\n**Verify:**\n- ✅ All previous fields preserved\n- ✅ LinkedIn fields hydrated (employee_count, funding_stage)\n- ✅ Metrics that require employee_count NOW ACTIVE\n- ✅ Revenue-per-employee metric activates\n- ✅ UI discovers new metrics\n- ✅ No errors, no rerenders needed (reactive update)\n\n**Test Code:**\n```typescript\ntest('LinkedIn connection adds new metrics without breaking', async () => {\n  // Pre-LinkedIn\n  let account = await spine.query('acc-abc123');\n  expect(account.employee_count).toBeNull();\n  \n  const preMetrics = await intelligence.computeMetrics(account);\n  const preActiveCount = preMetrics.filter(m => m.status === 'active').length;\n  \n  // Connect LinkedIn\n  const linkedinCompany = { \n    name: 'Acme Inc',\n    employee_count: 500\n  };\n  await loader.sync(linkedinCompany, 'LINKEDIN', 'company');\n  \n  // Post-LinkedIn\n  account = await spine.query('acc-abc123');\n  expect(account.employee_count).toBe(500);\n  \n  const postMetrics = await intelligence.computeMetrics(account);\n  const postActiveCount = postMetrics.filter(m => m.status === 'active').length;\n  \n  expect(postActiveCount).toBeGreaterThan(preActiveCount); // More metrics active\n  expect(metrics.find(m => m.id === 'M-SALES-02')?.status).toBe('active'); // Revenue/employee\n});\n```\n\n---\n\n### Scenario 4: Adaptive Metrics Don't Break\n\n**Given:** Metrics formula depends on potentially-null fields\n\n**Scenario:** Revenue/Headcount ratio metric\n\n```typescript\n// Metric definition\nconst REVENUE_PER_EMPLOYEE = {\n  id: 'M-SALES-02',\n  name: 'Revenue per Employee',\n  formula: (account) => account.revenue / account.employee_count,\n  requires: ['revenue', 'employee_count'],\n  active: (account) => account.revenue && account.employee_count\n};\n\n// Day 1: Both null → Metric inactive\nconst day1 = { revenue: null, employee_count: null };\nexpect(REVENUE_PER_EMPLOYEE.active(day1)).toBe(false);\nexpect(intelligence.compute(REVENUE_PER_EMPLOYEE, day1)).toEqual({\n  status: 'inactive',\n  reason: 'missing_fields: revenue, employee_count'\n});\n\n// Day 2: Revenue hydrated, but employee_count still null → Still inactive\nconst day2 = { revenue: 5000000, employee_count: null };\nexpect(REVENUE_PER_EMPLOYEE.active(day2)).toBe(false); // One missing is enough\nexpect(intelligence.compute(REVENUE_PER_EMPLOYEE, day2)).toEqual({\n  status: 'inactive',\n  reason: 'missing_fields: employee_count'\n});\n\n// Day 5: Both hydrated → Now active\nconst day5 = { revenue: 5000000, employee_count: 500 };\nexpect(REVENUE_PER_EMPLOYEE.active(day5)).toBe(true);\nexpect(intelligence.compute(REVENUE_PER_EMPLOYEE, day5)).toEqual({\n  status: 'active',\n  value: 10000\n});\n```\n\n**Verify:**\n- ✅ Metric never throws error (handles null gracefully)\n- ✅ Metric status transitions: inactive → active when fields hydrate\n- ✅ UI prompts for missing fields\n- ✅ Workspace shows metric value only when active\n\n**Implementation:**\n```typescript\n// Safe metric computation\n  if (!metric.active(account)) {\n    return {\n      status: 'inactive',\n      reason: `missing_fields: ${metric.requires.filter(f => !account[f]).join(', ')}`,\n      missing: metric.requires.filter(f => !account[f])\n    };\n  }\n  \n  try {\n    const value = metric.formula(account);\n    return { status: 'active', value };\n  } catch (e) {\n    return { status: 'error', message: e.message };\n  }\n}\n```\n\n---\n\n### Scenario 5: Cross-Domain Adaptive Model\n\n**Given:** User connects Salesforce + Stripe + Zendesk\n\n**Day 1 (Salesforce only):**\n```\nAccount Spine:\n  - sales fields hydrated (deals, arr, stage)\n  - finance fields: null\n  - support fields: null\n  \nActive Metrics:\n  - M-SALES-01: Pipeline Health ✅\n  - M-SALES-02: Revenue per Employee ❌ (needs headcount)\n  - M-FINANCE-01: Revenue Forecast ❌ (needs invoice data)\n  - M-SUPPORT-01: CSAT Trend ❌ (needs support data)\n```\n\n**Day 2 (+ Stripe):**\n```\nAccount Spine:\n  - sales fields hydrated\n  - finance fields hydrated (invoices, revenue recognition)\n  - support fields: null\n  \nActive Metrics:\n  - M-SALES-01: Pipeline Health ✅\n  - M-SALES-02: Revenue per Employee ❌ (still needs headcount)\n  - M-FINANCE-01: Revenue Forecast ✅ (Stripe data ready)\n  - M-SUPPORT-01: CSAT Trend ❌ (needs support)\n```\n\n**Day 5 (+ Zendesk):**\n```\nAccount Spine:\n  - sales fields hydrated\n  - finance fields hydrated\n  - support fields hydrated (tickets, csat, sentiment)\n  \nActive Metrics:\n  - M-SALES-01: Pipeline Health ✅\n  - M-SALES-02: Revenue per Employee ❌ (still needs headcount)\n  - M-FINANCE-01: Revenue Forecast ✅\n  - M-SUPPORT-01: CSAT Trend ✅\n```\n\n**Verify:**\n- ✅ Adding Finance data doesn't break Sales metrics\n- ✅ Adding Support data doesn't break Finance or Sales metrics\n- ✅ Metrics activate independently (no cascading failures)\n- ✅ Metrics can depend on fields from different domains\n- ✅ UI smoothly transitions as new metrics become available\n\n---\n\n## Verification Checklist\n\n### Pre-Launch Verification\n\n- [ ] **Null Handling**\n  - [ ] No metric throws error on null field\n  - [ ] All metrics have `active()` predicate\n  - [ ] All metrics have `requires: [...]` array\n  - [ ] Inactive metrics shown as \"insufficient data\" in UI\n\n- [ ] **Field Hydration**\n  - [ ] Upsert logic correctly merges fields\n  - [ ] Nulls don't overwrite existing values\n  - [ ] New tools enrich forward (don't re-normalize history)\n  - [ ] Field hydration doesn't break queries\n\n- [ ] **Metric Activation**\n  - [ ] Metrics activate when all required fields present\n  - [ ] Metrics deactivate if ANY required field becomes null\n  - [ ] UI discovers new active metrics without reload\n  - [ ] Metric value doesn't cache (always fresh)\n\n- [ ] **UI Graceful Degradation**\n  - [ ] Workspace renders 0 active metrics (shallow account)\n  - [ ] Workspace renders 10+ active metrics (deep account)\n  - [ ] \"Connect tool\" prompts appear for null SIGNAL fields\n  - [ ] Cards don't show broken data (empty instead of NaN)\n\n- [ ] **Cross-Domain Integration**\n  - [ ] Sales + Finance metrics don't interfere\n  - [ ] CSM + Support metrics don't interfere\n  - [ ] Metrics from different domains can depend on same entity\n\n- [ ] **Error Handling**\n  - [ ] Partial connector sync doesn't break Spine\n  - [ ] Type mismatches handled gracefully\n  - [ ] Connector errors don't cascade to metrics\n  - [ ] Normalizer rejects bad data, not entire row\n\n---\n\n## Test Suite\n\n### Unit Tests (Adaptive Core)\n\n```typescript\ndescribe('Adaptive Spine Model', () => {\n  it('should compute metric with all fields', () => { ... });\n  it('should mark metric inactive if any required field null', () => { ... });\n  it('should not throw error on null field', () => { ... });\n  it('should update field without overwriting existing', () => { ... });\n  it('should activate metric when field hydrates', () => { ... });\n});\n```\n\n### Integration Tests (Full Flow)\n\n```typescript\ndescribe('Full Connector → Metric Flow', () => {\n  it('should render shallow account without error', () => { ... });\n  it('should hydrate fields on connector sync', () => { ... });\n  it('should activate metrics as fields hydrate', () => { ... });\n  it('should not break existing metrics on new tool', () => { ... });\n});\n```\n\n### E2E Tests (Real User Journey)\n\n```typescript\ndescribe('Real User Adaptive Journey', () => {\n  it('should handle Day 1→2→5 scenario', async () => { ... });\n  it('should work across 12 domains', async () => { ... });\n  it('should maintain data integrity over 30 days', async () => { ... });\n});\n```\n\n---\n\n## Implementation Checklist\n\n- [ ] Metrics have `active()` predicate ← **CRITICAL**\n- [ ] Metrics have `requires: [...]` array ← **CRITICAL**\n- [ ] Normalizer upsert uses `COALESCE(new, old)` ← **CRITICAL**\n- [ ] Workspace queries active metrics only\n- [ ] Workspace renders SIGNAL prompts for nulls\n- [ ] Intelligence layer doesn't error on null\n- [ ] Loader enriches forward (doesn't re-normalize)\n- [ ] All tests pass (unit + integration + E2E)\n\n---\n\n## Summary\n\n**The Adaptive Model guarantees:**\n\n✅ Shallow Spine is valid (not broken)  \n✅ Deep Spine has rich intelligence (without rewriting code)  \n✅ Adding tools never breaks existing functionality  \n✅ Metrics activate independently (no cascading)  \n✅ UI degrades gracefully (no crashed cards)  \n✅ Data integrity maintained (never loses fields)  \n\n**Result:** Tenants can start with 5 fields and grow to 50. Intelligence grows with them. No code changes. No migration scripts. Just depth.\n\n"}