A spec was a promise; sixteen of them had become descriptions of code that already shipped, sitting beside four that describe work still outstanding, with nothing in the file telling the two apart. Half the statuses were also wrong — audio-equalizer read "Accepted" with the EQ live on both platforms, the native video spec said the flag stays off after the default was flipped on. The shipped designs move into docs/architecture, which is the maintained description of the build, and the spec files go. Git history keeps the originals; what a future change still needs is carried across: - 01-rust-backend: favourites rewritten (the old section named a file that no longer exists and called shipped buttons "planned"), domain vocabulary owned by Rust (SearchScope, exclusions, the bitrate ladder), background workers - 02-svelte-frontend: app shell and chrome, library mosaic, series/episode navigation, downloaded browse, safe-area insets, native-video store, logging - 03-data-flow: locally-indexed search - 05-platform-backends: audio settings on ExoPlayer, the equalizer's band vocabulary, native video compositing, the background-audio handoff - 06-downloads-and-offline: one storage model, offline catalog visibility - 09-security: path confinement and input binding docs/specs/README.md now says what the directory is for and where each shipped design went. Deferred work the specs recorded is kept beside the code it concerns rather than lost: season-bounded autoplay, the two dead search commands, why indexing is a full crawl. requirements.md had fourteen stale statuses — Android audio parity still read "Linux only", DR-150 still said the native-video default was off, DR-190 was Proposed after DR-196 implemented it, and five tooling requirements were Proposed after landing. Three unbuilt specs suggested requirement ids that have since been allocated to other work; each now carries a warning.
10 KiB
Requirement Traceability CI/CD Pipeline
This document explains the automated requirement traceability validation system for JellyTau.
Overview
The CI/CD pipeline automatically validates that code changes are properly traced to requirements. This ensures:
- ✅ Requirements are implemented with clear traceability
- ✅ No requirement coverage regressions
- ✅ Code changes are linked to specific requirements
- ✅ Quality metrics are tracked over time
Gitea Actions Workflows
Traceability validation lives in .gitea/workflows/traceability-check.yml:
- ✅ Automatic trace extraction
- ✅ Coverage validation against minimum threshold (88%, ratcheted)
- ✅ Modified file checking
- ✅ Artifact preservation
- ✅ Summary reports
Runs on: Every push and pull request to master/main/develop
A second workflow, traceability.yml, previously duplicated this one as a
"GitHub-compatible alternative". It was removed: CI here is Gitea Actions, and
its only unique step (PR comments via actions/github-script) depended on the
GitHub REST client, which Gitea does not provide. To add PR comments, post to
Gitea's /api/v1/repos/{owner}/{repo}/issues/{index}/comments from
traceability-check.yml rather than reviving the old file.
What Gets Validated
1. Trace Extraction
bun run traces:json > traces-report.json
Extracts all TRACES comments from:
- TypeScript files (
src/**/*.ts) - Svelte components (
src/**/*.svelte) - Rust code (
src-tauri/src/**/*.rs) - Test files
2. Coverage Thresholds
The workflow checks:
- Minimum overall coverage: 88% (
MIN_THRESHOLD)
Denominators are derived from docs/requirements.md at run time — they are
never hardcoded here or in the workflow. Run bun run traces:coverage for the
current per-type breakdown; any number written into this document is a snapshot
that will drift.
Why this matters. The workflow used to divide by frozen literals (UR/39, IR/24, DR/48, JA/3, total 114) while
requirements.mdhad grown past 200. It reported 158% coverage, so the 50% threshold was unreachable and the job could not fail regardless of how far coverage dropped. See The fix derives the denominators fromrequirements.mdat run time.
Coverage is the intersection of traced and defined IDs: an ID that appears in
a TRACES: comment but is not defined in requirements.md is reported as
orphaned and does not count toward coverage. UT/IT test identifiers are a
separate taxonomy and are excluded entirely.
The workflow fails and blocks merge if coverage drops below the threshold — or if it computes above 100%, which can only mean the gate is miscounting.
Ratchet policy
MIN_THRESHOLD only ever goes up. It is deliberately set a few points below
the coverage actually achieved (88 against a real ~90%), so a genuine regression
trips it. It previously sat at 50 while true coverage was 86%: nearly half the
matrix could have rotted before CI objected. It was ratcheted 50 → 82 when that
was found, and 82 → 88 once coverage had held above 88% for several releases.
When coverage rises durably, raise the threshold to just under the new figure.
Never lower it to make a red build pass — add the missing TRACES comments
instead. The same number lives in MIN_COVERAGE_PERCENT in
scripts/extract-traces.ts (so bun run traces:coverage gates locally on the
same bar); scripts/extract-traces.test.ts fails if the two drift apart.
2b. Dangling requirement IDs
bun run traces:validate
Every ID named by a TRACES: comment must be defined as a table row in
docs/requirements.md. The extractor used to accept any well-formed ID
silently, so a typo or a rename that missed a call site passed unnoticed —
DR-189 and UT-188 were referenced from three source files, defined nowhere,
for months.
This check spans all six ID types (UR/IR/DR/JA/UT/IT), unlike the coverage
orphaned list above, which considers only the four requirement types so that
UT/IT noise cannot bury a real typo in the ratio's reporting. The workflow step
fails the build on any dangling ID and prints each offender with the files
that reference it.
3. Modified File Checking
On pull requests, the workflow:
- Detects all changed TypeScript/Svelte/Rust files
- Warns if new/modified files lack TRACES comments
- Suggests the TRACES format for missing comments
How to Add Traces to New Code
When you add new code or modify existing code, include TRACES comments:
TypeScript/Svelte Example
// TRACES: UR-005, UR-026 | DR-029
export function handlePlayback() {
// Implementation...
}
Rust Example
/// TRACES: UR-005 | DR-001
pub fn player_state_changed(state: PlayerState) {
// Implementation...
}
Test Example
// TRACES: UR-005 | DR-001 | UT-026, UT-027
#[cfg(test)]
mod tests {
// Tests...
}
TRACES Format
TRACES: [UR-###, ...] | [IR-###, ...] | [DR-###, ...] | [JA-###, ...]
UR-###- User Requirements (features users see)IR-###- Integration Requirements (API/platform integration)DR-###- Development Requirements (internal architecture)JA-###- Jellyfin API Requirements (Jellyfin API usage)
Examples:
// TRACES: UR-005- Single requirement// TRACES: UR-005, UR-026- Multiple of same type// TRACES: UR-005 | DR-029- Multiple types// TRACES: UR-005, UR-026 | DR-001, DR-029 | UT-001- Complex
Workflow Behavior
On Push to Main Branch
- ✅ Extracts all traces from code
- ✅ Validates coverage is >= 88%
- ✅ Generates full traceability report
- ✅ Saves report as artifact
On Pull Request
- ✅ Extracts all traces
- ✅ Validates coverage >= 88%
- ✅ Checks modified files for TRACES
- ✅ Warns if new code lacks TRACES
- ✅ Suggests proper format
- ✅ Generates report artifact
Failure Scenarios
The workflow fails (blocks merge) if:
- Coverage drops below 88%
- A
TRACES:comment names an IDdocs/requirements.mddoes not define - JSON extraction fails
- Invalid trace format
The workflow warns (but doesn't block) if:
- New files lack TRACES comments
- Coverage drops (but still above threshold)
Viewing Reports
In Gitea Actions UI
- Go to Actions tab
- Click the Traceability Validation workflow run
- Download traceability-reports artifact
- View:
traces-report.json- Raw trace datadocs/traceability.md- Formatted report
Locally
# Extract current traces
bun run traces:json | jq '.byType'
# Generate full report
bun run traces:markdown
cat docs/traceability.md
Coverage Goals
Current Status
Run bun run traces:coverage — it prints the live figure and exits non-zero
below threshold. Numbers are deliberately not pinned here; the previous snapshot
in this section (51%, 56/114) was stale by roughly 100 requirements and was what
made the broken CI arithmetic look plausible for so long.
As of August 2026 overall coverage is ~90%.
Targets
- Short term (Sprint): Maintain ≥88% overall (the current ratchet)
- Medium term (Month): Hold above 90% and ratchet the gate to match
- Long term (Release): Reach 95% coverage with focus on:
- IR requirements (API clients)
- JA requirements (Jellyfin API endpoints)
- Remaining UR/DR requirements
Improving Coverage
For Missing User Requirements (UR)
- Review README.md for unimplemented features
- Add TRACES to code that implements them
- Focus on high-priority features (High/Medium priority)
For Missing Integration Requirements (IR)
- Add TRACES to Jellyfin API client methods
- Add TRACES to platform-specific backends (Android/Linux)
- Link to corresponding Jellyfin API endpoints
For Missing Development Requirements (DR)
- Add TRACES to UI components in
src/lib/components/ - Add TRACES to composables in
src/lib/composables/ - Add TRACES to player backend in
src-tauri/src/player/
For Jellyfin API Requirements (JA)
- Add TRACES to Jellyfin API wrapper methods
- Document which endpoints map to which requirements
- Link to Jellyfin API documentation
Example PR Checklist
When submitting a pull request:
- All new code has TRACES comments linking to requirements
- TRACES format is correct:
// TRACES: UR-001 | DR-002 - Workflow passes (coverage ≥ 88%)
- No coverage regressions
- Artifact traceability report was generated
Troubleshooting
"Coverage below minimum threshold"
Problem: Workflow fails with coverage < 88%
Solution:
- Run
bun run traces:jsonlocally - Check which requirements are traced
- Add TRACES to untraced code sections
- Re-run extraction to verify
"New files without TRACES"
Problem: Workflow warns about new files lacking TRACES
Solution:
- Add TRACES comments to all new code
- Format:
// TRACES: UR-001 | DR-002 - Map code to specific requirements from README.md
- Re-push
"Invalid JSON format"
Problem: Trace extraction produces invalid JSON
Solution:
- Check for malformed TRACES comments
- Run locally:
bun run traces:json - Look for parsing errors
- Fix and retry
Integration with Development
Before Committing
# Check your traces
bun run traces:json | jq '.byType'
# Regenerate report
bun run traces:markdown
# Verify traces syntax
grep "TRACES:" src/**/*.ts src/**/*.rs
In Your IDE
Add a file watcher to regenerate traces on save:
{
"fileWatcher.watchPatterns": [
"src/**/*.ts",
"src/**/*.svelte",
"src-tauri/src/**/*.rs"
],
"fileWatcher.command": "bun run traces:markdown"
}
Git Hooks
Add a pre-push hook to validate traces:
#!/bin/bash
# .git/hooks/pre-push
bun run traces:json > /dev/null
if [ $? -ne 0 ]; then
echo "❌ Invalid TRACES format"
exit 1
fi
References
Support
For issues or questions:
- Check this document
- Review example traces in
src/lib/stores/ - Check existing TRACES comments for format
- Review workflow logs in Gitea Actions
Last Updated: 2026-02-13