# Ask QIEN — Technical Specification

**Version:** 0.1.0  
**Status:** Draft  
**Date:** 2026-05-01

---

## Table of Contents

1. [System Overview](#1-system-overview)
2. [Architecture](#2-architecture)
3. [Data Schema](#3-data-schema) — includes `projects` (§3.5), R-fields and S-fields on `tickets` (§3.6)
4. [Ticket Lifecycle State Machine](#4-ticket-lifecycle-state-machine)
5. [AI Agent Hooks](#5-ai-agent-hooks)
6. [Knowledge System](#6-knowledge-system)
7. [Authentication & Authorization](#7-authentication--authorization)
8. [Configuration System](#8-configuration-system)
9. [API Design](#9-api-design)
10. [Technology Choices](#10-technology-choices)
11. [Testing Strategy](#11-testing-strategy)
12. [Security & Privacy](#12-security--privacy)
13. [Extensibility Roadmap](#13-extensibility-roadmap)

---

## 1. System Overview

Ask QIEN is an asynchronous AI-augmented ticket system that connects factory production line managers with domain-expert consultants. The system progressively reduces expert workload by building a structured knowledge base from each resolved ticket, so that over time the AI can suggest increasingly accurate responses with decreasing need for manual intervention.

### Primary Actors

| Actor | Description |
|---|---|
| **Line Manager** | Factory production line manager submitting an issue |
| **Expert / Consultant** | Domain expert reviewing, responding to, and resolving tickets |
| **AI Agents** | Automated processes that enrich, triage, and assist at defined lifecycle hooks |
| **System Admin** | Manages configuration, tenant memberships/invites, and knowledge base seeding |

### Key Design Principles

- **Progressive automation:** Each ticket interaction trains the system; expert effort decreases over time.
- **Human in the loop:** AI suggests; experts decide. No response reaches a line manager without expert approval.
- **Vendor neutrality:** All LLM, hosting, and third-party integrations are behind abstraction layers.
- **Observable knowledge:** The knowledge base is human-readable, human-editable, and LLM-maintained.
- **TDD/BDD first:** Every feature ships with automated tests. No exceptions.

---

## 2. Architecture

### High-Level Component Diagram

```
┌─────────────────────────────────────────────────────────────────────┐
│                         Client Layer                                 │
│  ┌──────────────────┐          ┌──────────────────────────────────┐  │
│  │  Line Manager UI │          │        Expert / Inbox UI         │  │
│  │  (Web / future   │          │  (Web — reverse-chron inbox,     │  │
│  │   WhatsApp)      │          │   detail view, KB browser)       │  │
│  └────────┬─────────┘          └──────────────┬───────────────────┘  │
└───────────┼──────────────────────────────────-┼─────────────────────┘
            │  HTTPS / WebSocket                │
┌───────────▼──────────────────────────────────▼─────────────────────┐
│                         API Gateway / BFF                            │
│           (REST + Server-Sent Events for real-time updates)          │
└───────────┬─────────────────────────────┬───────────────────────────┘
            │                             │
┌───────────▼──────────┐   ┌─────────────▼──────────────────────────┐
│   Auth Service        │   │           Core Application Service      │
│   (SSO / Magic Link)  │   │  (Ticket CRUD, state transitions,       │
└───────────────────────┘   │   event bus publishing)                 │
                            └─────────────┬──────────────────────────┘
                                          │ Events
                            ┌─────────────▼──────────────────────────┐
                            │           Event Bus                     │
                            │  (async job queue — Inngest or          │
                            │   BullMQ or cloud-native equivalent)    │
                            └──┬─────────┬────────────┬──────────────┘
                               │         │            │
               ┌───────────────▼─┐  ┌───▼──────┐  ┌─▼───────────────┐
               │  AI Agent       │  │ Notifier │  │ KB Indexer       │
               │  Orchestrator   │  │ Service  │  │ Service          │
               │  (see §5)       │  └──────────┘  └─────────────────-┘
               └─────────────────┘
                        │
            ┌───────────▼────────────────────────────────────────────┐
            │                  LLM Abstraction Layer                  │
            │  Provider adapter interface → Anthropic Claude /        │
            │  OpenAI / Bedrock / Vertex / local (Ollama)             │
            └────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────┐
│                         Persistence Layer                            │
│  ┌─────────────────┐   ┌──────────────────┐   ┌──────────────────┐ │
│  │  Primary DB      │   │  Vector Store     │   │  Knowledge Base  │ │
│  │  (PostgreSQL)    │   │  (pgvector or     │   │  (Markdown files │ │
│  │                  │   │   Qdrant)         │   │   in Git repo or │ │
│  │  Tickets, users, │   │  Semantic search  │   │   structured     │ │
│  │  messages, audit │   │  over KB + tickets│   │   wiki store)    │ │
│  └─────────────────┘   └──────────────────┘   └──────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
```

### Deployment Topology

The system is containerized (Docker / OCI). All services can run locally with `docker compose` for development, and deploy to any cloud (AWS, GCP, Azure) or self-hosted Kubernetes cluster. Infrastructure-as-code uses Terraform with provider-agnostic modules.

---

## 3. Data Schema

All schema definitions below are expressed as canonical logical models. Physical implementation uses PostgreSQL with Drizzle ORM (TypeScript) or SQLAlchemy (Python), enabling schema migrations via versioned files.

### 3.1 `tenants`

Represents a company / deployment instance. Enables multi-tenancy for future expansion to additional consultants and companies.

```
tenants
─────────────────────────────────────────────────────────
id                UUID          PK
name              TEXT          NOT NULL
slug              TEXT          UNIQUE NOT NULL          -- URL prefix, e.g. "acme"
config_id         UUID          FK → tenant_configs.id
created_at        TIMESTAMPTZ   NOT NULL DEFAULT now()
updated_at        TIMESTAMPTZ   NOT NULL DEFAULT now()
```

### 3.2 `tenant_configs`

All values that differ per tenant / deployment. Surfaced as a configuration file (YAML/JSON) that is loaded at boot and stored here for runtime access and auditability.

```
tenant_configs
─────────────────────────────────────────────────────────
id                          UUID    PK
tenant_id                   UUID    FK → tenants.id

-- UI copy
submission_prompt_text      TEXT    -- Shown to line manager on initial submission
additional_info_prompt_text TEXT    -- Prepended to clarification question list
submission_max_chars        INT     DEFAULT 4000

-- Initial access seed lists; runtime RBAC uses tenant_memberships
initial_expert_invites      TEXT[]
initial_dual_access_invites TEXT[]
-- dual-access invites seed expert/admin memberships with can_submit_tickets = true

-- AI behavior
ai_provider                 TEXT    DEFAULT 'anthropic'
ai_model                    TEXT    DEFAULT 'claude-sonnet-4-6'
ai_temperature              FLOAT   DEFAULT 0.3
min_info_score_threshold    FLOAT   DEFAULT 0.7  -- below this → ask for more info
max_clarification_cycles    INT     DEFAULT 2
ai_job_timeout_seconds      INT     DEFAULT 120
data_processing_consent     BOOLEAN DEFAULT TRUE

-- Attachment and thread limits
max_attachment_bytes        BIGINT  DEFAULT 26214400
max_ticket_attachment_bytes BIGINT  DEFAULT 104857600
max_messages_loaded_for_ai  INT     DEFAULT 50

-- Notification
notification_channels       JSONB   -- {"email": true, "slack": false, "whatsapp": false}

created_at                  TIMESTAMPTZ NOT NULL DEFAULT now()
updated_at                  TIMESTAMPTZ NOT NULL DEFAULT now()

UNIQUE (tenant_id)
```

### 3.3 `users`

Users represent global identities. Tenant access and role assignment live in `tenant_memberships`, not on the user row, so the same person can safely belong to multiple tenants with different roles.

```
users
─────────────────────────────────────────────────────────
id                UUID          PK
email             TEXT          UNIQUE NOT NULL
normalized_email  TEXT          UNIQUE NOT NULL
display_name      TEXT
auth_provider     TEXT          -- 'magic_link' | 'google' | 'saml'
auth_sub          TEXT          -- external identity subject claim
last_login_at     TIMESTAMPTZ
created_at        TIMESTAMPTZ   NOT NULL DEFAULT now()
updated_at        TIMESTAMPTZ   NOT NULL DEFAULT now()
```

### 3.4 `tenant_memberships`

Tenant-scoped authorization. This is the source of truth for RBAC; config invite lists are only used for initial seeding/invitation.

> **MVP simplification:** The current single-tenant implementation stores `role` directly on the `users` table (`line_manager | expert | admin`) rather than through `tenant_memberships`. The design above describes the full multi-tenant target. Migrating to `tenant_memberships` is a non-breaking additive step when multi-tenant support is needed.

```
tenant_memberships
─────────────────────────────────────────────────────────
id                UUID          PK
tenant_id         UUID          FK → tenants.id
user_id           UUID          FK → users.id
role              TEXT          CHECK (role IN ('line_manager', 'expert', 'admin'))
can_submit_tickets BOOLEAN      NOT NULL DEFAULT FALSE
status            TEXT          CHECK (status IN ('invited','active','disabled'))
created_by        UUID          NULLABLE FK → users.id
created_at        TIMESTAMPTZ   NOT NULL DEFAULT now()
updated_at        TIMESTAMPTZ   NOT NULL DEFAULT now()

UNIQUE (tenant_id, user_id)
```

### 3.5 `projects`

Expert-managed list of active projects. Line managers pick a project when submitting a ticket; experts manage the list via `/admin/projects`.

```
projects
─────────────────────────────────────────────────────────
id                UUID          PK
tenant_id         UUID          FK → tenants.id
name              TEXT          NOT NULL
created_by_id     UUID          NULLABLE FK → users.id
is_active         BOOLEAN       NOT NULL DEFAULT true
created_at        TIMESTAMPTZ   NOT NULL DEFAULT now()
updated_at        TIMESTAMPTZ   NOT NULL DEFAULT now()

UNIQUE (tenant_id, name)
```

Initial project list is seeded from `access.initial_projects` in `tenant.config.yaml`.

### 3.6 `tickets`

The central entity. One ticket per issue thread.

```
tickets
─────────────────────────────────────────────────────────
id                    UUID          PK
tenant_id             UUID          FK → tenants.id
submitter_id          UUID          FK → users.id
assigned_expert_id    UUID          FK → users.id  NULLABLE
reference_number      TEXT          NOT NULL  -- e.g. "AQ-VAL-0042" (category abbreviation + sequence)
status                TEXT          NOT NULL  -- see §4 state machine
ai_processing_state   TEXT          CHECK (ai_processing_state IN (
                                      'idle','queued','running','failed','degraded'
                                    )) DEFAULT 'idle'
ai_failure_reason     TEXT          NULLABLE
current_processing_run_id UUID      NULLABLE FK → ticket_processing_runs.id
last_message_id       UUID          NULLABLE FK → ticket_messages.id
clarification_cycle_count INT       NOT NULL DEFAULT 0
priority              TEXT          CHECK (priority IN ('low','medium','high','critical'))
  NULLABLE -- set by AI triage (Hooks 2/3; not yet implemented)
category_tags         TEXT[]        -- set by AI auto-tagger; editable by expert
sentiment_score       FLOAT         -- [-1.0, 1.0], set by sentiment agent
urgency_score         FLOAT         -- [0.0, 1.0], set by triage agent
info_completeness_score FLOAT       -- [0.0, 1.0], set by completeness agent
title                 TEXT          -- AI-generated one-line summary
channel               TEXT          DEFAULT 'web'  -- 'web' | 'whatsapp' | 'slack'
version               INT           NOT NULL DEFAULT 1  -- optimistic concurrency token
resolved_at           TIMESTAMPTZ

-- R-fields: requester-supplied structured intake (required on new submissions, nullable for compat)
project_id            UUID          NULLABLE FK → projects.id
department            TEXT          NULLABLE
date_of_occurrence    DATE          NULLABLE
issue_category        TEXT          NULLABLE  -- drives reference number abbreviation
affected_system       TEXT          NULLABLE
user_priority         TEXT          CHECK (user_priority IN ('critical','high','medium','low')) NULLABLE
  -- user-declared; never overwritten by AI
success_criteria      TEXT          NULLABLE
action_requested      TEXT          NULLABLE

-- S-fields: expert resolution metadata (captured when expert sends response)
resolution_severity   TEXT          NULLABLE  -- e.g. 'Critical', 'Major', 'Minor', 'Observation'
resolution_concerns   TEXT          NULLABLE  -- root cause category, e.g. 'Knowledge gap'
resolution_summary    TEXT          NULLABLE  -- expert's own-words summary; NULL = use AI summary

created_at            TIMESTAMPTZ   NOT NULL DEFAULT now()
updated_at            TIMESTAMPTZ   NOT NULL DEFAULT now()

UNIQUE (tenant_id, reference_number)
```

All ticket updates that change `status`, assignment, title, priority, tags, or AI-derived fields must use optimistic concurrency:

```
UPDATE tickets
SET ..., version = version + 1
WHERE id = :ticketId AND tenant_id = :tenantId AND version = :expectedVersion;
```

If zero rows are updated, the caller must refetch the ticket and retry or discard the stale operation.

### 3.7 `ticket_messages`

Each turn in a ticket thread — both line-manager submissions and expert replies.

```
ticket_messages
─────────────────────────────────────────────────────────
id                UUID          PK
tenant_id         UUID          FK → tenants.id
ticket_id         UUID          FK → tickets.id
author_id         UUID          FK → users.id  NULLABLE (system messages have NULL)
role              TEXT          CHECK (role IN ('line_manager', 'expert', 'system'))
body_raw          TEXT          NOT NULL   -- original submission text
body_sanitized    TEXT          NOT NULL   -- HTML-stripped, normalized
idempotency_key   TEXT          NULLABLE   -- client key for duplicate-submit protection
created_at        TIMESTAMPTZ   NOT NULL DEFAULT now()

UNIQUE (ticket_id, idempotency_key) WHERE idempotency_key IS NOT NULL
```

### 3.8 `ticket_attachments`

Attachments are stored outside PostgreSQL in S3-compatible object storage. The DB stores metadata, scan state, and authorization context.

```
ticket_attachments
─────────────────────────────────────────────────────────
id                UUID          PK
tenant_id         UUID          FK → tenants.id
ticket_id         UUID          FK → tickets.id
message_id        UUID          NULLABLE FK → ticket_messages.id
uploaded_by       UUID          FK → users.id
object_key        TEXT          NOT NULL   -- tenant-scoped storage path
filename          TEXT          NOT NULL
content_type      TEXT          NOT NULL
byte_size         BIGINT        NOT NULL
sha256            TEXT          NOT NULL
scan_status       TEXT          CHECK (scan_status IN ('pending','clean','blocked','failed'))
created_at        TIMESTAMPTZ   NOT NULL DEFAULT now()

UNIQUE (tenant_id, sha256, ticket_id)
```

Default limits: 25 MB per file, 100 MB per ticket, allowlisted MIME types only, malware scan required before download or AI ingestion.

### 3.9 `ticket_processing_runs`

Each async AI workflow has a durable run record. Ticket status and AI processing status are intentionally separate so humans can keep working when AI is slow or unavailable.

```
ticket_processing_runs
─────────────────────────────────────────────────────────
id                    UUID          PK
tenant_id             UUID          FK → tenants.id
ticket_id             UUID          FK → tickets.id
hook_name             TEXT          NOT NULL
trigger_event         TEXT          NOT NULL
source_ticket_version INT           NOT NULL
source_message_id     UUID          NULLABLE FK → ticket_messages.id
status                TEXT          CHECK (status IN (
                                      'queued','running','succeeded','failed','cancelled','stale'
                                    ))
attempt_count         INT           NOT NULL DEFAULT 0
max_attempts          INT           NOT NULL DEFAULT 3
idempotency_key       TEXT          NOT NULL
started_at            TIMESTAMPTZ
expires_at            TIMESTAMPTZ   NOT NULL
completed_at          TIMESTAMPTZ
error_code            TEXT
error_message         TEXT
created_at            TIMESTAMPTZ   NOT NULL DEFAULT now()

UNIQUE (tenant_id, hook_name, idempotency_key)
```

Runs that exceed `expires_at` or `max_attempts` move to `failed`. Blocking hooks fail open to expert review with `tickets.ai_processing_state = 'degraded'` and an audit entry.

### 3.10 `ai_artifacts`

Stores every AI-generated artefact (summaries, clarification questions, suggested responses) linked to the ticket and the specific message or lifecycle event that triggered them.

```
ai_artifacts
─────────────────────────────────────────────────────────
id                UUID          PK
tenant_id         UUID          FK → tenants.id
ticket_id         UUID          FK → tickets.id
processing_run_id UUID          NULLABLE FK → ticket_processing_runs.id
source_message_id UUID          NULLABLE FK → ticket_messages.id
trigger_event     TEXT          -- state transition or hook name that triggered this
artifact_type     TEXT          CHECK (artifact_type IN (
                                  'completeness_assessment',
                                  'clarification_questions',
                                  'summary',
                                  'suggested_response',
                                  'kb_update_proposal',
                                  'tag_suggestion',
                                  'sentiment_analysis'
                                ))
content           TEXT          NOT NULL   -- human-readable generated text
content_json      JSONB         NULLABLE   -- parsed/validated structured output
model_used        TEXT          NULLABLE   -- e.g. 'claude-sonnet-4-6'; NULL for deterministic fallback artifacts
prompt_version    TEXT          NULLABLE   -- semver of the prompt template used
validation_status TEXT          CHECK (validation_status IN ('valid','invalid','fallback_used'))
expert_accepted   BOOLEAN       NULLABLE   -- NULL = pending review
expert_edited     BOOLEAN       DEFAULT FALSE
expert_version    TEXT          NULLABLE   -- final text if expert edited the artifact
created_at        TIMESTAMPTZ   NOT NULL DEFAULT now()
```

Structured AI output must be parsed and validated before it can affect ticket fields or transitions. Invalid output is retained for debugging but cannot drive state.

### 3.11 `knowledge_entries`

The knowledge base. Each entry is a discrete piece of domain knowledge — a procedure, a known failure mode, a regulation, a best practice.

```
knowledge_entries
─────────────────────────────────────────────────────────
id                UUID          PK
tenant_id         UUID          FK → tenants.id
title             TEXT          NOT NULL
body              TEXT          NOT NULL   -- Markdown; human or LLM-authored
source            TEXT          CHECK (source IN ('human', 'llm', 'imported'))
category_tags     TEXT[]
embedding_id      TEXT          NULLABLE   -- pointer to vector store document ID
authored_by       UUID          NULLABLE   FK → users.id  (NULL = LLM-authored)
is_verified       BOOLEAN       DEFAULT FALSE  -- expert has reviewed LLM-authored entries
is_deleted        BOOLEAN       DEFAULT FALSE
version           INT           DEFAULT 1
created_at        TIMESTAMPTZ   NOT NULL DEFAULT now()
updated_at        TIMESTAMPTZ   NOT NULL DEFAULT now()
```

### 3.12 `knowledge_entry_revisions`

Full revision history for every knowledge entry (append-only).

```
knowledge_entry_revisions
─────────────────────────────────────────────────────────
id                UUID          PK
entry_id          UUID          FK → knowledge_entries.id
body_snapshot     TEXT          NOT NULL
changed_by        UUID          NULLABLE FK → users.id
change_reason     TEXT
version           INT
created_at        TIMESTAMPTZ   NOT NULL DEFAULT now()
```

### 3.13 Relationship Tables

Relationship arrays are not used for integrity-critical links. Join tables enforce tenant isolation, foreign keys, and metadata.

```
ticket_kb_sources
─────────────────────────────────────────────────────────
id                UUID          PK
tenant_id         UUID          FK → tenants.id
ticket_id         UUID          FK → tickets.id
knowledge_entry_id UUID        FK → knowledge_entries.id
artifact_id       UUID          NULLABLE FK → ai_artifacts.id
usage_type        TEXT          CHECK (usage_type IN ('retrieved','cited','expert_selected'))
created_at        TIMESTAMPTZ   NOT NULL DEFAULT now()

UNIQUE (ticket_id, knowledge_entry_id, artifact_id, usage_type)
UNIQUE (ticket_id, knowledge_entry_id, usage_type) WHERE artifact_id IS NULL

knowledge_entry_ticket_links
─────────────────────────────────────────────────────────
id                UUID          PK
tenant_id         UUID          FK → tenants.id
knowledge_entry_id UUID        FK → knowledge_entries.id
ticket_id         UUID          FK → tickets.id
link_reason       TEXT
created_at        TIMESTAMPTZ   NOT NULL DEFAULT now()

ticket_links
─────────────────────────────────────────────────────────
id                UUID          PK
tenant_id         UUID          FK → tenants.id
from_ticket_id    UUID          FK → tickets.id
to_ticket_id      UUID          FK → tickets.id
link_type         TEXT          CHECK (link_type IN ('blocked_by','relates_to','duplicates'))
created_by        UUID          FK → users.id
created_at        TIMESTAMPTZ   NOT NULL DEFAULT now()

UNIQUE (from_ticket_id, to_ticket_id, link_type)
CHECK (from_ticket_id <> to_ticket_id)
```

`blocked_by` links are validated in a transaction using a recursive CTE before insert. Any link that would create a cycle is rejected.

### 3.14 `audit_log`

Immutable append-only log of all significant system events. Used for compliance and debugging.

```
audit_log
─────────────────────────────────────────────────────────
id                UUID          PK
tenant_id         UUID
actor_id          UUID          NULLABLE FK → users.id
actor_type        TEXT          -- 'user' | 'system' | 'ai_agent'
action            TEXT          NOT NULL  -- e.g. 'ticket.status_changed'
entity_type       TEXT          -- 'ticket' | 'knowledge_entry' | ...
entity_id         UUID
payload           JSONB         -- before/after or relevant context
created_at        TIMESTAMPTZ   NOT NULL DEFAULT now()
```

---

## 4. Ticket Lifecycle State Machine

### 4.1 State Definitions

| State | Description |
|---|---|
| `DRAFT` | Line manager has started but not yet submitted |
| `SUBMITTED` | Line manager submitted; awaiting AI triage |
| `NEEDS_INFO` | Deterministic intake rules or AI assessment determined submission is incomplete; awaiting line-manager clarification |
| `TRIAGED` | Intake has passed completeness checks and triage enrichment is being finalized |
| `AWAITING_EXPERT` | Triage complete; ticket is ready for expert review or manual fail-open handling |
| `EXPERT_REVIEWING` | Expert has opened the ticket detail view, typically for manual review or draft retry |
| `RESPONSE_DRAFTING` | AI is generating or regenerating a suggested response |
| `RESPONSE_READY` | AI suggested response is available for expert review/edit |
| `RESPONDED` | Expert has sent their response to the line manager |
| `AWAITING_REPLY` | Expert responded; awaiting potential follow-up from line manager |
| `CLOSED` | Expert has marked the ticket resolved |
| `ARCHIVED` | Ticket has been moved to long-term archive post-closure |

Ticket lifecycle status is the human workflow state. AI execution progress is tracked separately in `tickets.ai_processing_state` and `ticket_processing_runs`. A failed or delayed AI job must never make a ticket invisible to experts.

### 4.2 State Transition Diagram

```
                  ┌─────────┐
                  │  DRAFT  │
                  └────┬────┘
                       │ line manager submits
                       ▼
                  ┌──────────┐
                  │SUBMITTED │◄──────────────────────────────┐
                  └────┬─────┘                               │
                       │ AI completeness check triggered      │
                       ▼                                      │
            ┌──────────────────┐                             │
       ┌────┤ [completeness    ├────┐                        │
       │    │  gate]           │    │                        │
       │    └──────────────────┘    │                        │
       │ score < threshold           │ score >= threshold     │
       ▼                            ▼                        │
 ┌────────────┐             ┌──────────┐                     │
 │ NEEDS_INFO │             │ TRIAGED  │                     │
 └─────┬──────┘             └────┬─────┘                     │
       │ line manager adds        │ AI artifacts ready        │
       │ clarification            ▼                           │
       └─────────────►  ┌──────────────────┐                 │
                        │ AWAITING_EXPERT  │                 │
                        └────────┬─────────┘                 │
                                 │ expert opens ticket        │
                                 ▼                            │
                        ┌──────────────────┐                 │
                        │ EXPERT_REVIEWING │                 │
                        └────────┬─────────┘                 │
                                 │                            │
                    ┌────────────▼──────────┐                │
                    │ [KB lookup result]    │                │
                    └────────────┬──────────┘                │
                                 │                            │
                                 ▼                            │
                        ┌──────────────────┐                 │
                        │RESPONSE_DRAFTING │                 │
                        └────────┬─────────┘                 │
                                 │ draft complete             │
                                 ▼                            │
                        ┌──────────────────┐                 │
                        │ RESPONSE_READY   │                 │
                        └────────┬─────────┘                 │
                                 │ expert edits / approves    │
                                 ▼                            │
                        ┌──────────────────┐                 │
                        │   RESPONDED      │                 │
                        └────────┬─────────┘                 │
                                 │                            │
                    ┌────────────▼──────────┐                │
                    │ line manager replies? │                │
                    └────────────┬──────────┘                │
                      yes ───────┘   no ──────┐              │
                       │                      │              │
                       ▼                      ▼              │
              ┌─────────────────┐     ┌──────────┐          │
              │ AWAITING_REPLY  │     │  CLOSED  │          │
              └────────┬────────┘     └────┬─────┘          │
                       │                   │                 │
              line mgr submits             │ archive         │
                       │           ┌───────▼────────┐       │
                       └──────────►│    ARCHIVED    │       │
                                   └────────────────┘       │
                                                             │
              Note: NEEDS_INFO → line manager adds info ─────┘
              re-enters SUBMITTED for re-triage
```

### 4.3 Allowed Transitions Table

| From | To | Trigger |
|---|---|---|
| `DRAFT` | `SUBMITTED` | Line manager submits form |
| `SUBMITTED` | `NEEDS_INFO` | AI completeness score < threshold |
| `SUBMITTED` | `TRIAGED` | AI completeness score ≥ threshold |
| `NEEDS_INFO` | `SUBMITTED` | Line manager submits clarification |
| `TRIAGED` | `AWAITING_EXPERT` | AI artifacts written to db |
| `AWAITING_EXPERT` | `RESPONSE_DRAFTING` | System automatically queues a KB-grounded response draft |
| `AWAITING_EXPERT` | `EXPERT_REVIEWING` | Expert opens ticket detail |
| `EXPERT_REVIEWING` | `RESPONSE_DRAFTING` | Expert manually retries or regenerates a response draft |
| `RESPONSE_DRAFTING` | `RESPONSE_READY` | AI draft complete |
| `RESPONSE_READY` | `RESPONSE_DRAFTING` | Expert requests regeneration with new KB context |
| `RESPONSE_READY` | `RESPONDED` | Expert approves and sends response |
| `RESPONDED` | `AWAITING_REPLY` | (no action — system sets after send) |
| `AWAITING_REPLY` | `SUBMITTED` | Line manager replies (re-enters triage pipeline) |
| `RESPONDED` | `CLOSED` | Expert marks resolved |
| `AWAITING_REPLY` | `CLOSED` | Expert marks resolved |
| `CLOSED` | `ARCHIVED` | Scheduled archival job or manual action |

### 4.4 Failure, Timeout, and Concurrency Rules

State transitions are executed only by the core application service inside a database transaction. AI agents may propose transitions, but the core service validates current status, tenant, actor capability, source ticket version, and source message before applying any change.

- Blocking AI hooks have a timeout and retry budget from `tenant_configs.ai_job_timeout_seconds` and `ticket_processing_runs.max_attempts`.
- If the completeness or summarization pipeline fails, the ticket moves to `AWAITING_EXPERT` with `ai_processing_state = 'degraded'`, `ai_failure_reason` populated, and a system message explaining that manual triage is required.
- If response drafting fails, the ticket returns to `EXPERT_REVIEWING` with an expert-visible retry action. The expert can still write and send a manual response.
- If a line manager submits another message while an AI run is active, the active run is marked `stale` unless its `source_message_id` and `source_ticket_version` still match the ticket.
- After `max_clarification_cycles`, a ticket cannot be sent back to `NEEDS_INFO` by AI alone; it is escalated to `AWAITING_EXPERT`.
- Critical deterministic signals such as "line down", "safety issue", or configured emergency keywords bypass `NEEDS_INFO` and route to `AWAITING_EXPERT` with high priority even if the LLM considers the report incomplete.

---

## 5. AI Agent Hooks

All AI agents are invoked via the event bus as named jobs. They are stateless functions that receive a `ticket_id`, a `processing_run_id`, and the ticket/message version that triggered the run. They perform work and write `ai_artifacts` rows back to the database. They never directly change ticket status — they emit events that the core service processes after validation.

### Agent Invocation Contract

```typescript
interface AgentJob {
  jobName: AgentHookName;
  processingRunId: string;
  ticketId: string;
  tenantId: string;
  sourceTicketVersion: number;
  sourceMessageId?: string;
  idempotencyKey: string;
  triggeredBy: 'state_transition' | 'expert_action' | 'scheduled';
  context?: Record<string, unknown>;
}

interface AgentResult {
  artifactType: AiArtifactType;
  content: string;
  contentJson?: unknown;
  kbSources: string[];      // knowledge_entry IDs consulted
  modelUsed: string;
  promptVersion: string;
  validationStatus: 'valid' | 'invalid' | 'fallback_used';
  suggestedStatusTransition?: TicketStatus; // advisory only
}
```

### Agent Reliability Contract

- Each job is idempotent by `(tenant_id, hook_name, idempotency_key)`.
- The core service discards results whose `sourceTicketVersion` or `sourceMessageId` no longer matches the ticket.
- Every structured LLM response must pass strict schema validation, range checks, and tenant-scoped source validation before it can update ticket fields.
- Agents use sanitized message content by default. Raw content is sent to an LLM only when a hook explicitly requires it and tenant policy allows it.
- The AI gateway enforces provider/model allowlists, data-processing consent, token limits, redaction policy, and prompt-injection checks before any provider call.
- If an LLM call fails, times out, returns invalid JSON, or returns unsafe content, the agent writes a failed run/artifact and the core service applies the fallback path in §4.4.

---

### Hook 1: Completeness Assessment

**Trigger:** `SUBMITTED` state entry (immediately after line manager submits or re-submits clarification)

**Agent:** `completeness-assessment-agent`

**Purpose:** Determine whether the submission contains enough information for the expert to act on, without the expert having to ask. Score the submission and, if below threshold, generate specific targeted questions to ask the line manager.

**Deterministic pre-gate:**
- Enforce `submission_max_chars`, required tenant-specific intake fields, and attachment limits before the LLM runs.
- If emergency keywords or high-impact phrases are detected, set priority to `high` or `critical` and route to `AWAITING_EXPERT` after artifact generation.
- If `clarification_cycle_count >= tenant_config.max_clarification_cycles`, bypass `NEEDS_INFO` and route to `AWAITING_EXPERT`.

**Input context built by agent:**
- The `body_sanitized` of the latest `ticket_message`
- `tenant_config.submission_prompt_text` (to understand what was asked)
- `tenant_config.additional_info_prompt_text`
- Prior clarification exchanges (if any) to avoid repeating questions

**Prompt contract (simplified):**
```
You are evaluating whether a factory production line issue report contains
sufficient information for an expert consultant to begin formulating a response.

Score the submission on a scale of 0.0–1.0 across these dimensions:
- problem_description: Is the problem clearly described?
- affected_equipment: Is the machine/line/component identified?
- observed_symptoms: Are symptoms and behavior described?
- timeline: When did the issue start? Is it intermittent or constant?
- attempted_remedies: Has anything been tried?
- impact: What is the production impact?

Return JSON: { "score": float, "missing_dimensions": [...], "questions": [...] }
```

**Outputs written:**
- `ai_artifacts` row of type `completeness_assessment` with JSON payload
- `ai_artifacts` row of type `clarification_questions` if score < threshold (questions formatted using `additional_info_prompt_text` prefix from config)

**Status transition emitted:**
- `NEEDS_INFO` if `score < tenant_config.min_info_score_threshold`
- `TRIAGED` (via summary hook chain) if score ≥ threshold

If the output is invalid or the agent fails, the core service routes the ticket to `AWAITING_EXPERT` with `ai_processing_state = 'degraded'`.

---

### Hook 2: Sentiment & Urgency Analysis

**Trigger:** Chained immediately after completeness assessment passes (score ≥ threshold), before triage completes

**Agent:** `sentiment-urgency-agent`

**Purpose:** Assess the emotional tone of the submission and the inferred operational urgency so the expert can prioritize their inbox.

**Input context:**
- Latest sanitized ticket message body
- Prior messages in thread (for trajectory — is urgency escalating?)

**Outputs written:**
- `ai_artifacts` row of type `sentiment_analysis` with:
  ```json
  {
    "sentiment_score": -0.72,
    "sentiment_label": "frustrated",
    "urgency_score": 0.88,
    "urgency_label": "high",
    "reasoning": "Language indicates line stoppage..."
  }
  ```
- Requests updates to `tickets.sentiment_score`, `tickets.urgency_score`, and `tickets.priority` (mapped from urgency bands) via an optimistic-concurrency write. Deterministic emergency keyword rules can override LLM priority if they indicate a higher severity.

---

### Hook 3: Auto-Tagging & Categorization

**Trigger:** Chained after sentiment/urgency, before triage completes

**Agent:** `auto-tag-agent`

**Purpose:** Classify the ticket into the taxonomy used by the knowledge base so that KB retrieval in later hooks is semantically targeted.

**Input context:**
- Sanitized ticket message body
- Existing `knowledge_entries.category_tags` vocabulary (fetched from DB, distinct values, to encourage consistent tag reuse)

**Prompt contract:** Agent is instructed to prefer existing tags over inventing new ones, and to return 2–5 tags maximum.

**Outputs written:**
- `ai_artifacts` row of type `tag_suggestion` with suggested tags
- Requests updates to `tickets.category_tags` via an optimistic-concurrency write. Tags are constrained to the tenant taxonomy unless the result is explicitly marked as a proposed new tag for expert review.

---

### Hook 4: Summarization

**Trigger:** Chained after auto-tagging; marks the end of the triage pipeline

**Agent:** `summarization-agent`

**Purpose:** Produce a concise, expert-facing summary of the issue that distils all messages in the thread into one scannable paragraph, and a one-line title. This is what the expert sees in the inbox list row and at the top of the detail view.

**Input context:**
- Recent `ticket_messages` up to `tenant_config.max_messages_loaded_for_ai`
- Current rolling ticket summary, if the thread exceeds the message window
- Tags from Hook 3
- Urgency/sentiment from Hook 2

**Outputs written:**
- `ai_artifacts` row of type `summary`
- Updates `tickets.title` with the one-line summary

**Status transition emitted:** `RESPONSE_DRAFTING` after triage/summarization completes and the response draft job is queued

Long threads are summarized incrementally. The system stores a rolling summary artifact whenever the thread crosses the configured AI message window, so summarization never requires loading an unbounded number of comments into the prompt.

---

### Hook 5: Knowledge Base Retrieval & Response Drafting

**Trigger:** Automatically after summarization queues a response draft, OR manually by expert clicking "Generate Response" / "Regenerate draft"

**Agent:** `response-draft-agent`

**Purpose:** Search the knowledge base for relevant entries, compose a suggested expert response grounded in that knowledge, and surface the sources used so the expert can verify.

**Input context:**
- AI summary (from Hook 4 artifact)
- Ticket tags and category
- Recent prior messages plus rolling summary for long threads
- Semantic search results from vector store (top-K knowledge entries, filtered by tenant and tags)

**Retrieval strategy:**
1. Embed the ticket summary using the same embedding model used to index the KB.
2. Retrieve top-10 candidate `knowledge_entries` by cosine similarity.
3. Re-rank by tag overlap with `tickets.category_tags`.
4. Pass top-5 entries as context to the LLM.

**Prompt contract (simplified):**
```
You are drafting a response on behalf of an expert consultant.
Use ONLY the knowledge base entries provided. Do not fabricate.
If the knowledge base does not contain enough information to answer,
explicitly say so and indicate what knowledge is missing.

Knowledge base entries: [...]
Issue summary: [...]
Relevant thread context: [...]

Draft a helpful, professional response. Cite which knowledge entries
you used by their ID. If coverage is insufficient, output a
"knowledge_gaps" field listing what is missing.
```

**Outputs written:**
- `ai_artifacts` row of type `suggested_response` with:
  ```json
  {
    "draft": "...",
    "kb_source_ids": ["uuid1", "uuid2"],
    "knowledge_gaps": ["No documented procedure for X found"],
    "confidence": 0.82
  }
  ```
- Writes cited/retrieved sources to `ticket_kb_sources`

**UI behavior driven by output:**
- If `knowledge_gaps` is non-empty → UI shows expert a prompt to select additional KB entries or create new ones, then re-triggers this hook (→ `RESPONSE_DRAFTING` again)
- If `knowledge_gaps` is empty → transitions to `RESPONSE_READY`

`knowledge_gaps` is advisory. The expert can proceed with a manual response even when the KB is insufficient or the draft agent fails.

---

### Hook 6: Knowledge Base Update Proposal

**Trigger:** After expert sends response (`RESPONDED` state entry)

**Agent:** `kb-update-agent`

**Purpose:** Compare the expert's final (possibly edited) response against the AI-suggested response and the KB entries used. If the expert meaningfully changed the response, propose a new or updated KB entry to capture that knowledge delta. Expert must approve before any KB entry is created or modified.

**Input context:**
- AI suggested response (`ai_artifacts` where `artifact_type = 'suggested_response'`)
- Expert's final response (`ticket_messages` where `role = 'expert'`)
- KB entries that were referenced (`ticket_kb_sources`)
- Ticket summary and tags

**Outputs written:**
- `ai_artifacts` row of type `kb_update_proposal` with:
  ```json
  {
    "action": "create" | "update" | "none",
    "target_entry_id": "uuid | null",
    "proposed_title": "...",
    "proposed_body": "...",  // Markdown
    "reasoning": "Expert response contained procedure not in KB..."
  }
  ```

**Expert UI:** KB update proposals appear in a "Knowledge Review" panel. Expert can accept, edit, or dismiss each proposal. Accepted proposals are written to `knowledge_entries` with `source = 'llm'` and `is_verified = true` (since expert approved).

---

### Hook 7: Post-Resolution KB Enrichment (Scheduled)

**Trigger:** Nightly scheduled job over all tickets that entered `CLOSED` or `ARCHIVED` in the past 7 days and have not yet had a KB update reviewed

**Agent:** `kb-enrichment-agent`

**Purpose:** Batch process resolved tickets to identify patterns, extract reusable knowledge, and propose consolidations or improvements to existing KB entries (e.g., merging two similar entries, adding a new failure mode example to an existing entry).

**Outputs written:**
- New `ai_artifacts` rows of type `kb_update_proposal` (requires expert approval before application)
- Optionally proposes new `category_tags` if it detects a recurring issue type not yet in the taxonomy

---

### Hook Summary Table

| # | Hook Name | Trigger State/Event | Artifact Type | Blocks Status? |
|---|---|---|---|---|
| 1 | Completeness Assessment | `SUBMITTED` entry | `completeness_assessment`, `clarification_questions` | Yes, but fails open to `AWAITING_EXPERT` |
| 2 | Sentiment & Urgency | Post-Hook 1 pass | `sentiment_analysis` | No (enrichment) |
| 3 | Auto-Tagging | Post-Hook 2 | `tag_suggestion` | No (enrichment) |
| 4 | Summarization | Post-Hook 3 | `summary` | Yes, but fails open to `AWAITING_EXPERT` |
| 5 | KB Retrieval & Draft | `EXPERT_REVIEWING` entry | `suggested_response` | No hard block; expert can respond manually |
| 6 | KB Update Proposal | `RESPONDED` entry | `kb_update_proposal` | No (async, expert reviews) |
| 7 | Post-Resolution Enrichment | Nightly scheduled | `kb_update_proposal` | No (batch, async) |

---

## 6. Knowledge System

### 6.1 Design Philosophy

The knowledge base is the system's long-term memory. It must be:
- **Human-readable:** All entries are Markdown documents.
- **Human-editable:** Experts can create, edit, and delete entries directly.
- **LLM-maintained:** Agents propose additions and edits (Hooks 6 and 7); experts approve.
- **Versioned:** Every change is recorded in `knowledge_entry_revisions`.
- **Semantically searchable:** Every entry is embedded and stored in a vector store for retrieval.

### 6.2 Entry Structure

Each knowledge entry follows a Markdown template:

```markdown
---
id: <uuid>
title: <human-readable title>
tags: [tag1, tag2]
source: human | llm
verified: true | false
linked_tickets: [<ticket-reference-numbers>]
created_at: <ISO date>
updated_at: <ISO date>
---

## Summary
One or two sentences describing what this entry covers.

## Context
When does this situation arise? What equipment, process, or regulation does it relate to?

## Procedure / Resolution
Step-by-step guidance or explanation.

## Notes
Edge cases, caveats, references to standards (e.g. ISO 13485, 21 CFR Part 820).
```

`linked_tickets` in Markdown frontmatter is a human-readable export of `knowledge_entry_ticket_links`; the relational join table remains the source of truth.

### 6.3 KB Storage Options

Two implementation options (choose one per tenant based on preference):

**Option A — Database-native (default)**  
Entries stored in `knowledge_entries` table. Markdown body stored as TEXT. Git-like versioning via `knowledge_entry_revisions`. Simple export to flat Markdown files on demand.

**Option B — Git-backed (Karpathy-inspired)**  
Entries stored as individual `.md` files in a dedicated Git repository (one file per entry). The application reads/writes via Git operations. Provides full diff history, branch-based review workflows, and human-editability via any text editor or GitHub UI. The vector indexer watches for commits and re-indexes changed files.

Both options expose the same internal API. The storage backend is a swappable adapter.

### 6.4 Embedding & Vector Search

- **Embedding model:** configurable per tenant; defaults to provider-matched embedding (e.g., `text-embedding-3-small` for OpenAI, `voyage-3` for Anthropic-adjacent workflows, or a local model via Ollama).
- **Vector store:** `pgvector` extension on the existing PostgreSQL instance (low-ops default); can swap to Qdrant or Weaviate for higher scale via adapter interface.
- **Indexing:** A background worker listens for `knowledge_entry.created` and `knowledge_entry.updated` events, re-embeds the entry body, and upserts the vector.

### 6.5 Integrity and Staleness

- Vector namespaces are tenant-scoped and include `tenant_id` in every document payload. Retrieval rejects any result whose DB row does not match the request tenant.
- Deleted or unverified entries are excluded from response drafting unless an expert explicitly selects them.
- KB writes use optimistic concurrency on `knowledge_entries.version`; concurrent edits that target the same version must be reviewed and retried.
- `ticket_kb_sources` records whether an entry was merely retrieved, actually cited by the AI, or manually selected by an expert.
- The vector index is eventually consistent, so the relational DB remains the source of truth. If a vector result points to a missing, deleted, unverified, or cross-tenant entry, it is discarded and logged.

---

## 7. Authentication & Authorization

### 7.1 Auth Methods

| Method | Use case |
|---|---|
| **Magic link (email)** | Default for all users; no password required |
| **Google OAuth / OIDC** | Optional SSO for companies using Google Workspace |
| **SAML 2.0** | Enterprise SSO for larger tenants |

Auth is handled by a dedicated auth service (e.g., Lucia Auth, Auth.js, or a managed service like Auth0 / Supabase Auth behind an adapter interface to avoid lock-in).

Sessions are JWT-based (short-lived access tokens + refresh tokens). Access tokens include the active tenant ID and membership ID; tenant switching requires issuing a token for the selected tenant.

### 7.2 Role-Based Access Control

| Role | Capabilities |
|---|---|
| `line_manager` | Submit tickets, view own tickets and replies, submit clarifications |
| `expert` | Full inbox view, respond to tickets, manage KB entries, review AI proposals; may submit tickets when `can_submit_tickets = true` |
| `admin` | All expert capabilities + tenant config management, user management; may submit tickets when `can_submit_tickets = true` |

Access is gated at the API layer and enforced again by PostgreSQL row-level security. Role is read from `tenant_memberships`, not from config files. Config invite lists seed initial memberships but are not consulted for runtime authorization.

Authorization rules:
- `line_manager` can view and append to tickets where `submitter_id = current_user_id`.
- Memberships with `can_submit_tickets = true` can also create and view their own submitted tickets.
- `expert` can view the tenant inbox, respond to tickets, manage KB entries, and review AI proposals.
- `admin` can manage tenant config, memberships, and audit access.
- Disabled memberships invalidate refresh tokens and prevent all tenant-scoped access.
- Every request sets `app.tenant_id`, `app.user_id`, and `app.membership_role` in the DB transaction so row-level security cannot be bypassed by a missed ORM helper.

---

## 8. Configuration System

All tenant-specific configuration lives in a YAML file (one per tenant) that is loaded at boot and synced to `tenant_configs`. Changes to the YAML are applied via a migration/seed step and take effect on next boot (or hot-reload if the runtime supports it).

### 8.1 Example `tenant.config.yaml`

```yaml
tenant:
  name: "Acme Medical Devices"
  slug: "acme"

access:
  initial_expert_invites:
    - "qien@acme.com"
    - "backup-expert@acme.com"
  initial_dual_access_invites:
    - "supervisor@acme.com"
    # seeded with can_submit_tickets = true

ui:
  submission_prompt_text: >
    Please describe the issue you are experiencing on the production line.
    Include the machine name, what you observed, and when it started.
  additional_info_prompt_text: >
    To help our expert respond quickly, please provide the following additional details:
  submission_max_chars: 4000

ai:
  provider: "anthropic"              # anthropic | openai | bedrock | vertex | ollama
  model: "claude-sonnet-4-6"
  temperature: 0.3
  min_info_score_threshold: 0.70
  max_clarification_cycles: 2
  job_timeout_seconds: 120
  data_processing_consent: true
  embedding_model: "voyage-3"

limits:
  max_attachment_bytes: 26214400
  max_ticket_attachment_bytes: 104857600
  max_messages_loaded_for_ai: 50

notifications:
  email: true
  slack: false
  whatsapp: false

intake:
  # Dropdown options for the structured submission form
  departments: ["Quality", "Operations", "Engineering", "Regulatory Affairs"]
  issue_categories: ["Validation", "Equipment", "Compliance", "Deviation", "CAPA", "Other"]
  affected_systems: ["Sealing", "Testing", "Assembly", "Packaging", "Other"]
  action_requested_options:
    - "Approval of proposed strategy"
    - "Guidance on next steps"
    - "Regulatory interpretation"
    - "Other"
  # Expert resolution metadata options
  resolution_severity_levels: ["Critical", "Major", "Minor", "Observation"]
  resolution_concern_types:
    - "Process gap"
    - "Knowledge gap"
    - "Confusion / unclear guidance"
    - "Other"
  # Used to build reference numbers: AQ-VAL-0042
  category_abbreviations:
    Validation: "VAL"
    Equipment: "EQP"
    Compliance: "CMP"
    Deviation: "DEV"
    CAPA: "CAP"
    Other: "OTH"

access:
  # Projects are seeded once; experts manage them at /admin/projects thereafter
  initial_projects:
    - "Atlas"
    - "Orion"
```

---

## 9. API Design

### 9.1 REST Endpoints (core)

All endpoints are prefixed `/api/v1/{tenant_slug}/`.

**Tickets**
```
POST   /tickets                        # line manager submits new ticket
GET    /tickets                        # expert: inbox list (reverse-chron, paginated)
GET    /tickets/:id                    # detail view (all messages + ai artifacts)
PATCH  /tickets/:id/status             # expert: status transitions
POST   /tickets/:id/messages           # line manager: submit clarification or follow-up
POST   /tickets/:id/respond            # expert: approve and send response

GET    /tickets/:id/ai-artifacts       # fetch AI artifacts for a ticket
POST   /tickets/:id/regenerate-draft   # expert: request new draft (with updated KB context)
POST   /tickets/:id/attachments        # upload attachment metadata + signed upload URL
GET    /tickets/:id/attachments        # list clean attachments
POST   /tickets/:id/links              # create blocked_by / relates_to / duplicates link
DELETE /tickets/:id/links/:linkId      # remove a ticket link
```

Message and artifact lists are cursor-paginated. `GET /tickets/:id` returns ticket metadata, the latest message page, the latest valid AI artifacts, attachment metadata, and pagination cursors.

**Knowledge Base**
```
GET    /knowledge                      # list entries (filterable by tag, search)
POST   /knowledge                      # expert: create new entry
GET    /knowledge/:id                  # get entry (with revision history)
PUT    /knowledge/:id                  # expert: update entry
DELETE /knowledge/:id                  # expert: soft-delete entry
GET    /knowledge/proposals            # expert: list pending KB update proposals
PATCH  /knowledge/proposals/:id        # expert: accept / reject / edit proposal
```

**Auth**
```
POST   /auth/magic-link                # request magic link email
GET    /auth/verify                    # verify magic link token
POST   /auth/oauth/callback            # OAuth callback
POST   /auth/logout
```

**Admin**
```
GET    /admin/config                   # read tenant config
PUT    /admin/config                   # update tenant config
GET    /admin/users                    # list users
POST   /admin/memberships              # invite/add tenant member
PATCH  /admin/memberships/:id          # change role or disable membership
```

### 9.2 Real-Time Updates

Server-Sent Events (SSE) are used to push status changes to open browser sessions, avoiding polling:

```
GET /tickets/:id/events               # SSE stream for ticket state changes
GET /inbox/events                     # SSE stream for new inbox items
```

### 9.3 Idempotency and Concurrency

High-write endpoints must accept `Idempotency-Key`:
- `POST /tickets`
- `POST /tickets/:id/messages`
- `POST /tickets/:id/respond`
- `POST /tickets/:id/regenerate-draft`
- `POST /tickets/:id/attachments`

Mutation endpoints that update existing rows must require `If-Match: <version>` or an equivalent explicit version field. The API returns `409 Conflict` when the supplied version is stale. State-changing API handlers must publish events only after the database transaction commits.

### 9.4 Attachment Flow

Attachments use a two-step flow:
1. Client requests an upload URL with filename, content type, byte size, and checksum.
2. Object storage upload completes, then the scanner marks the attachment `clean`, `blocked`, or `failed`.

Only `clean` attachments can be downloaded by users or included in AI context. Blocked or failed attachments remain visible as metadata with an error state for auditability.

---

## 10. Technology Choices

### 10.1 Recommended Stack

| Layer | Choice | Rationale |
|---|---|---|
| **Language** | TypeScript (Node.js) | Full-stack type safety; Drizzle ORM; excellent LLM SDK ecosystem; AI-agent coding (vibe coding) tooling is TypeScript-first |
| **Web Framework** | Next.js (App Router) | SSR + API routes in one repo; responsive by default; easy to add mobile/tablet support |
| **Database** | PostgreSQL + pgvector | Single store for relational data and vector embeddings; avoids extra infrastructure |
| **ORM / Migrations** | Drizzle ORM | Type-safe; schema-first; migrations are plain SQL files (easy review, non-breaking changes) |
| **Job Queue** | Inngest | Serverless-native event-driven job queue; built-in retry, observability, and step functions; cloud and self-hosted options |
| **LLM Abstraction** | Vercel AI SDK (provider-agnostic) | Single interface for Anthropic, OpenAI, Bedrock, Vertex, Ollama; swap by changing one config value |
| **Auth** | Auth.js (NextAuth v5) | Supports magic link, OAuth, SAML; runs anywhere; not a managed service |
| **Styling** | Tailwind CSS | Responsive out of the box; no CSS framework lock-in |
| **Testing** | Vitest (unit/integration) + Playwright (E2E) | Fast, TypeScript-native; Playwright covers browser E2E with BDD-style `test.step()` |
| **Infrastructure** | Docker + Terraform | Local dev via `docker compose`; cloud-agnostic Terraform modules |
| **CI/CD** | GitHub Actions (provider-agnostic) | Standard; easy to migrate to GitLab CI or other runners |

### 10.2 LLM Abstraction Interface

```typescript
interface LLMProvider {
  complete(params: CompletionParams): Promise<CompletionResult>;
  embed(text: string): Promise<number[]>;
}

// Registered providers: AnthropicProvider, OpenAIProvider,
//   BedrockProvider, VertexProvider, OllamaProvider
// Selected at runtime via tenant_config.ai.provider
```

No LLM-specific SDK types leak outside the provider implementation files.

All provider calls go through an AI gateway module before reaching `LLMProvider`. The gateway enforces tenant consent, model allowlists, max token budgets, redaction rules, prompt-injection checks, provider retention settings, structured-output validation, and audit logging. Application code must not call provider SDKs directly.

---

## 11. Testing Strategy

### 11.1 Philosophy

Testing is non-negotiable. Every feature ships with passing tests. The test pyramid:

```
         /\
        /E2E\          Playwright — critical user journeys
       /──────\
      / Integ  \       Vitest — API routes, DB, AI agent jobs (real DB, mocked LLM)
     /──────────\
    /    Unit    \     Vitest — pure functions, state machine transitions, prompt builders
   /______________\
```

### 11.2 Test Categories

**Unit tests (`*.test.ts`)**
- State machine transition validator: exhaustively tests all allowed and disallowed transitions
- AI fallback rules: timeout, invalid JSON, stale run, max clarification cycles, emergency keyword bypass
- Prompt builders: snapshot tests for each agent's prompt construction
- Config parser: validates YAML config parsing and defaults
- Schema validators: input/output shapes for all API endpoints
- Ticket link validator: rejects self-links and circular `blocked_by` graphs

**Integration tests (`*.integration.test.ts`)**
- API route tests against a real PostgreSQL instance (Docker, seeded per test suite)
- Agent job tests: inject a real ticket, run the agent with a mocked LLM that returns fixture responses, assert DB state
- KB retrieval tests: seed vector store with known entries, assert correct retrieval for known queries
- Concurrency tests: duplicate idempotency keys, stale `If-Match` versions, simultaneous message submit and AI completion
- RLS tests: verify cross-tenant reads and vector-source joins are rejected at the DB layer
- Attachment tests: size/type limits, scan states, blocked download, and AI exclusion

**E2E tests (`*.e2e.ts` — Playwright)**
- Line manager journey: submit ticket → receive clarification request → submit clarification → see "awaiting expert" state
- Expert journey: see ticket in inbox → open detail → view AI summary and suggested response → edit and send response → mark resolved
- KB journey: expert creates a KB entry → entry appears in vector search for a related ticket
- Auth journeys: magic link login, role-based access enforcement
- AI degradation journey: mocked LLM timeout still routes ticket to expert inbox with a visible degraded AI state
- Long-thread journey: ticket with 1,000+ comments remains paginated and uses rolling summaries for AI context

**AI agent contract tests**
- Each agent has a deterministic "golden path" test: fixed input + mocked LLM = expected `ai_artifacts` row content and DB state changes. These tests run without real LLM calls.
- Prompt regression tests: if a prompt template changes, snapshot tests catch unintended changes before deploy.
- Invalid and adversarial output tests verify that unsafe, malformed, cross-tenant, or prompt-injected model output is stored for audit but cannot drive state transitions.

### 11.3 BDD Feature Files (optional, for Cucumber/Gherkin clarity)

For the highest-value user journeys, Gherkin feature files serve as living documentation and drive the Playwright tests:

```gherkin
Feature: Line manager submits an issue

  Scenario: Submission missing critical information
    Given I am logged in as a line manager
    When I submit a ticket with only a vague description
    Then I should see a "We need more information" prompt
    And I should be shown specific questions about my issue
    And the ticket status should be "NEEDS_INFO"
```

---

## 12. Security & Privacy

### 12.1 Data Classification

The system handles **corporate confidential** data (production issues, remediation procedures, proprietary equipment information). While no PII is collected by design, corporate data requires strong controls.

### 12.2 Controls

| Control | Implementation |
|---|---|
| **Encryption in transit** | TLS 1.3 everywhere; HSTS enabled |
| **Encryption at rest** | PostgreSQL on encrypted volumes; cloud KMS for key management |
| **Authentication** | Short-lived JWTs (15 min access token); secure httpOnly refresh token cookies |
| **Authorization** | Per-request role + tenant validation at API layer; membership-backed RBAC; row-level security in DB |
| **Tenant isolation** | DB transactions set tenant/user/session variables consumed by RLS policies; ORM tenant filters are defense-in-depth |
| **Input sanitization** | All user text sanitized before storage (`body_sanitized`); raw text preserved with restricted access and retention controls |
| **Attachment safety** | Object storage keys are tenant-scoped; signed URLs are short-lived; malware scan and MIME allowlist required before access |
| **LLM data handling** | AI gateway enforces tenant consent, provider/model allowlists, redaction, prompt-injection checks, and retention settings |
| **Audit log** | All state transitions and data modifications written to immutable `audit_log` |
| **Secret management** | All credentials in environment variables; never in code or config YAML; rotated via CI secrets manager |
| **Dependency scanning** | Automated `npm audit` and Dependabot in CI |
| **Rate limiting** | API gateway enforces per-user and per-IP rate limits on all endpoints |

### 12.3 AI Data Handling Policy

By default, ticket content is sent to the configured cloud LLM provider only when `data_processing_consent = true`. Tenants with data residency requirements can:
1. Configure an on-premises Ollama instance as the LLM provider.
2. Configure a Bedrock or Vertex endpoint within their own cloud account.

A `data_processing_consent` flag in tenant config gates LLM calls and is logged in the audit trail. If consent is false and no approved local provider is configured, AI hooks are skipped, artifacts record `fallback_used`, and tickets route to expert workflows without model-generated content.

Additional AI controls:
- Prompt context is built only from tenant-scoped, permission-checked records.
- Retrieved vector hits are reloaded from the relational DB and tenant-checked before use.
- Raw uploaded files are not sent to LLMs. Only extracted/sanitized text from clean, allowlisted attachments may be included, subject to token limits.
- User/KB content is treated as untrusted prompt input. System prompts instruct the model to ignore instructions embedded in ticket text or KB entries, and the gateway runs prompt-injection heuristics before provider calls.
- Provider retention and training settings must be explicitly configured per provider adapter and recorded in audit logs.

---

## 13. Extensibility Roadmap

The following extensions are anticipated. The current architecture explicitly accommodates them without requiring rework.

| Future Capability | How Current Design Enables It |
|---|---|
| **WhatsApp / Slack intake** | `tickets.channel` field + channel adapter pattern in the submission API; same core pipeline |
| **Mobile / tablet UI** | Tailwind responsive design is built in from day one |
| **Additional expert users** | `tenant_memberships` supports multiple experts/admins per tenant |
| **Multi-tenant SaaS** | `tenants` and `tenant_configs` tables are present; all queries are tenant-scoped |
| **Self-hosted / air-gapped** | Ollama provider adapter; `docker compose` deployment; no mandatory cloud services |
| **New DB fields** | Drizzle migrations are additive by default; new nullable columns are non-breaking |
| **Analytics / reporting** | `audit_log` + `ai_artifacts` tables provide a complete event log for BI tooling |
| **Second-brain / wiki tools** | `knowledge_entries` + revision history + vector store is the foundation; additional query/browse UI is additive |
| **Switching LLM providers** | One config value change; adapter interface is already in place |
| **Switching vector stores** | Vector store adapter interface; swap pgvector for Qdrant without touching agent code |

---

*This document is a living specification. It should be updated as design decisions are validated or revised during implementation.*
