Update .kiro: remove SQLite hooks, add PostgreSQL migration hook, add workflow steering, sync specs

This commit is contained in:
Jordan Ramos
2026-05-12 14:45:58 -06:00
parent 3ee8487286
commit 1bb8ec1658
35 changed files with 4645 additions and 48 deletions

View File

@@ -0,0 +1 @@
{"specId": "c9369370-989b-476a-8305-b62401390b71", "workflowType": "requirements-first", "specType": "feature"}

View File

@@ -0,0 +1,311 @@
# 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
```mermaid
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`
```javascript
// 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:**
```javascript
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**
```json
// 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**
```json
// 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**
```json
// Response 200
{ "message": "Link deleted successfully" }
```
## Data Models
### `ticket_links` Table
```sql
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
```sql
-- 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:
```sql
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**
### Property 3: Self-Link Rejection
*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**
### Property 6: Link Creation Round-Trip
*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**
### Property 7: Delete Removes Link from Queries
*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](https://github.com/dubzzz/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

View File

@@ -0,0 +1,123 @@
# Requirements Document
## Introduction
The CVE Dashboard currently tracks three entity types — Archer tickets (risk acceptance exceptions), Jira tickets (work items), and CVEs (vulnerabilities) — but relationships between them are limited to a single primary CVE foreign key on each ticket. This feature introduces a generic `ticket_links` table that enables bidirectional, many-to-many associations between any combination of these entities. Users will be able to view, create, and remove links from ticket detail views in the UI.
## Glossary
- **Link_Service**: The backend service responsible for creating, querying, and deleting ticket links.
- **Link_UI**: The frontend component that displays linked items and provides controls for adding/removing links.
- **Ticket_Links_Table**: The PostgreSQL table storing associations between entities.
- **Entity**: One of the three linkable types in the system — an Archer ticket, a Jira ticket, or a CVE.
- **Source**: The entity on the left side of a link record.
- **Target**: The entity on the right side of a link record.
- **Entity_Type**: A classification string identifying the kind of entity: `archer`, `jira`, or `cve`.
- **Entity_ID**: The unique identifier for an entity within its type (e.g., `EXC-6056`, `STEAM-1234`, `CVE-2025-0905`).
- **Relationship**: A label describing the nature of a link (e.g., `related`, `spawned_by`, `blocks`).
## Requirements
### Requirement 1: Create the ticket_links Table
**User Story:** As a database administrator, I want a dedicated table for storing entity associations, so that any entity can be linked to any other entity without schema changes.
#### Acceptance Criteria
1. THE Ticket_Links_Table SHALL store a source entity (source_type, source_id) and a target entity (target_type, target_id) in each row.
2. THE Ticket_Links_Table SHALL enforce a UNIQUE constraint on the combination of (source_type, source_id, target_type, target_id) to prevent duplicate links.
3. THE Ticket_Links_Table SHALL restrict source_type and target_type values to `archer`, `jira`, or `cve`.
4. THE Ticket_Links_Table SHALL store a relationship label defaulting to `related`.
5. THE Ticket_Links_Table SHALL record the user who created the link via a created_by foreign key to the users table.
6. THE Ticket_Links_Table SHALL record the creation timestamp defaulting to the current time.
7. THE Ticket_Links_Table SHALL include indexes on (source_type, source_id) and (target_type, target_id) to support efficient bidirectional queries.
### Requirement 2: Create a Link
**User Story:** As a security analyst, I want to create a link between two entities, so that I can document relationships between Archer exceptions, Jira work items, and CVEs.
#### Acceptance Criteria
1. WHEN a valid source entity, target entity, and relationship are provided, THE Link_Service SHALL insert a new row into the Ticket_Links_Table and return the created link record.
2. WHEN the source entity and target entity are identical (same type and same ID), THE Link_Service SHALL reject the request with a validation error.
3. WHEN a link between the same source and target already exists, THE Link_Service SHALL reject the request with a conflict error indicating the duplicate.
4. WHEN the source_type or target_type is not one of `archer`, `jira`, or `cve`, THE Link_Service SHALL reject the request with a validation error.
5. WHEN the entity ID format does not match the expected pattern for its type, THE Link_Service SHALL reject the request with a validation error.
6. THE Link_Service SHALL require the user to be authenticated before creating a link.
7. THE Link_Service SHALL log an audit entry when a link is successfully created.
### Requirement 3: Query Links for an Entity
**User Story:** As a security analyst, I want to see all items linked to a given entity, so that I can understand the full context of a ticket or vulnerability.
#### Acceptance Criteria
1. WHEN an entity type and entity ID are provided, THE Link_Service SHALL return all links where the entity appears as either source or target (bidirectional query).
2. THE Link_Service SHALL return each linked entity's type, ID, relationship label, creator, and creation timestamp.
3. WHEN no links exist for the given entity, THE Link_Service SHALL return an empty list.
4. THE Link_Service SHALL require the user to be authenticated before querying links.
### Requirement 4: Delete a Link
**User Story:** As a security analyst, I want to remove a link between two entities, so that I can correct mistakes or remove outdated associations.
#### Acceptance Criteria
1. WHEN a valid link ID is provided, THE Link_Service SHALL delete the corresponding row from the Ticket_Links_Table.
2. WHEN the provided link ID does not exist, THE Link_Service SHALL return a not-found error.
3. THE Link_Service SHALL require the user to be authenticated before deleting a link.
4. THE Link_Service SHALL log an audit entry when a link is successfully deleted.
### Requirement 5: Display Linked Items in the UI
**User Story:** As a security analyst, I want to see a "Linked Items" section on ticket detail views, so that I can quickly navigate between related entities.
#### Acceptance Criteria
1. WHILE viewing an Archer ticket detail page, THE Link_UI SHALL display a "Linked Items" section listing all entities linked to that Archer ticket.
2. WHILE viewing a Jira ticket detail page, THE Link_UI SHALL display a "Linked Items" section listing all entities linked to that Jira ticket.
3. WHEN no links exist for the displayed entity, THE Link_UI SHALL show an empty state message indicating no linked items.
4. THE Link_UI SHALL display each linked item with its entity type, entity ID, and relationship label.
5. THE Link_UI SHALL render each linked item's entity ID as a navigable link to that entity's detail view.
### Requirement 6: Add a Link from the UI
**User Story:** As a security analyst, I want an "Add Link" button on ticket detail views, so that I can create associations without leaving the page.
#### Acceptance Criteria
1. WHEN the user clicks the "Add Link" button, THE Link_UI SHALL display a form allowing the user to enter a target entity key and select a relationship type.
2. WHEN the user submits the form with a valid entity key, THE Link_UI SHALL call the Link_Service to create the link and refresh the Linked Items section.
3. WHEN the Link_Service returns a validation or conflict error, THE Link_UI SHALL display the error message to the user without clearing the form.
4. THE Link_UI SHALL auto-detect the entity type from the entered key format (EXC-XXXX for Archer, PROJECT-XXXX for Jira, CVE-YYYY-NNNNN for CVE).
### Requirement 7: Remove a Link from the UI
**User Story:** As a security analyst, I want to remove a link directly from the Linked Items section, so that I can manage associations without navigating away.
#### Acceptance Criteria
1. THE Link_UI SHALL display a remove control on each linked item in the Linked Items section.
2. WHEN the user activates the remove control, THE Link_UI SHALL prompt for confirmation before proceeding.
3. WHEN the user confirms removal, THE Link_UI SHALL call the Link_Service to delete the link and refresh the Linked Items section.
4. WHEN the Link_Service returns an error during removal, THE Link_UI SHALL display the error message to the user.
### Requirement 8: Bidirectional Link Symmetry
**User Story:** As a security analyst, I want links to be visible from both sides of the association, so that navigating from either entity shows the connection.
#### Acceptance Criteria
1. WHEN a link is created from Entity A to Entity B, THE Link_Service SHALL return that link when queried from Entity A.
2. WHEN a link is created from Entity A to Entity B, THE Link_Service SHALL return that link when queried from Entity B.
3. THE Link_Service SHALL treat (source=A, target=B) and (source=B, target=A) as the same logical link — creating one SHALL prevent creating the other.
### Requirement 9: Links are Optional
**User Story:** As a security analyst, I want the linking feature to be purely additive, so that existing tickets without links continue to function normally.
#### Acceptance Criteria
1. THE Link_Service SHALL not require any entity to have links — entities with zero links remain fully functional.
2. THE Link_Service SHALL not modify the existing cve_id and vendor foreign key columns on archer_tickets or jira_tickets.
3. WHEN an entity has no links, THE Link_UI SHALL display the empty state without errors or warnings.

View File

@@ -0,0 +1,169 @@
# 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_links` table 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 EXISTS` and `CREATE INDEX IF NOT EXISTS` for 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_
- [ ] 1.2 Add `ticket_links` DDL to `backend/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_links` table exists with correct columns, constraints, and indexes. Ask the user if questions arise.
- [ ] 3. Backend API route
- [ ] 3.1 Create `backend/routes/links.js` with 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_
- [ ]* 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 `detectEntityType` returns 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_
- [ ]* 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) and `id` (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_
- [ ]* 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_
- [ ]* 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 `createLinksRouter` from `./routes/links`
- Add `app.use('/api/links', createLinksRouter())` alongside existing route mounts
- _Requirements: 2.6, 3.4, 4.3_
- [ ] 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') and `entityId` (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_
- [ ] 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"` and `entityId={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"` and `entityId={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_id` column 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