tx spec
Spec-to-test traceability with multi-language discovery and FCI phase scoring
Purpose
tx spec bridges docs and tests:
- Refresh doc-derived invariants from tx docs
- Discover test mappings across languages from source annotations and
.tx/spec-tests.yml - Record test outcomes
- Compute Feature Completion Index (FCI) and phase (
BUILD -> HARDEN -> COMPLETE)
This is primitive infrastructure, not a test runner. You keep your existing framework and CI.
The normal workflow is docs-first: edit docs, run tx spec discover, ingest test outcomes, inspect fci or status, then record COMPLETE.
Prerequisites
To get useful output from tx spec, you need three things in place already:
tx doc: structured PRDs and design docs on disk so the spec has a canonical source- Existing tests with either source annotations or
.tx/spec-tests.yml: discovery maps tests from tags, comments, or explicit manifest entriestx spec discoverrefreshes doc-derived invariants automatically before scanning tests.
Common follow-on primitives:
tx verify: make spec or FCI checks part of task completiontx gate: require a human checkpoint before release movementtx trace: inspect failing runs, transcripts, and stderr when outcomes regress
At A Glance
Spec Pipeline
FCI Gate
Use this as a release gate: automation can move from BUILD to HARDEN, but only a human can mark COMPLETE.
Canonical Test ID
Every mapped test uses:
{relative_file}::{test_name}Example:
test/integration/core.test.ts::ready detection returns unblocked tasksMulti-Language Discovery
Discovery is regex-based on raw text files. No AST parsing.
tx spec discover always refreshes doc-derived invariants first:
tx spec discoverrefreshes all docs, then scans the repotx spec discover --doc <name>refreshes that doc scope, then scans for that scope
Supported annotation conventions:
- Tag in test names:
[INV-EARS-FL-001] - Underscore tags in function names:
_INV_EARS_FL_001 - Comment annotations:
@spec INV-EARS-FL-001 - Manifest mappings:
.tx/spec-tests.yml
Default scan patterns include common JS/TS, Python, Go, Rust, Java, Ruby, and C/C++ conventions. Override via .tx/config.toml [spec].test_patterns.
CLI
# Discovery + mapping
tx spec discover [--doc <name>] [--patterns <glob1,glob2,...>]
tx spec link <inv-id> <file> [name] [--framework <name>]
tx spec unlink <inv-id> <test-id>
tx spec tests <inv-id>
tx spec gaps [--doc <name>] [--sub <name>]
tx spec matrix [--doc <name>] [--sub <name>]
# Scoring + lifecycle
tx spec fci [--doc <name>] [--sub <name>]
tx spec status [--doc <name>] [--sub <name>]
tx spec complete [--doc <name> | --sub <name>] --by <human> [--notes <text>]
# Run ingestion
tx spec run <test-id> --passed|--failed [--duration <ms>] [--details <text>]
tx spec batch [--from generic|vitest|pytest|go|junit] # reads stdinNotes:
tx spec runrequires exactly one of--passedor--failed.tx spec completerequires--byand only succeeds fromHARDEN.
Interfaces
tx spec discover --doc PRD-033-spec-test-traceability
tx spec fci --doc PRD-033-spec-test-traceability
tx spec status --doc PRD-033-spec-test-traceability
vitest run --reporter=json | tx spec batch --from vitest
tx spec complete --doc PRD-033-spec-test-traceability --by jamesimport { TxClient } from '@jamesaphoenix/tx-agent-sdk'
const tx = new TxClient({ dbPath: '.tx/tasks.db' })
const junitXml = '<testsuites>...</testsuites>'
const discover = await tx.spec.discover({
doc: 'PRD-033-spec-test-traceability',
})
const status = await tx.spec.status({
doc: 'PRD-033-spec-test-traceability',
})
const matrix = await tx.spec.matrix({
doc: 'PRD-033-spec-test-traceability',
})
await tx.spec.batch({
from: 'junit',
raw: junitXml,
})
if (status.phase === 'HARDEN') {
await tx.spec.complete({
doc: 'PRD-033-spec-test-traceability',
signedOffBy: 'james',
})
}
console.log({
discovered: discover.discoveredLinks,
phase: status.phase,
blockers: status.blockers,
mappedInvariants: matrix.length,
})tx_spec_discover
tx_spec_link
tx_spec_unlink
tx_spec_tests
tx_spec_invariants_for_test
tx_spec_gaps
tx_spec_fci
tx_spec_status
tx_spec_matrix
tx_spec_record_run
tx_spec_batch_run
tx_spec_completePOST /api/spec/discover
GET /api/spec/tests/:invariantId
GET /api/spec/invariants?testId=<canonical-id>
GET /api/spec/gaps
GET /api/spec/fci
GET /api/spec/status
GET /api/spec/matrix
POST /api/spec/link
POST /api/spec/unlink
POST /api/spec/run
POST /api/spec/batch
POST /api/spec/completeFCI and Phases
FCI = passing_invariants / total_active_invariants * 100
Use the primitives like this:
-
tx spec fci: compact machine score for agents and automation -
tx spec status: human-readable closure state with blocker reasons -
BUILD: uncovered, untested, or failing invariants still exist -
HARDEN: FCI = 100 and a human can now sign off -
COMPLETE: human sign-off (tx spec complete)
Use this as a readiness primitive for moving from implementation to hardening and release review.