10 KiB
Implementation Plan: Ticket Linking
Overview
Add a generic bidirectional many-to-many linking system between Archer tickets, Jira tickets, and CVEs. Implementation spans three layers: a ticket_links PostgreSQL table with CHECK constraints and bidirectional indexes, a new Express router at backend/routes/links.js with POST/GET/DELETE endpoints behind requireAuth(), and a reusable LinkedItems React component mounted on Archer and Jira detail views. Entity type is auto-detected from key format. Audit logging on create/delete.
Tasks
-
1. Database schema and migration
-
1.1 Create migration file
backend/migrations/add_ticket_links_table.js- Create
ticket_linkstable with columns: id (SERIAL PK), source_type (TEXT NOT NULL), source_id (TEXT NOT NULL), target_type (TEXT NOT NULL), target_id (TEXT NOT NULL), relationship (TEXT NOT NULL DEFAULT 'related'), created_by (INTEGER REFERENCES users(id)), created_at (TIMESTAMPTZ DEFAULT NOW()) - Add CHECK constraints: source_type IN ('archer', 'jira', 'cve'), target_type IN ('archer', 'jira', 'cve'), relationship IN ('related', 'spawned_by', 'blocks')
- Add UNIQUE constraint on (source_type, source_id, target_type, target_id)
- Add indexes: idx_ticket_links_source ON (source_type, source_id), idx_ticket_links_target ON (target_type, target_id)
- Use
CREATE TABLE IF NOT EXISTSandCREATE INDEX IF NOT EXISTSfor idempotence - Use the PostgreSQL pool from
backend/db.js(not SQLite) - Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7
- Create
-
1.2 Add
ticket_linksDDL tobackend/db-schema.sql- Append the CREATE TABLE and CREATE INDEX statements to the existing schema file for documentation and fresh-install setup
- Requirements: 1.1, 1.2, 1.3, 1.7
-
1.3 Run migration against development database
- Execute the migration script to create the table on the running Postgres instance
- Verify table exists with correct constraints via a quick SELECT
- Requirements: 1.1
-
-
2. Checkpoint — Verify database schema
- Ensure the
ticket_linkstable exists with correct columns, constraints, and indexes. Ask the user if questions arise.
- Ensure the
-
3. Backend API route
-
3.1 Create
backend/routes/links.jswith validation helpers- Define ENTITY_PATTERNS regex map: archer
/^EXC-\d{4,}$/, jira/^[A-Z][A-Z0-9_]+-\d+$/, cve/^CVE-\d{4}-\d{4,}$/ - Implement
detectEntityType(key)function that returns 'cve', 'archer', 'jira', or null based on pattern matching - Implement
validateEntityId(type, id)that checks the ID matches the pattern for the given type - Define VALID_RELATIONSHIPS array: ['related', 'spawned_by', 'blocks']
- Requirements: 2.4, 2.5, 6.4
- Define ENTITY_PATTERNS regex map: archer
-
* 3.2 Write property test for entity type detection (Property 1)
- Property 1: Entity Type Detection from Key Format
- Generate random strings matching EXC-XXXX, PROJECT-XXXX, CVE-YYYY-NNNNN patterns and verify
detectEntityTypereturns correct type; generate non-matching strings and verify null return - Validates: Requirements 6.4, 1.3, 2.4
-
* 3.3 Write property test for entity ID format validation (Property 2)
- Property 2: Entity ID Format Validation
- Generate (entity_type, entity_id) pairs with valid and invalid formats, verify validation accepts/rejects correctly
- Validates: Requirements 2.5
-
3.4 Implement POST /api/links endpoint
- Require authentication via
requireAuth() - Validate required fields: source_type, source_id, target_type, target_id
- Validate entity types are in ['archer', 'jira', 'cve']
- Validate entity ID formats match expected patterns for their types
- Reject self-links (same type AND same ID) with 400
- Check for duplicate links in both directions (A→B and B→A) with SELECT query, return 409 if exists
- Insert new row into ticket_links with created_by from req.user.id
- Log audit entry via
logAudit()with action 'link_create' - Return 201 with created link record
- Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 8.3
- Require authentication via
-
* 3.5 Write property test for self-link rejection (Property 3)
- Property 3: Self-Link Rejection
- Generate valid entities, attempt self-link creation, verify rejection with 400 status
- Validates: Requirements 2.2
-
* 3.6 Write property test for bidirectional duplicate prevention (Property 4)
- Property 4: Bidirectional Duplicate Prevention
- Generate entity pairs, create link A→B, attempt A→B again and B→A, verify both return 409
- Validates: Requirements 2.3, 8.3
-
3.7 Implement GET /api/links endpoint
- Require authentication via
requireAuth() - Accept query params:
type(entity type) andid(entity ID) - Validate type and id are provided
- Execute bidirectional query: SELECT where (source_type=$1 AND source_id=$2) OR (target_type=$1 AND target_id=$2)
- Transform results to return
linked_type,linked_id(the "other" side from the queried entity), relationship, created_by_username (JOIN with users table), created_at - Return empty array when no links exist
- Requirements: 3.1, 3.2, 3.3, 3.4, 8.1, 8.2
- Require authentication via
-
* 3.8 Write property test for bidirectional query completeness (Property 5)
- Property 5: Bidirectional Query Completeness
- Generate a set of links, query for a specific entity, verify result contains exactly the links where that entity appears on either side
- Validates: Requirements 3.1, 8.1, 8.2
-
* 3.9 Write property test for link creation round-trip (Property 6)
- Property 6: Link Creation Round-Trip
- Generate valid link inputs (source ≠ target, no duplicate), create link, query for it, verify returned record matches input values
- Validates: Requirements 2.1, 3.2
-
3.10 Implement DELETE /api/links/:id endpoint
- Require authentication via
requireAuth() - Query the link by ID to verify it exists, return 404 if not found
- Delete the row from ticket_links
- Log audit entry via
logAudit()with action 'link_delete', including source/target details - Return 200 with success message
- Requirements: 4.1, 4.2, 4.3, 4.4
- Require authentication via
-
* 3.11 Write property test for delete removes link from queries (Property 7)
- Property 7: Delete Removes Link from Queries
- Generate links, delete one by ID, verify querying from either side no longer includes the deleted link
- Validates: Requirements 4.1
-
3.12 Mount links router in
backend/server.js- Import
createLinksRouterfrom./routes/links - Add
app.use('/api/links', createLinksRouter())alongside existing route mounts - Requirements: 2.6, 3.4, 4.3
- Import
-
-
4. Checkpoint — Verify backend API
- Ensure POST/GET/DELETE endpoints work correctly, validation rejects bad input, duplicates return 409, bidirectional queries return links from both sides, and audit logs are created. Ask the user if questions arise.
-
5. Frontend LinkedItems component
-
5.1 Create
frontend/src/components/LinkedItems.js- Accept props:
entityType('archer' | 'jira' | 'cve') andentityId(string) - Manage state: links array, loading boolean, showAddForm boolean, error string
- On mount, fetch links from
GET /api/links?type={entityType}&id={entityId} - Render "Linked Items" section header
- Display empty state message when no links exist
- Render each linked item with: entity type badge, entity ID as navigable link, relationship label
- Display remove button (×) on each linked item
- Requirements: 5.1, 5.2, 5.3, 5.4, 5.5, 7.1
- Accept props:
-
5.2 Implement "Add Link" form in LinkedItems component
- "Add Link" button toggles form visibility
- Form contains: text input for target entity key, dropdown for relationship type (related, spawned_by, blocks)
- Auto-detect entity type from entered key using same regex patterns as backend
- On submit, call POST /api/links with source (current entity) and target (entered key)
- On success, refresh the links list
- On error, display error message inline without clearing the form
- Requirements: 6.1, 6.2, 6.3, 6.4
-
5.3 Implement link removal in LinkedItems component
- Remove button triggers confirmation prompt
- On confirm, call DELETE /api/links/:id
- On success, refresh the links list
- On error, display error message to user
- Requirements: 7.1, 7.2, 7.3, 7.4
-
-
6. Integrate into existing views
-
6.1 Mount LinkedItems on Archer ticket detail view
- Import LinkedItems component
- Render with
entityType="archer"andentityId={excNumber}(the EXC-XXXX identifier) - Place in the detail panel below existing ticket information
- Requirements: 5.1, 8.1, 8.2, 9.1
-
6.2 Mount LinkedItems on Jira ticket detail/page view
- Import LinkedItems component
- Render with
entityType="jira"andentityId={ticketKey}(the PROJECT-XXXX identifier) - Place in the detail view below existing ticket information
- Requirements: 5.2, 8.1, 8.2, 9.1
-
-
7. Checkpoint — Verify end-to-end flow
- Ensure LinkedItems renders on both Archer and Jira detail views, links can be created/viewed/deleted from the UI, bidirectional visibility works (link created from Archer shows on Jira side), and entities with no links show empty state without errors. Ask the user if questions arise.
-
8. Final checkpoint — Ensure all tests pass
- Ensure all tests pass, audit log entries are created on link create/delete, and the feature is fully functional. Ask the user if questions arise.
Notes
- Tasks marked with
*are optional and can be skipped for faster MVP - Each task references specific requirements for traceability
- Checkpoints ensure incremental validation between major phases
- Property tests validate universal correctness properties from the design document
- The design uses JavaScript — all code examples and implementations use Node.js/Express/React
- Entity IDs are text-based (EXC-6056, STEAM-1234, CVE-2025-0905) — not integer PKs
- The existing
cve_idcolumn on archer/jira tickets remains unchanged (backward compatible) - Links are purely additive — they supplement the primary CVE relationship, not replace it
- Auto-detection of entity type from ID format reduces user friction
- Duplicate prevention checks both directions (A→B and B→A) since links are logically undirected
- The UNIQUE constraint on the table acts as a final safety net for concurrent duplicate inserts