Files
cve-dashboard/.kiro/specs/ticket-linking/design.md

12 KiB

Design Document: Ticket Linking

Overview

The ticket-linking feature introduces a generic many-to-many association system between the three entity types in the CVE Dashboard: Archer tickets (EXC-XXXX), Jira tickets (PROJECT-XXXX), and CVEs (CVE-YYYY-NNNNN). A new ticket_links PostgreSQL table stores directional link records, but the system treats them as bidirectional — querying from either side returns the link. The existing cve_id/vendor foreign key columns on archer_tickets and jira_tickets remain unchanged; this feature is purely additive.

The implementation spans three layers:

  1. Database — A ticket_links table with CHECK constraints, unique index, and bidirectional query indexes.
  2. Backend API — A new Express router (routes/links.js) exposing POST/GET/DELETE endpoints behind requireAuth().
  3. Frontend — A reusable LinkedItems React component rendered on Archer and Jira detail views.

Architecture

flowchart TD
    subgraph Frontend
        A[ArcherTicketDetail] --> C[LinkedItems Component]
        B[JiraPage Detail View] --> C
        C -->|POST /api/links| D[Links Router]
        C -->|GET /api/links?type=&id=| D
        C -->|DELETE /api/links/:id| D
    end

    subgraph Backend
        D --> E[Validation Layer]
        E --> F[PostgreSQL Pool]
    end

    subgraph Database
        F --> G[ticket_links table]
        F --> H[audit_logs table]
    end

Design Decisions

Decision Rationale
TEXT columns for source/target types and IDs Avoids polymorphic FK complexity; entity types have well-defined ID patterns validated at the application layer
Bidirectional queries via OR conditions Single query returns links regardless of which side was the "source" at creation time
Duplicate prevention checks both (A→B) and (B→A) Treats links as undirected edges — prevents logical duplicates stored in different directions
Separate ticket_links table (not modifying existing FKs) Purely additive; zero risk to existing Archer/Jira/CVE workflows
Relationship types as TEXT with CHECK constraint Extensible without migrations; initial set: related, spawned_by, blocks
Auto-detect entity type from key format Reduces user friction — no need to manually select a type dropdown

Components and Interfaces

Backend: routes/links.js

// Express router factory — follows existing project pattern
function createLinksRouter() {
  const router = express.Router();

  // POST /api/links — Create a new link
  router.post('/', requireAuth(), async (req, res) => { /* ... */ });

  // GET /api/links?type=archer&id=EXC-6056 — Query links for an entity
  router.get('/', requireAuth(), async (req, res) => { /* ... */ });

  // DELETE /api/links/:id — Remove a link by ID
  router.delete('/:id', requireAuth(), async (req, res) => { /* ... */ });

  return router;
}

Validation helpers:

const ENTITY_PATTERNS = {
  archer: /^EXC-\d{4,}$/,
  jira: /^[A-Z][A-Z0-9_]+-\d+$/,
  cve: /^CVE-\d{4}-\d{4,}$/
};

const VALID_ENTITY_TYPES = ['archer', 'jira', 'cve'];
const VALID_RELATIONSHIPS = ['related', 'spawned_by', 'blocks'];

function detectEntityType(key) {
  if (ENTITY_PATTERNS.cve.test(key)) return 'cve';
  if (ENTITY_PATTERNS.archer.test(key)) return 'archer';
  if (ENTITY_PATTERNS.jira.test(key)) return 'jira';
  return null;
}

Frontend: LinkedItems Component

Props:
  - entityType: 'archer' | 'jira' | 'cve'
  - entityId: string (e.g., 'EXC-6056')

State:
  - links: Array<LinkRecord>
  - loading: boolean
  - showAddForm: boolean
  - error: string | null

The component fetches links on mount via GET /api/links?type={entityType}&id={entityId}, renders a list of linked items with type badges and relationship labels, and provides "Add Link" / "Remove" controls.

API Contract

POST /api/links

// Request
{
  "source_type": "archer",
  "source_id": "EXC-6056",
  "target_type": "jira",
  "target_id": "STEAM-1234",
  "relationship": "related"
}

// Response 201
{
  "id": 42,
  "source_type": "archer",
  "source_id": "EXC-6056",
  "target_type": "jira",
  "target_id": "STEAM-1234",
  "relationship": "related",
  "created_by": 7,
  "created_at": "2025-01-15T10:30:00Z"
}

GET /api/links?type=archer&id=EXC-6056

// Response 200
[
  {
    "id": 42,
    "linked_type": "jira",
    "linked_id": "STEAM-1234",
    "relationship": "related",
    "created_by_username": "jsmith",
    "created_at": "2025-01-15T10:30:00Z"
  }
]

DELETE /api/links/:id

// Response 200
{ "message": "Link deleted successfully" }

Data Models

CREATE TABLE IF NOT EXISTS ticket_links (
    id SERIAL PRIMARY KEY,
    source_type TEXT NOT NULL CHECK (source_type IN ('archer', 'jira', 'cve')),
    source_id TEXT NOT NULL,
    target_type TEXT NOT NULL CHECK (target_type IN ('archer', 'jira', 'cve')),
    target_id TEXT NOT NULL,
    relationship TEXT NOT NULL DEFAULT 'related'
        CHECK (relationship IN ('related', 'spawned_by', 'blocks')),
    created_by INTEGER REFERENCES users(id),
    created_at TIMESTAMPTZ DEFAULT NOW(),
    UNIQUE (source_type, source_id, target_type, target_id)
);

CREATE INDEX IF NOT EXISTS idx_ticket_links_source
    ON ticket_links(source_type, source_id);
CREATE INDEX IF NOT EXISTS idx_ticket_links_target
    ON ticket_links(target_type, target_id);

Bidirectional Query Pattern

-- Find all links involving entity (type=?, id=?)
SELECT * FROM ticket_links
WHERE (source_type = $1 AND source_id = $2)
   OR (target_type = $1 AND target_id = $2);

Duplicate Prevention Query

Before inserting a new link (A→B), check that neither (A→B) nor (B→A) already exists:

SELECT id FROM ticket_links
WHERE (source_type = $1 AND source_id = $2 AND target_type = $3 AND target_id = $4)
   OR (source_type = $3 AND source_id = $4 AND target_type = $1 AND target_id = $2);

Migration File

A new migration add_ticket_links_table.js will be added to backend/migrations/ following the existing pattern (idempotent DDL via CREATE TABLE IF NOT EXISTS).

Correctness Properties

A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.

Property 1: Entity Type Detection from Key Format

For any valid entity key matching one of the known patterns (EXC-XXXX for Archer, PROJECT-XXXX for Jira, CVE-YYYY-NNNNN for CVE), the detectEntityType function SHALL return the correct entity type; and for any string not matching any known pattern, it SHALL return null.

Validates: Requirements 6.4, 1.3, 2.4

Property 2: Entity ID Format Validation

For any (entity_type, entity_id) pair where the entity_id does not match the expected regex pattern for that type, the Link_Service validation SHALL reject the input; and for any pair where the entity_id does match, validation SHALL accept it.

Validates: Requirements 2.5

For any valid entity (type, id), attempting to create a link where source and target are identical SHALL be rejected with a validation error, and the ticket_links table SHALL remain unchanged.

Validates: Requirements 2.2

Property 4: Bidirectional Duplicate Prevention

For any two distinct entities A and B, after successfully creating a link from A to B, attempting to create either A→B again or B→A SHALL be rejected with a conflict error.

Validates: Requirements 2.3, 8.3

Property 5: Bidirectional Query Completeness

For any entity E that appears in the ticket_links table (as either source or target), querying links for E SHALL return every link record where E appears on either side, and SHALL NOT return any link record where E does not appear.

Validates: Requirements 3.1, 8.1, 8.2

For any valid (source_type, source_id, target_type, target_id, relationship) tuple where source ≠ target and no duplicate exists, creating the link and then querying for it SHALL return a record with matching source_type, source_id, target_type, target_id, and relationship values.

Validates: Requirements 2.1, 3.2

For any existing link record, after deleting it by ID, querying links for either the source or target entity SHALL no longer include that link in the results.

Validates: Requirements 4.1

Error Handling

Backend Error Responses

Scenario HTTP Status Response Body
Missing/invalid auth 401 { "error": "Authentication required" }
Invalid entity type 400 { "error": "Invalid entity type. Must be one of: archer, jira, cve" }
Invalid entity ID format 400 { "error": "Invalid ID format for type '{type}'. Expected: {pattern}" }
Self-link attempt 400 { "error": "Cannot link an entity to itself" }
Duplicate link (either direction) 409 { "error": "A link between these entities already exists", "existing_id": N }
Link not found on delete 404 { "error": "Link not found" }
Database error 500 { "error": "Internal server error" }
Missing required fields 400 { "error": "source_type, source_id, target_type, target_id are required" }

Frontend Error Handling

  • API errors are caught and displayed inline in the LinkedItems component (toast or inline message).
  • Network failures show a generic "Unable to reach server" message with a retry option.
  • The form is NOT cleared on error so the user can correct and resubmit.
  • Optimistic UI is NOT used — the list only refreshes after confirmed server success.

Edge Cases

  • Concurrent duplicate creation: The UNIQUE constraint on the table acts as a final safety net if two requests pass the application-layer check simultaneously. The second insert will fail with a Postgres 23505 error, which the backend maps to a 409 response.
  • Deleted entities: Links are not cascade-deleted when an entity is removed from its source table. Orphaned links are acceptable — the UI will show the ID but may indicate "entity not found" if navigation fails.
  • Very long entity IDs: Entity IDs are TEXT columns with no length limit at the DB level, but the regex patterns enforce reasonable lengths (Archer: 4+ digits, Jira: standard key format, CVE: standard format).

Testing Strategy

Unit Tests (Example-Based)

  • Validation function tests: specific valid/invalid inputs for each entity type
  • Default relationship value when omitted
  • Auth rejection (401) for unauthenticated requests
  • Empty state rendering in LinkedItems component
  • Form display/hide toggle on "Add Link" button click
  • Error message display on API failure
  • Confirmation prompt on remove action

Property-Based Tests

Library: fast-check (already compatible with the project's Jest test runner)

Configuration: Minimum 100 iterations per property test.

Each property test will be tagged with a comment referencing the design property:

// Feature: ticket-linking, Property N: {property_text}

Properties to implement:

  1. Entity type detection — Generate random strings matching/not-matching patterns, verify detectEntityType correctness
  2. Entity ID format validation — Generate (type, id) pairs with valid/invalid formats, verify acceptance/rejection
  3. Self-link rejection — Generate random valid entities, verify self-link is always rejected
  4. Bidirectional duplicate prevention — Generate entity pairs, create link, verify both directions are blocked
  5. Bidirectional query completeness — Generate a set of links, verify query returns exactly the correct subset
  6. Link creation round-trip — Generate valid link inputs, create and query, verify data integrity
  7. Delete removes from queries — Generate links, delete one, verify it disappears from both sides

Integration Tests

  • Full POST → GET → DELETE lifecycle against a test database
  • Audit log entries created on link creation and deletion
  • UNIQUE constraint fires on concurrent duplicate inserts
  • Migration is idempotent (can run multiple times without error)
  • Existing archer_tickets and jira_tickets schemas unchanged after migration