{
  "audited_against_commit": "a71cb75b4af9773acefc182d5bc9631da7ab0dc3",
  "specs": [
    "docs/specs/2026-05-30-crm-support-center.md",
    "docs/specs/2026-06-01-settings-crm.md"
  ],
  "summary": {
    "p0": 3,
    "p1": 15,
    "p2": 14
  },
  "findings": [
    {
      "id": "csc-001",
      "severity": "P0",
      "dimension": "spec",
      "title": "Inbound createSupportTicket seam is a throwing stub — Telegram/WhatsApp/Slack never create tickets",
      "location": "apps/zync-api/src/queues/comms-inbound.ts:48",
      "evidence": "createSupportTicketStub throws `createSupportTicket not wired: crm-support-center must inject the implementation` and is passed to routeInboundMessage at line 96. No real implementation exists anywhere under apps/zync-api.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "csc-002",
      "severity": "P0",
      "dimension": "spec",
      "title": "Staff reply saves message only — no source-aware outbound routing to email/Telegram/WhatsApp/portal",
      "location": "apps/zync-api/src/routes/support/index.ts:255",
      "evidence": "POST /:id/reply inserts ticket_messages then returns 201. Comment at lines 255–258 states channel dispatch is future work. No sendEmail, Telegram sendMessage, portal notification, or webhook enqueue in handler.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "csc-003",
      "severity": "P0",
      "dimension": "spec",
      "title": "Support UI navigation uses /support paths but routes are mounted at /crm/support",
      "location": "apps/zync-app/src/pages/support/SupportPage.tsx:236",
      "evidence": "Ticket list onClick navigates `/support/${ticket.id}`; TicketDetailPage back button navigates `/support` (lines 167, 182). CRM module only registers `support` and `support/:id` under /crm (apps/zync-app/src/modules/crm.tsx:19–33). No top-level /support route.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "csc-004",
      "severity": "P1",
      "dimension": "spec",
      "title": "Email inbound support ticket path not implemented",
      "location": "apps/zync-api/src/intake/email-routing.ts:93",
      "evidence": "Spec requires Cloudflare Email Routing → Worker → Queue → ticket create/thread append. Repo has intake/email-expense.ts for expenses only; no email→ticket parser or comms.inbound enqueue for support. createSupportTicket stub would still block even if email worker existed.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "csc-005",
      "severity": "P1",
      "dimension": "spec",
      "title": "Kanban board default view with drag-between-columns not implemented",
      "location": "apps/zync-app/src/pages/support/SupportPage.tsx:219",
      "evidence": "Spec default view is Kanban columns by status with drag → PATCH status. SupportPage renders a flat Card list of TicketRow components; no column layout, no dnd-kit, no status PATCH on drag.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "csc-006",
      "severity": "P1",
      "dimension": "spec",
      "title": "Status lifecycle system transition messages absent",
      "location": "packages/db/src/queries/support.ts:359",
      "evidence": "updateTicket sets status/resolvedAt/closedAt only. No createTicketMessage with author_type=system on transitions. closeStaleResolvedTickets bulk-updates to closed with no system messages (lines 598–612).",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "csc-007",
      "severity": "P1",
      "dimension": "spec",
      "title": "autoReopenOnCustomerReply not implemented",
      "location": "packages/db/src/queries/support.ts:259",
      "evidence": "findTicketByThread exists for inbound threading but has zero callers outside support.ts. Portal reply (apps/zync-api/src/routes/portal/tickets.ts:212) and staff reply paths never reopen pending_customer → in_progress. grep autoReopenOnCustomerReply returns no implementation.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "csc-008",
      "severity": "P1",
      "dimension": "spec",
      "title": "Outbound webhook events ticket.created/replied/resolved never emitted from ticket handlers",
      "location": "apps/zync-api/src/routes/support/index.ts:153",
      "evidence": "grep emitWebhook|webhook.deliver|ticket.created in apps/zync-api/src/routes/support returns zero. POST/PATCH/reply handlers commit DB rows only.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "csc-009",
      "severity": "P1",
      "dimension": "spec",
      "title": "TenantRealtime DO broadcast not wired on ticket create/update/reply",
      "location": "apps/zync-app/src/pages/support/TicketDetailPage.tsx:137",
      "evidence": "realtime hooks document ticket.message_added (lib/realtime/hooks.ts:18) but SupportPage/TicketDetailPage do not call useRealtimeEvent. Support API routes never import broadcast helper.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "csc-010",
      "severity": "P1",
      "dimension": "spec",
      "title": "SLA due_at and first_response_at helpers exist but are not wired into ticket create/reply",
      "location": "apps/zync-api/src/routes/support/index.ts:153",
      "evidence": "computeAndSetDueAt and markFirstResponse exported from packages/db/src/queries/ticket-sla.ts but grep shows zero usage in support routes. POST /api/tickets and POST /:id/reply never call them despite sla_policies schema and cron existing.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "csc-011",
      "severity": "P1",
      "dimension": "spec",
      "title": "Start timer control missing from ticket detail sidebar",
      "location": "apps/zync-app/src/pages/support/TicketDetailPage.tsx:199",
      "evidence": "Spec requires Start timer popover → POST /api/time/start with source=auto. Sidebar shows status, priority, source, created only. StartTimerPopover exists elsewhere but is not imported in support pages.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "csc-012",
      "severity": "P1",
      "dimension": "spec",
      "title": "Sidebar nav /crm has no index route — support center unreachable from default nav target",
      "location": "apps/zync-app/src/shell/nav-model.ts:126",
      "evidence": "nav-model Support Center link to='/crm'. crm module (apps/zync-app/src/modules/crm.tsx) defines only support and support/:id child routes with no index redirect.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "csc-013",
      "severity": "P1",
      "dimension": "spec",
      "title": "Auto-close cron uses hardcoded 7-day window — no tenant_settings.ticket_auto_close_days",
      "location": "apps/zync-api/src/routes/cron/ticket-close-stale.ts:33",
      "evidence": "closeStaleResolvedTickets(db, 7) called with literal 7. grep ticket_auto_close in packages/db/src/schema returns zero column. Spec requires configurable tenant_settings.ticket_auto_close_days (default 7).",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "csc-014",
      "severity": "P1",
      "dimension": "security",
      "title": "Ticket message create/reply stores raw content without server-side HTML allowlist sanitization",
      "location": "apps/zync-api/src/routes/support/index.ts:243",
      "evidence": "Staff reply passes parsed.data.content directly to createTicketMessage. Task messages route uses sanitizeCommentHtml (routes/task-messages.ts:155); support routes have no equivalent sanitizer.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "csc-015",
      "severity": "P1",
      "dimension": "security",
      "title": "DELETE message endpoint soft-deletes any message without author ownership or system-message guard",
      "location": "apps/zync-api/src/routes/support/index.ts:270",
      "evidence": "Handler calls softDeleteTicketMessage(db, tid, ticketId, mid) with no session.sub vs author_id check and no author_type !== system guard. Spec: soft delete own message only; system messages not deletable.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "csc-016",
      "severity": "P1",
      "dimension": "spec",
      "title": "New ticket form missing customer select/create, assignee, and rich-text description sheet",
      "location": "apps/zync-app/src/pages/support/NewTicketDialog.tsx:47",
      "evidence": "Spec New Ticket sheet: customer, title, rich-text description, priority, category, assignee. Dialog sends title/description/priority/category_id only via plain Textarea; no customer_id or assignee_id fields.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "csc-017",
      "severity": "P1",
      "dimension": "spec",
      "title": "GET ticket detail omits joined customer/contact/assignee display fields",
      "location": "packages/db/src/queries/support.ts:243",
      "evidence": "getTicket selects tickets.* only — no joins to customers/users/categories. TicketDetailPage sidebar shows source and created date but no customer name, assignee avatar, or category label.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "csc-018",
      "severity": "P1",
      "dimension": "spec",
      "title": "Settings CRM pipeline_stages JSONB not connected to marketing pipeline_stages table used by Kanban",
      "location": "packages/db/src/queries/settings-crm.ts:124",
      "evidence": "settings-crm PATCH writes tenant_settings.pipeline_stages JSONB. Marketing pipeline reads listPipelineStages from pipeline_stages relational table (queries/marketing.ts:292, routes/marketing/leads.ts:159). Saving /settings/crm does not update the board at /marketing/pipeline.",
      "fix_direction": "spec_review"
    },
    {
      "id": "csc-019",
      "severity": "P1",
      "dimension": "spec",
      "title": "lead_scoring_enabled toggle not honored by scoring cron or computeAndSaveLeadScore",
      "location": "apps/zync-api/src/cron/lead-score-refresh.ts:15",
      "evidence": "settings-crm spec: when Disabled, scoring cron does not run and score shows NULL. runLeadScoreRefresh queues all stale leads via listAllActiveLeadTenantPairs with no lead_scoring_enabled filter. computeAndSaveLeadScore (lead-scoring.ts:87) never reads the flag.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "csc-020",
      "severity": "P2",
      "dimension": "spec",
      "title": "Activity bonus fields from settings-crm spec absent (Email reply, Meeting booked, Proposal viewed as add-on points)",
      "location": "apps/zync-app/src/features/settings/crm/LeadScoringSection.tsx:42",
      "evidence": "settings-crm spec Lead Scoring section shows separate Activity bonus inputs (+5/+10/+5). UI only exposes eight BANT weight fields from spec 118; proposal_viewed is a weight not a bonus; no email-reply or meeting-booked bonus inputs.",
      "fix_direction": "spec_review"
    },
    {
      "id": "csc-021",
      "severity": "P2",
      "dimension": "spec",
      "title": "List view is flat rows — not sortable DataTable with Customer/Category/Assignee/Last reply columns",
      "location": "apps/zync-app/src/pages/support/SupportPage.tsx:52",
      "evidence": "Spec list view table columns: Title, Customer, Priority, Category, Assignee, Status, Created, Last reply with sortable headers. TicketRow shows priority, title, status text, optional source, created date only.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "csc-022",
      "severity": "P2",
      "dimension": "spec",
      "title": "URL-synced filters incomplete — missing assignee, category, source, date range, customer",
      "location": "apps/zync-app/src/pages/support/SupportPage.tsx:119",
      "evidence": "UI syncs status, priority, q only. ticketFiltersSchema supports assignee_id/category_id/customer_id/source but UI never sets them. TicketFilters interface has no date range fields.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "csc-023",
      "severity": "P2",
      "dimension": "spec",
      "title": "Staff UI mounted at /crm/support not spec paths /support and /support/:id",
      "location": "apps/zync-app/src/modules/crm.tsx:20",
      "evidence": "Module nested routes crm/support and crm/support/:id. Spec documents /support and /support/:id as staff routes.",
      "fix_direction": "spec_review"
    },
    {
      "id": "csc-024",
      "severity": "P2",
      "dimension": "spec",
      "title": "Portal customer paths use /portal/:tenantSlug/tickets not spec /portal/:tenantSlug/support",
      "location": "apps/zync-app/src/portal/portalRoutes.tsx:59",
      "evidence": "Portal routes tickets and tickets/:id. Spec customer-facing list/detail at /portal/:tenantSlug/support with /support/new form route.",
      "fix_direction": "spec_review"
    },
    {
      "id": "csc-025",
      "severity": "P2",
      "dimension": "spec",
      "title": "Staff-created tickets default source=web instead of spec manual",
      "location": "packages/db/src/validation/support.ts:34",
      "evidence": "createTicketSchema source default 'web'. Spec POST staff create uses source='manual'. Schema CHECK allows web|email|telegram|whatsapp|portal — no manual value.",
      "fix_direction": "spec_review"
    },
    {
      "id": "csc-026",
      "severity": "P2",
      "dimension": "spec",
      "title": "Staff reply auto-sets pending_customer on open/in_progress tickets",
      "location": "apps/zync-api/src/routes/support/index.ts:251",
      "evidence": "Reply handler updateTicket status pending_customer when ticket.status is open or in_progress. Spec lifecycle describes pending_customer when waiting on customer but does not require automatic transition on every staff reply.",
      "fix_direction": "spec_review"
    },
    {
      "id": "csc-027",
      "severity": "P2",
      "dimension": "spec",
      "title": "List API uses cursor pagination not offset page metadata",
      "location": "packages/db/src/queries/support.ts:178",
      "evidence": "listTickets returns { rows, nextCursor }. Spec GET /api/tickets filterable paginated without mandating cursor shape; plan referenced buildPaginated offset style.",
      "fix_direction": "spec_review"
    },
    {
      "id": "csc-028",
      "severity": "P2",
      "dimension": "spec",
      "title": "MEMBER system role lacks tickets:read/write permissions",
      "location": "packages/db/src/seed/permission-keys.ts:74",
      "evidence": "MEMBER grants tasks/projects/time/kb only. VIEWER gets all :read including tickets:read. Typical staff MEMBER users receive 403 on GET /api/tickets.",
      "fix_direction": "spec_review"
    },
    {
      "id": "csc-029",
      "severity": "P2",
      "dimension": "constraint",
      "title": "Support ticket routes skip requireModuleEnabled guard",
      "location": "apps/zync-api/src/routes/support/router.ts:16",
      "evidence": "supportRoutes.use only authMiddleware. Other modules (projects, time) call requireModuleEnabled. Disabling CRM module does not block /api/tickets when user has permission.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "csc-030",
      "severity": "P2",
      "dimension": "spec",
      "title": "WhatsApp webhook Enterprise tier gate returns 403 not spec 402",
      "location": "apps/zync-api/src/routes/webhooks/whatsapp.ts:85",
      "evidence": "meetsMinimumTier check returns c.json({ error: 'Enterprise tier required for WhatsApp' }, 403). Spec: stub returns 402 on lower tiers.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "csc-031",
      "severity": "P2",
      "dimension": "spec",
      "title": "ticket.resolved missing from public webhook event catalog",
      "location": "packages/public-api/src/routes/webhooks.ts:41",
      "evidence": "WEBHOOK_EVENT_CATALOG lists ticket.created and ticket.replied only. Spec also defines ticket.resolved payload.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "csc-032",
      "severity": "P2",
      "dimension": "quality",
      "title": "SLABadge component exists but is not rendered on support list or detail pages",
      "location": "apps/zync-app/src/components/support/SLABadge.tsx:34",
      "evidence": "SLABadge reads dueAt/slaBreached for ticket SLA display. grep SLABadge in pages/support returns zero imports despite tickets.due_at and sla_breached columns in schema.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "csc-033",
      "severity": "P2",
      "dimension": "spec",
      "title": "Ticket cards omit customer chip, assignee avatar, relative age, and source icons",
      "location": "apps/zync-app/src/pages/support/SupportPage.tsx:52",
      "evidence": "Spec board/list card: customer name + company chip, priority badge, assignee avatar, age, source icon. TicketRow renders priority badge, title, status label, optional source text, locale date.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "csc-034",
      "severity": "P2",
      "dimension": "quality",
      "title": "empty-state-catalog links to nonexistent /support/new route",
      "location": "apps/zync-app/src/lib/empty-state-catalog.ts:91",
      "evidence": "support.empty action href '/support/new'. No matching staff route; NewTicketDialog opens via button state not URL.",
      "fix_direction": "code_to_spec"
    }
  ]
}
