Update .kiro: remove SQLite hooks, add PostgreSQL migration hook, add workflow steering, sync specs
This commit is contained in:
311
.kiro/specs/ticket-linking/design.md
Normal file
311
.kiro/specs/ticket-linking/design.md
Normal 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
|
||||
Reference in New Issue
Block a user