Part V: Language Backends — One Protocol, Seven Scanners
One JSON protocol. Seven language backends. The Diem instance doesn't care which language produced the scan.
Most specification tools treat every language the same: parse comments, match strings, hope for the best. That works until someone renames a method, deletes a test file, or typos a feature identifier. Then the spec drifts silently and nobody notices until a customer reports the bug that was supposedly "verified."
tspec takes the opposite approach. Each language gets a backend that speaks that language's native type system. The C# backend uses Roslyn. The TypeScript backend uses decorators and keyof. The Rust backend uses proc macros. They all disagree on syntax. They all agree on output: a single JSON document that Diem consumes without knowing or caring which compiler produced it.
The Backend Architecture
Seven backends, seven scanners, seven ways to express the same idea: "this code implements that acceptance criterion."
tspec-cs--- C#: Roslyn source generators +[Verifies(typeof(F), nameof(F.AC))]. Full semantic model. The compiler catches every broken link before the binary exists.tspec-ts--- TypeScript: decorators (@Implements<F>('ac')) +keyof T. Can run as a regex scanner for speed or invoke the TS compiler API for full type resolution.tspec-java--- Java: annotations (@Implements(feature=F.class, ac="name")) + annotation processor. Violations surface at compile time, not in a log file three sprints later.tspec-groovy--- Groovy: AST transformations + annotations. Same annotation surface as Java but with Groovy's compile-time metaprogramming.tspec-py--- Python: decorators (@implements(Feature, 'ac')) +astmodule scanner. No runtime import required for scanning --- the AST module reads the source directly.tspec-go--- Go: interface-based features + struct tags +go/astpackage. Idiomatic Go: no magic, no reflection, just interfaces and tags.tspec-rs--- Rust: trait-based features + proc macros (#[implements(NavigationFeature, "toc_click")]) +syn/quote. The proc macro validates at compile time; if the feature or AC does not exist,cargo buildfails.
Each backend understands its own language deeply. None of them try to be generic. That is the entire point.
Three Languages, One Idea
The same test link expressed in TypeScript, C#, and Rust:
TypeScript:
@FeatureTest(NavigationFeature)
class NavigationTests {
@Implements<NavigationFeature>('tocClickLoadsPage')
async 'clicking TOC loads page'({ page }) { ... }
}@FeatureTest(NavigationFeature)
class NavigationTests {
@Implements<NavigationFeature>('tocClickLoadsPage')
async 'clicking TOC loads page'({ page }) { ... }
}C#:
[ForRequirement(typeof(NavigationFeature))]
public class NavigationTests
{
[Verifies(typeof(NavigationFeature), nameof(NavigationFeature.TocClickLoadsPage))]
public async Task TocClick_LoadsPage() { ... }
}[ForRequirement(typeof(NavigationFeature))]
public class NavigationTests
{
[Verifies(typeof(NavigationFeature), nameof(NavigationFeature.TocClickLoadsPage))]
public async Task TocClick_LoadsPage() { ... }
}Rust:
#[feature_test(NavigationFeature)]
mod navigation_tests {
#[implements(NavigationFeature, "toc_click_loads_page")]
#[test]
fn toc_click_loads_page() { ... }
}#[feature_test(NavigationFeature)]
mod navigation_tests {
#[implements(NavigationFeature, "toc_click_loads_page")]
#[test]
fn toc_click_loads_page() { ... }
}Different syntax. Different compilers. Same semantic: "this test verifies the toc_click_loads_page acceptance criterion on NavigationFeature."
The JSON Protocol
Every backend produces the same schema. No exceptions. The Diem instance receives this document and does not inspect the language field to decide how to parse it --- the structure is identical regardless of origin.
{
"project": "my-app",
"language": "typescript",
"branch": "main",
"commit": "abc123def",
"timestamp": "2026-03-28T10:00:00Z",
"flavor": "agile",
"hierarchy": [
{
"id": "IDENTITY",
"title": "Identity & Access Management",
"level": "Epic",
"parent": null,
"acs": []
},
{
"id": "ROLES",
"title": "User Roles and Permissions",
"level": "Feature",
"parent": "IDENTITY",
"priority": "critical",
"acs": [
{
"name": "adminCanAssignRoles",
"description": "Admin users can assign roles to other users.",
"covered": true,
"tests": [
{ "file": "test/e2e/roles.spec.ts", "name": "admin assigns editor role" }
]
},
{
"name": "nonAdminCannotAssign",
"description": "Non-admin users cannot assign roles.",
"covered": true,
"tests": [
{ "file": "test/e2e/roles.spec.ts", "name": "viewer gets 403" }
]
}
]
}
],
"crossCutting": [
{
"id": "BUG-42",
"type": "Bug",
"target": "ROLES",
"severity": "critical",
"acs": [
{ "name": "emptyRoleListHandled", "covered": false, "tests": [] }
]
}
],
"summary": {
"totalFeatures": 20,
"totalACs": 112,
"coveredACs": 110,
"percentage": 98
}
}{
"project": "my-app",
"language": "typescript",
"branch": "main",
"commit": "abc123def",
"timestamp": "2026-03-28T10:00:00Z",
"flavor": "agile",
"hierarchy": [
{
"id": "IDENTITY",
"title": "Identity & Access Management",
"level": "Epic",
"parent": null,
"acs": []
},
{
"id": "ROLES",
"title": "User Roles and Permissions",
"level": "Feature",
"parent": "IDENTITY",
"priority": "critical",
"acs": [
{
"name": "adminCanAssignRoles",
"description": "Admin users can assign roles to other users.",
"covered": true,
"tests": [
{ "file": "test/e2e/roles.spec.ts", "name": "admin assigns editor role" }
]
},
{
"name": "nonAdminCannotAssign",
"description": "Non-admin users cannot assign roles.",
"covered": true,
"tests": [
{ "file": "test/e2e/roles.spec.ts", "name": "viewer gets 403" }
]
}
]
}
],
"crossCutting": [
{
"id": "BUG-42",
"type": "Bug",
"target": "ROLES",
"severity": "critical",
"acs": [
{ "name": "emptyRoleListHandled", "covered": false, "tests": [] }
]
}
],
"summary": {
"totalFeatures": 20,
"totalACs": 112,
"coveredACs": 110,
"percentage": 98
}
}The hierarchy array carries the full Epic/Feature/Story/Task tree. The crossCutting array holds Bugs and any items that reference features without being children of them. The summary block gives the Diem dashboard exactly the numbers it needs to render a compliance gauge without recomputing anything.
This schema is versioned. Backends target a schema version. Diem rejects documents with an unknown version rather than guessing. No silent data loss.
Shared CLI Subcommands
Every backend shares the same CLI surface. The tspec command dispatches to the correct language scanner based on project configuration. You never need to call tspec-cs directly --- tspec scan reads your .tspec.yaml and does the right thing.
| Subcommand | Purpose | Local/Remote | API Required |
|---|---|---|---|
tspec init --flavor=agile --lang=ts |
Scaffold project | Local | No |
tspec scan |
Run language-specific scanner | Local | No |
tspec push |
Send JSON to Diem API | Remote | Yes |
tspec sync --source=jira |
Generate features from external | Remote | Source API |
tspec lint |
Language-specific linter rules | Local | No |
tspec diff |
Compare two scan snapshots | Local | No |
tspec report --format=json|csv|md|html |
Generate local reports | Local | No |
tspec gate |
Pre-commit quality gate | Local | No |
tspec ci |
CI-optimized scan + push + gate | Remote | Yes |
tspec reverse test/e2e/nav.spec.ts |
Which features does this test cover? | Local | No |
tspec workflow transition NAV --to=Review |
Manage workflow state | Remote | Yes |
tspec assign NAV --to=alice@acme-corp.com |
Assign feature | Remote | Yes |
tspec work --item #ID |
Focus on item (DX) | Remote | Yes |
tspec done |
Finish current item (DX) | Remote | Yes |
tspec next |
Pick next from backlog (DX) | Remote | Yes |
The split matters. Commands marked "Local" work offline, on an airplane, in a sandbox without network access. Commands marked "Remote" talk to the Diem API or an external source. A developer working on a feature can scan, lint, gate, and report without ever authenticating. Only push requires a token.
Type Safety Comparison
Not all languages offer the same guarantees. That is fine. tspec meets each language where it is, extracts maximum safety from the type system available, and makes the tradeoffs explicit.
| Backend | Feature def | AC linking | Typo detection | Rename safety |
|---|---|---|---|---|
| C# | abstract record | nameof() + typeof() |
Compile-time | Full IDE |
| TypeScript | abstract class | keyof T |
Compile-time | Full IDE |
| Java | abstract class | annotation + reflection | Compile-time (annotation processor) | IDE refactor |
| Groovy | abstract class | AST transformation | Compile-time | IDE refactor |
| Python | abstract class | decorator + string | Runtime (linter catches) | Partial |
| Go | interface + struct tags | struct tag | Build-time (go vet) |
Partial |
| Rust | trait + proc macro | #[implements] attribute |
Compile-time (proc macro) | Full (rust-analyzer) |
C#, TypeScript, and Rust sit at the top: typos are impossible because the compiler rejects them. Java and Groovy are close behind thanks to annotation processors and AST transforms. Python and Go trade some static safety for ecosystem conventions --- but tspec lint fills the gap by scanning for broken references before tspec gate lets a commit through.
The point is not that every language must be equally strict. The point is that every language pushes as far as its type system allows, and the JSON output is equally trustworthy regardless.
Before and After
Where you start:
One language. One scanner. Output goes to the console and dies.
Where you end up:
Seven languages. Seven scanners. One protocol. One Diem instance that aggregates everything into a single compliance dashboard. A polyglot organization --- .NET backend, TypeScript frontend, Rust edge services, Python ML pipeline --- gets one unified view of what is specified, what is implemented, and what is tested.
Why This Matters
The backend architecture is the reason tspec scales beyond a single team or a single language. Without it, you either force everyone onto one stack (unrealistic) or you accept that cross-language traceability is impossible (unacceptable). The JSON protocol is the contract. The backends are the adapters. The Diem instance is the single source of truth.
Add a new language? Write a scanner that outputs the JSON schema. Register it. Done. The rest of the ecosystem --- dashboards, gates, reports, workflow transitions --- works immediately because it never depended on the language in the first place.
Previous: Part IV: Custom Workflows | Next: Part VI: Developer Experience