* docs: add PRD traceability and work-item hierarchy design (#773) Design document for first-class traceability layer connecting tasks to PRD requirements, risks, decisions, and verification evidence. Key design decisions addressed: - WorkItemLevel uses 'child-task' not 'subtask' to avoid collision with existing task.subtasks[] checklist model - next_safe query evaluates depends_on ∪ blockedBy (covers both modern dependency graph and legacy blockedBy semantics) - next_safe forces status=todo; returns 400 on conflicting status filter - stopConditions includes stopConditionResolved map for machine-queryable state rather than free-form strings only - Coverage endpoints introduce optional project requirement/risk catalogs (POST /api/projects/:id/catalog/{requirements,risks}) to enable true uncovered-row semantics; without a catalog, total = observed IDs only - Verification semantics: 'verified' requires done task + checked verificationSteps or verificationIds (presence alone is insufficient) - Archive/hierarchy: ON DELETE SET NULL is physical-delete-only; service layer warns on archiving parents with active children - Cross-scope parent links rejected at the project level (400) - SQLite JSON columns for ID arrays with json_each() query model; forward path to normalized junction tables documented Changes: - docs/features/prd-traceability.md — new design doc (958 lines) - docs/FEATURES.md — add design-draft entry with link to doc GPT cross-model review addressed before commit. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * docs: fix gated/blocked next-safe exclusion and archive wording consistency Two semantic contradictions flagged in PR #800 review: 1. next_safe gated exclusion — RiskDisposition.gated is documented as 'this task may not proceed until the gate is cleared' and coverage treats gated as an open risk, but the algorithm only excluded blocked and unknown. Fix: exclude blocked and gated always (no override), exclude unknown unless allow_unknown_risks=true. Updated in: - next-safe algorithm criterion 6+7 - acceptance criterion #5 and #7 - rollout step 10 - B-5 backlog row 2. Archive wording mismatch — SQLite schema section said 'issues a warning and requires reparent or cascade archive', but AC #14 said 'warning only'. Resolved as warning-only throughout: archive proceeds regardless, children retain parentId, response includes archiveWarning field. Updated in: - SQLite schema archive semantics prose Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
39 KiB
PRD Traceability and Work-Item Hierarchy
Status: Design Draft
GitHub Issue: #773
Priority: High
Related: PRD-Driven Development, SQLite Schema, API Reference
Overview
This document specifies a first-class, optional traceability layer for Veritas Kanban that
connects work items to the PRD requirements, risks, decisions, and verification evidence they
satisfy. Combined with an explicit work-item hierarchy (initiative → epic → story → task → child-task), this layer allows both humans and autonomous agents to answer planning and safety
questions from structured fields—without parsing prose descriptions or inferring hierarchy from
title prefixes.
The design is entirely additive. Existing boards continue to work with zero changes. All new fields are optional. When traceability metadata is absent, all existing behavior is unchanged.
Current State
Veritas Kanban v5.2.1 already provides:
| Existing capability | Surface |
|---|---|
| Project and sprint grouping | task.project, task.sprint |
| Subtasks with AC | task.subtasks[].acceptanceCriteria |
| Top-level verification | task.verificationSteps |
| Bidirectional dependencies | task.dependencies.depends_on / .blocks |
| Blocked state and reasons | task.blockedReason, BlockedCategory |
| Observations | task.observations (decision, blocker, insight, context) |
| Deliverables | task.deliverables |
| External tracker backlinks | task.externalWorkItems |
| Task readiness checks | shared/src/utils/task-readiness.ts |
| PRD-driven development SOPs | docs/features/prd-driven-development.md |
Not yet present in any surface:
- Parent task ID / hierarchy link
- Explicit work-item level (
initiative,epic,story,task,child-task) — note: the levelsubtaskis deliberately avoided here becausetask.subtasks[]already uses that term for checklist items;child-taskrefers to a full-record structural child (see Design section) - PRD requirement IDs mapped to tasks
- Risk IDs and per-risk disposition (
mitigated,gated,accepted,blocked,deferred) - Verification IDs linking to test/evidence artifacts
- Queryable human-gate labels as structured fields
- Stop conditions as named predicates with a resolved/unresolved toggle
- Dependency-aware "next safe unblocked task" selection that considers gate and risk state
Problem Statement
The current state works for human-driven boards where practitioners can read descriptions and infer context. For autonomous or semi-autonomous agent work it creates fragile operating conditions:
-
Agents infer hierarchy from prose. Title prefixes like
EPIC-01AorUS-3are conventions, not structured fields. Agents must parse them and risk misreading new formats. -
Requirement coverage is invisible. There is no machine-readable answer to "which PRD requirement does this task satisfy?" or "which requirements have no associated tasks?"
-
Risk disposition is buried in comments. Whether a given risk is mitigated, gated, accepted, or still open requires reading comments. Autonomous agents cannot safely evaluate whether it is appropriate to proceed.
-
Human gates are implicit. Tasks that require explicit human approval before an agent proceeds encode that constraint in task descriptions or observation notes, not in queryable fields.
-
"What is the next safe thing to do?" is underdefined.
task.status = 'todo'and alldependencies.depends_ondone is necessary but not sufficient. Gate and risk state matter too.
Goals
- Give agents structured planning metadata to safely choose the next work item.
- Give humans auditable requirement-coverage and risk-coverage views without parsing prose.
- Make existing dependency, verification, and observation surfaces more useful by connecting them to explicit IDs.
- Remain completely optional. No existing workflow changes.
Non-Goals
- Not a Jira-style process requirement. Nothing in this design is mandatory.
- Not a replacement for projects, sprints, subtasks, or dependencies.
- Not a full Gantt or critical-path system.
- Not storing the full PRD inside VK. Requirement IDs reference durable external docs.
- Not a new top-level entity type. Hierarchy is expressed through fields on existing tasks.
Design
Work-Item Hierarchy Model
The hierarchy is a labeling convention on individual tasks. VK does not enforce a strict
tree structure—tasks may exist without a level or parent. The parentId field creates an
optional parent–child edge navigable through the API.
initiative
└─ epic
└─ story
└─ task
└─ child-task ← full-record structural child task
child-task vs. task.subtasks[]: VK already uses task.subtasks[] for lightweight
checklist items (not full task records). The level child-task is for a full first-class
Task record that belongs under a parent task in the hierarchy. To avoid any naming
collision, the WorkItemLevel union uses 'child-task' (not 'subtask').
Level semantics are advisory and do not gate status transitions. Teams may use any subset of levels.
TypeScript Schema
New types in shared/src/types/task.types.ts:
/**
* Five-level work-item hierarchy. All values are optional labels; enforcement is advisory.
* NOTE: 'child-task' is used instead of 'subtask' to avoid collision with
* task.subtasks[], which holds lightweight checklist items (not full task records).
*/
export type WorkItemLevel = 'initiative' | 'epic' | 'story' | 'task' | 'child-task';
/**
* Disposition of a tracked risk as it relates to this task.
* - mitigated — Risk is resolved; evidence present or linked in verificationIds.
* - gated — Risk remains open; this task may not proceed until the gate is cleared.
* - accepted — Risk is acknowledged and consciously accepted by the team.
* - blocked — Task is blocked because this risk is unresolved.
* - deferred — Risk handling is intentionally deferred to a later task or phase.
* - unknown — Risk disposition has not been evaluated (default when riskIds set but disposition absent).
*/
export type RiskDisposition =
| 'mitigated'
| 'gated'
| 'accepted'
| 'blocked'
| 'deferred'
| 'unknown';
/**
* Optional traceability layer for a task.
* All fields are optional; absent traceability is equivalent to an empty object.
* Existing board behavior is entirely unaffected when this field is absent or null.
*
* PATCH semantics: sending `"traceability": null` clears all traceability.
* Sending a partial object replaces each present key. To clear a scalar field, pass null.
* To clear an array field, pass []. To clear riskDisposition/stopConditionResolved, pass {}.
*/
export interface TaskTraceability {
/**
* ID of the parent task in the work-item hierarchy.
* Pass null to explicitly clear a previously set parentId.
* The referenced task must exist in the same workspace.
* Circular parent chains are rejected with 400.
*/
parentId?: string | null;
/**
* Advisory level in the work-item hierarchy.
* 'child-task' refers to a full-record structural child task.
* This is distinct from task.subtasks[], which holds lightweight checklist items.
* Pass null to clear.
*/
workItemLevel?: WorkItemLevel | null;
/**
* PRD requirement IDs this task satisfies. Format is project-defined.
* Examples: ["REQ-001", "REQ-017"], ["auth.1", "auth.2"].
* Coverage reporting cross-references these against the project requirement catalog.
* Replaces the array on each update; pass [] to clear.
*/
requirementIds?: string[];
/**
* Risk IDs tracked by this task. Format is project-defined.
* Examples: ["RISK-SEC-01"], ["auth-risk-1"].
* Replaces the array on each update; pass [] to clear.
*/
riskIds?: string[];
/**
* Decision IDs that affect this task. Format is project-defined.
* Examples: ["ADR-0001"], ["DEC-2026-03-auth"].
* Replaces the array on each update; pass [] to clear.
*/
decisionIds?: string[];
/**
* Verification / test IDs that prove requirements or risks are handled.
* Examples: ["TEST-42"], ["spec/auth.test.ts"].
* Cross-references to task.verificationSteps are by description or position within the same task.
* Replaces the array on each update; pass [] to clear.
*/
verificationIds?: string[];
/**
* Named human-gate labels.
* Any non-empty humanGates array blocks next-safe selection for this task.
* Pass [] to clear all gates.
*/
humanGates?: string[];
/**
* Named stop conditions. Each condition is a free-form string.
* Resolved state is tracked separately in stopConditionResolved.
* next_safe excludes tasks with at least one unresolved stop condition.
* Pass [] to clear.
*/
stopConditions?: string[];
/**
* Resolved state for each stop condition, keyed by condition string.
* A stop condition absent from this map (but present in stopConditions) is unresolved.
* Example: { "regression suite must be green": true }
* Pass {} to reset all resolved states.
*/
stopConditionResolved?: Record<string, boolean>;
/**
* Per-risk disposition keyed by risk ID.
* Disposition for IDs not present in this map is treated as 'unknown'.
* Example: { "RISK-SEC-01": "mitigated", "RISK-PERF-02": "gated" }
* Pass {} to reset all dispositions.
*/
riskDisposition?: Record<string, RiskDisposition>;
}
Add to the Task interface (after qaGate):
export interface Task {
// ... existing fields ...
/**
* Optional traceability metadata connecting this task to PRD requirements,
* risks, decisions, verification evidence, and the work-item hierarchy.
* Absent or null is equivalent to no traceability metadata.
*/
traceability?: TaskTraceability | null;
}
Add to UpdateTaskInput:
export interface UpdateTaskInput {
// ... existing fields ...
traceability?: TaskTraceability | null;
}
Add traceability to TaskSummary when ?include=traceability is requested (see API section).
SQLite Schema
New table added in migration 0050_task_traceability.up.sql (number to be assigned per
docs/SQLITE-SCHEMA.md migration numbering rules):
CREATE TABLE task_traceability (
task_id TEXT PRIMARY KEY
REFERENCES tasks(id) ON DELETE CASCADE,
parent_id TEXT
REFERENCES tasks(id) ON DELETE SET NULL,
work_item_level TEXT CHECK (
work_item_level IN (
'initiative','epic','story','task','child-task'
)
),
requirement_ids_json TEXT, -- JSON array of strings, e.g. ["REQ-001","REQ-002"]
risk_ids_json TEXT, -- JSON array of strings
decision_ids_json TEXT, -- JSON array of strings
verification_ids_json TEXT, -- JSON array of strings
human_gates_json TEXT, -- JSON array of strings
stop_conditions_json TEXT, -- JSON array of strings
stop_condition_resolved_json TEXT, -- JSON object: { "condition string": true/false }
risk_disposition_json TEXT, -- JSON object: { "RISK-01": "mitigated", ... }
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
created_by TEXT,
updated_by TEXT
);
CREATE INDEX idx_task_traceability_parent
ON task_traceability(parent_id)
WHERE parent_id IS NOT NULL;
CREATE INDEX idx_task_traceability_level
ON task_traceability(work_item_level)
WHERE work_item_level IS NOT NULL;
Down migration (0050_task_traceability.down.sql):
DROP INDEX IF EXISTS idx_task_traceability_level;
DROP INDEX IF EXISTS idx_task_traceability_parent;
DROP TABLE IF EXISTS task_traceability;
Design trade-off — JSON columns vs. normalized junction tables: The requirement/risk/
decision/verification ID arrays are stored as JSON blobs rather than junction tables (unlike
task_dependencies, task_subtasks, task_verification_steps). This is intentional for
the initial phase: these are reference-only identifiers, not VK entity IDs requiring FK
integrity. Coverage queries use json_each() to iterate, which is adequate for typical
cardinalities. If performance or integrity requirements change (e.g., a native risk register
entity lands), a follow-up migration can normalize these into task_requirements,
task_risks, etc. junction tables.
Archive/delete semantics for hierarchy: VK archives tasks rather than physically deleting
them. The ON DELETE SET NULL on parent_id applies only to hard deletes (rare purge path).
For archived parents, the implementation must define explicit behavior:
- Archiving a task that has active (non-archived) children issues a warning in the API
response (
archiveWarning: "N active children retain this task as parent"). The archive proceeds regardless. Children'sparentIdis preserved — callers that want to reparent or cascade the archive must do so explicitly in separate requests. (Enforced at the service layer; not a DB FK constraint.) /api/tasks/:id/childrenexcludes archived children by default; pass?include_archived=trueto include them.GET /api/hierarchyexcludes archived nodes by default; archived ancestors are rendered as[archived]stub nodes when a live descendant is shown.
Cross-scope parent links: When project or sprint is set on a task, its parentId
is validated to reference a task in the same project (same project field). Cross-project
parent links are rejected with 400 Bad Request. Tasks without a project may parent tasks
in any project, but hierarchy queries scoped to a project treat such parents as external roots.
File-Backed Storage Compatibility
For file-backed (v4) projects, traceability is persisted as an optional top-level field in
the per-task JSON file, following the same pattern as qaGate, checkpoint, and observations.
No schema change is required for file-backed projects. The storage abstraction layer handles
serialization transparently.
The file-to-SQLite migration (FileToSqliteMigrationService) maps the traceability field
to the task_traceability table. Absent or null traceability in the source file produces no
row in task_traceability.
API Design
Updated Endpoints
GET /api/tasks and GET /api/tasks/:id
No breaking changes. Traceability is not included in default responses to preserve payload size for boards with many tasks.
Include traceability in responses:
GET /api/tasks?include=traceability
GET /api/tasks/:id?include=traceability
The include parameter accepts a comma-separated list (e.g. include=traceability,deliverables)
for future extensibility.
PATCH /api/tasks/:id
Accepts traceability partial update in the existing request body:
{
"traceability": {
"parentId": "TASK-001",
"workItemLevel": "story",
"requirementIds": ["REQ-005", "REQ-006"],
"riskIds": ["RISK-AUTH-01"],
"riskDisposition": { "RISK-AUTH-01": "mitigated" },
"humanGates": [],
"verificationIds": ["TEST-22"]
}
}
Send "traceability": null to clear all traceability metadata.
Partial updates are merged at the top traceability object level. Individual array fields
are replaced, not appended, consistent with how subtasks and verificationSteps are updated.
Validation rules:
parentId, if provided, must reference an existing task in the same workspace. Passnullto clear; omit to leave unchanged.- Parent must be in the same
projectas the child task (when both have a project). Cross-project parent links are rejected with400 Bad Request. - Circular parent references (A → B → A) are rejected with
400 Bad Request. workItemLevelmust be one of the five permitted values ornullto clear.riskDispositionkeys should appear inriskIds(soft validation warning in response, not rejection).humanGatesandstopConditionsitems are free-form strings, max 255 chars each.- Arrays are bounded at 100 items each.
stopConditionResolvedkeys must match strings instopConditions(soft warning, not rejection).
GET /api/tasks — next-safe selection
New optional query parameters:
GET /api/tasks?next_safe=true
GET /api/tasks?next_safe=true&project=rubicon
GET /api/tasks?next_safe=true&allow_unknown_risks=true
next_safe=true forces status=todo regardless of other status filters. Combining
next_safe=true with an explicit status other than todo returns 400 Bad Request.
A task qualifies as "next safe" when all of the following hold:
statusis exactlytodo.- All tasks in the union of
dependencies.depends_onandblockedByhavestatus = 'done'. This covers both the modern dependency graph and legacyblockedBysemantics. blockedReasonis absent or null.traceability.humanGatesis absent or empty.traceability.stopConditionsis absent or empty, OR every stop condition instopConditionshas a correspondingtrueentry instopConditionResolved.- None of the
traceability.riskIdshas dispositionblockedorgatedintraceability.riskDisposition. Bothblockedandgatedexplicitly signal "this task may not proceed" and are always excluded with no override available. - None of the
traceability.riskIdshas dispositionunknown(either absent fromriskDispositionor explicitly set tounknown), unlessallow_unknown_risks=trueis passed.
Results are ordered by: priority descending (critical first), then position ascending,
then created ascending.
Response includes traceability fields when next_safe=true is set (equivalent to
include=traceability).
New Endpoints
GET /api/tasks/:id/traceability
Dedicated read endpoint for traceability metadata. Returns the TaskTraceability object or
{} if none is set.
GET /api/tasks/TASK-042/traceability
Response:
{
"taskId": "TASK-042",
"traceability": {
"parentId": "TASK-001",
"workItemLevel": "story",
"requirementIds": ["REQ-005", "REQ-006"],
"riskIds": ["RISK-AUTH-01"],
"riskDisposition": { "RISK-AUTH-01": "mitigated" },
"humanGates": [],
"stopConditions": ["regression suite must be green"],
"stopConditionResolved": { "regression suite must be green": true },
"verificationIds": ["TEST-22"]
}
}
GET /api/tasks/:id/children
Returns direct children of a task in the hierarchy (parentId = :id). Excludes archived
children by default; pass ?include_archived=true to include them. Supports standard
pagination and ?include=traceability.
GET /api/tasks/TASK-001/children?page=1&limit=25
GET /api/coverage/requirements
Requirement coverage report for a project or sprint.
GET /api/coverage/requirements?project=rubicon
GET /api/coverage/requirements?sprint=sprint-3
Coverage semantics and the requirement catalog:
total in the summary is the count of distinct requirementIds seen across all tasks in
scope plus any IDs registered in the project's requirement catalog (see
POST /api/projects/:id/catalog/requirements below). Without a catalog, total equals the
number of distinct IDs observed in tasks, so "uncovered" means no task in scope references
that ID. With a catalog, requirement IDs registered but not referenced by any task appear as
uncovered rows (tasks: []).
Verification semantics: A requirement is considered verified when at least one task
that references it has:
status = 'done', and- at least one
verificationStepsentry withchecked = true, or - at least one entry in
verificationIds.
Mere presence of verificationSteps or verificationIds without done status or checked
steps does not count as verified.
Response:
{
"project": "rubicon",
"catalogSource": "registered",
"requirements": [
{
"id": "REQ-001",
"tasks": ["TASK-010", "TASK-011"],
"verificationIds": ["TEST-01", "TEST-02"],
"covered": true,
"verified": true
},
{
"id": "REQ-007",
"tasks": [],
"verificationIds": [],
"covered": false,
"verified": false
}
],
"summary": {
"total": 12,
"covered": 9,
"verified": 7,
"uncovered": 3,
"coveragePercent": 75,
"verificationPercent": 58
}
}
catalogSource: "registered" when a catalog exists; "derived" when computed from task
IDs only (no uncovered rows possible in derived mode).
POST /api/projects/:id/catalog/requirements
Register a set of requirement IDs for a project. Enables GET /api/coverage/requirements to
report truly uncovered requirements (those in the catalog but referenced by no tasks).
{
"requirementIds": ["REQ-001", "REQ-002", "REQ-007", "REQ-010", "REQ-011"],
"replace": true
}
replace: true (default) clears the existing catalog before saving. replace: false merges.
POST /api/projects/:id/catalog/risks
Same semantics as the requirements catalog, for risk IDs.
{
"riskIds": ["RISK-AUTH-01", "RISK-PERF-02", "RISK-SEC-05"],
"replace": true
}
GET /api/coverage/risks
Risk coverage report.
GET /api/coverage/risks?project=rubicon
Verification semantics: A risk is verified when at least one task that tracks it has
status = 'done' and at least one verificationSteps entry with checked = true or
at least one entry in verificationIds. Risks with disposition: 'accepted' are excluded
from the verified check (accepted risks do not require evidence).
Response:
{
"project": "rubicon",
"catalogSource": "registered",
"risks": [
{
"id": "RISK-AUTH-01",
"tasks": ["TASK-010"],
"disposition": "mitigated",
"verified": true
},
{
"id": "RISK-PERF-02",
"tasks": ["TASK-015"],
"disposition": "gated",
"verified": false
},
{
"id": "RISK-SEC-05",
"tasks": [],
"disposition": "unknown",
"verified": false
}
],
"summary": {
"total": 8,
"mitigated": 3,
"gated": 2,
"accepted": 1,
"blocked": 0,
"deferred": 1,
"unknown": 1,
"openRisks": 4
}
}
openRisks = count of risks with disposition gated, blocked, deferred, or unknown.
Risks in the catalog but not referenced by any task are included with disposition: 'unknown'
and tasks: [] when a catalog is registered (catalogSource: "registered").
GET /api/hierarchy
Returns the task tree for a project, optionally rooted at a specific task. Archived tasks
are excluded by default; pass ?include_archived=true to include them (rendered as stub nodes
with archived: true).
GET /api/hierarchy?project=rubicon
GET /api/hierarchy?root=TASK-001&depth=3
Response:
{
"nodes": [
{
"id": "TASK-001",
"title": "Authentication epic",
"status": "in-progress",
"workItemLevel": "epic",
"children": [
{
"id": "TASK-010",
"title": "OAuth2 login flow",
"status": "done",
"workItemLevel": "story",
"children": []
},
{
"id": "TASK-011",
"title": "Token refresh handling",
"status": "todo",
"workItemLevel": "story",
"children": []
}
]
}
]
}
Depth defaults to 5. Tasks without traceability.parentId appear as roots when no root
parameter is given. Tasks without workItemLevel are included with workItemLevel: null.
CLI Design
All changes are additive to existing vk update and vk list commands.
vk update — traceability flags
# Work-item hierarchy
vk update <id> --parent <taskId>
vk update <id> --level initiative|epic|story|task|child-task
vk update <id> --clear-parent # remove parentId (sets parentId: null)
# Requirement and risk traceability
vk update <id> --requirements REQ-001,REQ-002
vk update <id> --risks RISK-AUTH-01,RISK-PERF-02
vk update <id> --decisions ADR-0001
vk update <id> --verification-ids TEST-22,TEST-23
# Risk disposition
vk update <id> --risk-disposition RISK-AUTH-01=mitigated,RISK-PERF-02=gated
# Gates and stop conditions
vk update <id> --human-gate "Coby approval before production write"
vk update <id> --stop-condition "regression suite must be green"
vk update <id> --resolve-stop-condition "regression suite must be green" # marks resolved=true
vk update <id> --clear-human-gates
vk update <id> --clear-stop-conditions
# Show current traceability
vk show <id> --traceability
Array flags (--requirements, --risks, etc.) replace the existing array. Prepend + to
append: --requirements +REQ-003. Prepend - to remove: --requirements -REQ-001.
vk list --next-safe
vk list --next-safe
vk list --next-safe --project rubicon
vk list --next-safe --allow-unknown-risks # include tasks where risk disposition is unknown
vk list --next-safe --json
Output (default):
┌ Next safe tasks (3 found) ──────────────────────────────────┐
│ TASK-011 [story] Token refresh handling [todo] │
│ TASK-018 [task] Write migration tests [todo] │
│ TASK-021 [task] Update API docs [todo] │
└──────────────────────────────────────────────────────────────┘
Blocked reasons are shown inline when --verbose is used.
vk coverage commands
vk coverage requirements [--project <id>] [--sprint <id>] [--json]
vk coverage risks [--project <id>] [--sprint <id>] [--json]
Default (non-JSON) output:
Requirements coverage — project: rubicon
Covered: 9/12 (75%)
Verified: 7/12 (58%)
Uncovered: REQ-007, REQ-010, REQ-011
Risk coverage — project: rubicon
Mitigated: 3 Gated: 2
Accepted: 1 Blocked: 0
Deferred: 1 Unknown: 1
Open risks: RISK-PERF-02 (gated), RISK-SEC-05 (unknown)
vk hierarchy command
vk hierarchy [--project <id>] [--root <taskId>] [--depth <n>] [--json]
Default output renders an indented tree. --json returns the same structure as GET /api/hierarchy.
PRD / Backlog Import Helper (Phase 4)
A future vk import prd command will accept a structured JSON payload and create a hierarchy
of tasks with traceability populated.
Input schema (v1):
{
"project": "rubicon",
"sprint": "sprint-4",
"items": [
{
"externalId": "REQ-001",
"title": "User can log in via OAuth2",
"level": "story",
"parentExternalId": null,
"requirementIds": ["REQ-001"],
"riskIds": ["RISK-AUTH-01"],
"acceptanceCriteria": ["Login succeeds with Google", "Login fails gracefully on error"],
"verificationIds": ["TEST-01", "TEST-02"]
}
]
}
The importer creates tasks in dependency order, sets traceability fields from the payload,
and returns a summary of created task IDs keyed by externalId.
This phase is out of scope for the initial implementation. The endpoint placeholder is
POST /api/import/prd.
Migration and Backward Compatibility
Task API
GET /api/tasksresponses do not includetraceabilityby default. No payload size regression for existing clients.PATCH /api/tasks/:idignores unknown top-level fields under theUpdateTaskInputZod schema, so old clients sending requests withouttraceabilityare unaffected.- All new query parameters (
next_safe,include=traceability) are additive and ignored by older server versions.
SQLite Migration
Migration 0050_task_traceability is forward-only-safe:
- Adding a new table does not affect existing queries.
- Existing tasks have no row in
task_traceability, which is equivalent totraceability: null. - The down migration drops the table cleanly, restoring pre-0050 schema state.
The migration sequence must be applied atomically: up migration runs in a SQLite transaction;
rollback drops the table and removes the schema_migrations row.
File-Backed (v4) Projects
traceabilityis an optional top-level field in per-task JSON. Absent means no traceability.- Reading old task files without the field returns
undefinedfortraceability, which the API serializes as absent (notnull) in responses. - Writing traceability to a v4-backed task adds the field to the JSON file. No file structure change; no migration required.
CLI
All new flags are additive. Existing vk update, vk list, and vk show behavior is unchanged.
The vk coverage and vk hierarchy commands are new; they do not conflict with existing commands.
Acceptance Criteria
| # | Criterion |
|---|---|
| 1 | Existing boards continue to work with no changes. Zero regressions in task CRUD, board rendering, and agent task-pick behavior. |
| 2 | PATCH /api/tasks/:id with a valid traceability payload persists all fields and returns them in subsequent GET /api/tasks/:id?include=traceability responses. |
| 3 | PATCH /api/tasks/:id with "traceability": { "parentId": null } clears parentId only; other traceability fields are preserved. |
| 4 | PATCH /api/tasks/:id with "traceability": null clears all traceability metadata. |
| 5 | GET /api/tasks?next_safe=true returns only todo tasks where: (a) all depends_on and blockedBy tasks are done, (b) blockedReason is absent, (c) humanGates is empty, (d) stopConditions are all resolved, and (e) no risk has disposition blocked, gated, or unknown. |
| 6 | GET /api/tasks?next_safe=true with an explicit status other than todo returns 400 Bad Request. |
| 7 | GET /api/tasks?next_safe=true&allow_unknown_risks=true includes tasks whose risks have unknown disposition but still excludes tasks whose risks have blocked or gated disposition. |
| 8 | GET /api/coverage/requirements?project=<id> in "derived" mode (no catalog) reports only IDs observed in tasks; the uncovered count is 0. In "registered" mode (catalog present), IDs in the catalog but absent from tasks appear as uncovered rows. |
| 9 | A requirement is reported verified: true only when at least one done task referencing it has at least one verificationSteps entry with checked = true, or at least one verificationIds entry. |
| 10 | GET /api/coverage/risks?project=<id> returns a risk register with per-disposition counts and correctly identifies open risks; risks with disposition: 'accepted' are excluded from the verified check. |
| 11 | GET /api/hierarchy?project=<id> returns a correct tree of tasks linked via traceability.parentId, up to 5 levels deep; archived tasks are excluded by default. |
| 12 | PATCH /api/tasks/:id with a cross-project parentId (parent in a different project) returns 400 Bad Request. |
| 13 | PATCH /api/tasks/:id with a circular parentId chain returns 400 Bad Request. |
| 14 | Archiving a task with active (non-archived) children without reparenting them returns a warning (not a hard block) and the child's parentId is preserved until explicitly cleared. |
| 15 | vk update <id> --parent <taskId> sets traceability.parentId. vk update <id> --clear-parent sets it to null. vk show <id> --traceability prints the current traceability fields. |
| 16 | vk list --next-safe returns the same set as API criterion 5. |
| 17 | vk coverage requirements and vk coverage risks produce human-readable output matching the API response summaries. |
| 18 | File-backed (v4) projects can set and retrieve traceability fields with no migration step. |
| 19 | SQLite migration 0050 applies and rolls back cleanly in a test fixture with no foreign-key violations. |
Rollout Sequence
Phase 1 — Schema and Core API (required for everything downstream)
- Add
TaskTraceability,WorkItemLevel,RiskDispositiontypes toshared/src/types/task.types.ts. Use'child-task'for the hierarchy leaf level. - Add
traceabilityfield toTaskandUpdateTaskInput. IncludestopConditionResolved. - Add
task_traceabilitytable migration (0050) withstop_condition_resolved_jsoncolumn andwork_item_levelCHECK for'child-task'instead of'subtask'. - Implement
TaskTraceabilityRepository(SQLite and file-backed). - Update
TaskRepository.update()andTaskRepository.getById()to persist and hydrate traceability wheninclude=traceabilityis requested. - Add
traceabilitytoUpdateTaskInputZod schema with validation:- Circular parent check (traverse
parentIdchain up to depth 20 before failing). - Cross-project parent check (reject
parentIdin a different project). workItemLevelenum check.parentId: nullclears the field cleanly.- Array bounds (100 items each).
- Circular parent check (traverse
- Update
GET /api/tasks/:id,PATCH /api/tasks/:id, andGET /api/taskswith theinclude=traceabilityparameter. - Add
GET /api/tasks/:id/traceabilitydedicated endpoint. - Add
GET /api/tasks/:id/childrenendpoint (with?include_archived=true). - Add
next_safe=truefilter toGET /api/tasks. The filter must evaluate:depends_on ∪ blockedByall done.blockedReasonabsent.humanGatesempty.- All
stopConditionsresolved (or no stop conditions). - No risk with disposition
blockedorgated; no risk with dispositionunknownunlessallow_unknown_risks=trueis set. (blockedandgatedare always excluded — no override.) - Returns
400when combined with a non-todostatus filter.
- Unit tests for storage, validation (circular/cross-project), and next-safe logic.
Phase 2 — Coverage and Hierarchy APIs
- Implement
POST /api/projects/:id/catalog/requirementsand/catalog/risks. - Implement
GET /api/coverage/requirementswithprojectandsprintscopes. Verified =donetask + at least oneverificationSteps[].checked=trueorverificationIdsentry.catalogSource:"registered"or"derived". - Implement
GET /api/coverage/riskswithprojectandsprintscopes. Accepted risks excluded from verified check. - Implement
GET /api/hierarchywithproject,root,depth, andinclude_archivedparameters. Archived ancestors shown as{ id, title, archived: true, children: [] }stubs when a live descendant is included. - Integration tests for coverage (derived/registered modes), verification semantics, and hierarchy depth/archive behavior.
Phase 3 — CLI
- Add
--parent,--clear-parent,--level,--requirements,--risks,--decisions,--verification-ids,--risk-disposition,--human-gate,--stop-condition,--resolve-stop-condition,--clear-human-gates, and--clear-stop-conditionstovk update. - Add
--traceabilityflag tovk show. - Add
vk list --next-safe(passesallow_unknown_riskswhen--allow-unknown-risksset). - Add
vk coverage requirementsandvk coverage risks. - Add
vk hierarchy. - CLI integration tests using the test server fixture.
Phase 4 — PRD Import Helper (follow-up issue)
- Define
POST /api/import/prdendpoint with versioned input schema ("version": "1"). - Implement dependency-ordered batch creation with traceability population.
- Add
vk import prdcommand.
Phase 5 — UI (follow-up issue)
- Hierarchy tree view in task detail sidebar.
- Coverage dashboard widget in project view.
- Traceability fields in task edit form (collapsed by default).
- "Safe to start" filter in board UI (equivalent to
next_safe=true).
Implementation Backlog
The following issues should be filed when Phase 1 begins. Each issue closes a distinct increment to keep PRs reviewable.
| # | Title | Phase | Notes |
|---|---|---|---|
| B-1 | Add TaskTraceability types to shared/ and UpdateTaskInput Zod schema |
1 | Purely additive TypeScript and Zod. No runtime change. Includes stopConditionResolved. |
| B-2 | Add task_traceability SQLite migration and repository |
1 | Migration 0050. work_item_level CHECK uses 'child-task'. Includes file-backed path. |
| B-3 | Integrate traceability into GET /api/tasks/:id and PATCH /api/tasks/:id |
1 | ?include=traceability, null-clear, parentId: null clear, circular+cross-project validation. |
| B-4 | Add GET /api/tasks/:id/traceability and GET /api/tasks/:id/children |
1 | Children endpoint excludes archived by default. |
| B-5 | Add next_safe=true filter to GET /api/tasks |
1 | Evaluates depends_on ∪ blockedBy, gates, resolved stop conditions, risk disposition. Excludes blocked and gated (no override); excludes unknown unless allow_unknown_risks=true. Returns 400 on bad status combo. |
| B-6 | Add project requirement and risk catalogs | 2 | POST /api/projects/:id/catalog/requirements and /risks. Enables uncovered-row semantics. |
| B-7 | Implement GET /api/coverage/requirements |
2 | Derived and registered catalog modes. Verified = done + checked steps. |
| B-8 | Implement GET /api/coverage/risks |
2 | Derived and registered catalog modes. Accepted risks exempt from verified check. |
| B-9 | Implement GET /api/hierarchy |
2 | Tree builder with configurable depth. Archive stub semantics. |
| B-10 | CLI: vk update traceability flags |
3 | Includes --resolve-stop-condition. Add/replace/remove array syntax (+, - prefix). |
| B-11 | CLI: vk list --next-safe and vk show --traceability |
3 | Consumes Phase 1 API. Passes allow_unknown_risks to server. |
| B-12 | CLI: vk coverage and vk hierarchy commands |
3 | Consumes Phase 2 API. |
| B-13 | PRD import helper (POST /api/import/prd, vk import prd) |
4 | Separate issue; versioned input schema, dependency-ordered batch create. |
| B-14 | UI: Hierarchy tree, coverage widget, traceability edit form | 5 | Separate issue. Design exploration needed. |
Open Questions
-
Soft enforcement of levels — Should VK optionally warn (not block) when a
story-level task has noparentIdpointing to anepic? Flagging inTaskReadinessCheckseems appropriate rather than a hard validation. Decision deferred to Phase 1 implementation. -
Risk register entity — Risk IDs are currently free-form strings. A future risk register entity (outside scope here) would allow richer metadata per risk. This design is forward compatible:
riskIdscan point to VK risk entity IDs when that entity exists, and to external risk-tracker IDs until it does. The catalog endpoints (B-6) are an incremental step. -
Human-gate clearance UX — This design treats
humanGatesas a simple array of strings. A future enhancement could make each gate a structured record with a cleared timestamp and cleared-by actor. Out of scope for Phase 1. -
Import format versioning — The PRD import payload (Phase 4) must include a
"version": "1"field from the start. Deferred to the Phase 4 issue (B-13).