Skip to content

Spec links & drift

A spec link is the record that connects one spec to one code symbol. It carries state. When the spec changes, the code changes, or the link breaks, the state transitions automatically. The state is what tells you whether a requirement is actually shipped — and stays shipped.

┌──────────────────────────┐
▼ │
drafted → implementing → implemented → verified
│ │
└──→ drifted ←┘
▲ │
│ ▼
broken (target missing)
│ │
└──→ orphaned (target gone for good)
StateMeaningHow you got here
draftedSpec exists; link is declared; code may or may not exist yet.Author wrote @link src/foo.ts:bar in the spec, but bar isn’t built yet.
implementingAn agent has started implementing this spec.A workflow’s agent step started editing the linked target.
implementedCode exists and matches the spec’s content hash.specship_link_assert succeeded; target signature matches.
verifiedTests linked to this spec all pass.A spec-verify (or any tests link runner) passed and promoted.
driftedEither spec body or target signature changed since the link was set.Drift detector caught it on the next sync.
brokenTarget file exists but the symbol’s signature no longer matches what was linked.A rename, an arg-list change, a return-type flip — anything that breaks the “same function” hypothesis.
orphanedTarget file or symbol is gone entirely.A delete, a rename to elsewhere, a move out of the file.

Healthy links spend most of their life in verified. The drift queue surfaces everything that isn’t.

When SpecShip syncs (after a file watcher event, on a manual specship sync, or at the start of a specship serve --mcp session), it does two passes:

For every link, recompute hash(spec.body). Compare to spec_hash_at_link. If different → the spec body has changed since the link was set → mark the link drifted with drift_axis: spec.

This catches the case where a PM rewrote a requirement and forgot to update the code. The link tells you to re-check.

For every link, look up the target symbol in the graph and compute its current signature (name + parameters + return type, or for a method: name + parameters + parent class). Compare to node_sig_at_link. If different → mark drifted with drift_axis: code. If the symbol isn’t found at all → mark broken (file exists but symbol gone) or orphaned (file gone).

This catches refactors that move code but forget to re-link.

A verified link is one with both of these properties:

  1. Spec hash and code signature both match snapshots. (Promotes drifted/broken/orphaned back to implemented first.)
  2. At least one test linked to this spec passed in a recent run. (Promotes implementedverified.)

Both conditions are checked by specship_link_verify and by the spec-verify workflow. The result is durable until something drifts.

In the desktop UI, Drift queue filters every link in a concerning state with controls per row:

ActionEffect
Re-verifyRe-runs the linked tests. If they pass and signatures match, promotes back to verified.
Re-attachSearches the graph for a likely new target (rename / move / split). Promotes the link to point at the new target. Used for broken and orphaned.
Open in editorReveals the linked code in your default editor.
Open specReveals the spec source for editing.
FixKicks off the spec-fix workflow with this link pre-loaded.

CLI equivalent: specship drifted. The flag --fail-on=drifted,broken makes it a CI gate.

Every link records how it was created. Three sources:

ProvenanceSourceTrust
pragmaA // @implements REQ-AUTH-005 comment in the code itself, parsed at index time.High — the author wrote it.
agent-assertedAn agent called specship_link_assert after writing code.Medium — agent claims it implemented the spec.
heuristicThe resolver matched a target’s qualified name to a @link declaration in the spec.Lower — text-match guess.

The desktop UI shows provenance as a small chip on each link row. When something looks fishy, the chip tells you whether to trust it.

A link is more than just “spec → code”; it carries a kind:

KindMeaning
implementsThis code is the implementation of this requirement.
testsThis code (a test) validates this requirement.
documentsThis code is relevant to the requirement (e.g. config, schema) but isn’t the implementation.
validatesThis code is a runtime check (an assertion, a guard) that enforces the requirement.

A single spec usually has multiple links of different kinds:

REQ-AUTH-005
├── implements → src/auth.ts:checkExpiry
├── tests → test/auth.test.ts:expiredTokenReturnsToken_expired
├── tests → test/auth.test.ts:clockSkewToleranceIs30s
└── documents → docs/auth-tokens.md

The verified state cares about tests links. The drifted state cares about all of them.

The agent does most link assertions for you. But there are cases where you reach for the CLI:

Terminal window
# I refactored checkExpiry into a class method. Re-attach the existing link.
specship spec link-assert \
--spec-id REQ-AUTH-005 \
--target-file src/auth/SessionValidator.ts \
--target-qualified-name SessionValidator.checkExpiry \
--kind implements

The pragma + comment approach is friendlier for “this is the spec source of truth”: just drop a // @implements REQ-AUTH-005 above the function and the next sync picks it up.

→ Next: The @implements pragma — the comment annotation form.