Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Multi-repo dependencies

MarkSpec can consume another repository’s requirements as a versioned git dependency. The upstream repo’s entries hydrate into your project’s own traceability graph, so a Satisfies: link can cross the repo boundary and markspec check resolves it exactly like a same-project reference. This recipe walks the consumer side end to end: declare the dependency, lock it, trace into it, verify, and see each failure mode.

For the graph-integration semantics that make this work — how upstream entries join the graph, resolve, and gate collisions — see Upstream entries resolve in the graph in the CLI guide and ADR-031. This recipe is the how-to; that section is the reference.

1. Declare the dependency

Add a dependencies: entry to project.yaml naming the upstream repo’s git URL:

# project.yaml
name: io.acme.braking-system
version: "2.3.0"

dependencies:
  - url: git@github.com:acme/aeb-icd.git
    name: icd # short id: cache dir, lock row, Origin badge
    version: "v2.1.0" # exact tag → frozen baseline
  • url (required) — the upstream git repository.
  • name (optional) — a short id used for the cache directory, the lockfile row, and the Origin: badge on hydrated entries. Derived from the URL when absent (aeb-icd).
  • version (optional) — the resolution intent:
    • absent → auto: the latest release tag, else the default-branch head.
    • an exact tag (v2.1.0) → a frozen baseline that never moves until you change it.
    • a branch name (main) → tracks that branch’s head on every re-lock.

2. Lock it

markspec lock

markspec lock resolves the declared version intent against the upstream repo, acquires the tree at the resolved commit via a shallow git fetch by sha (no clone, no history), compiles it in-process, writes the compiled snapshot to .markspec/cache/upstreams/icd/, and pins the result as an [[upstream.dependency]] lockfile row. The cache directory is gitignored automatically — lock appends .markspec/cache/ to .gitignore the first time it runs.

The pinned row records the requested intent, what actually resolved, the exact commit, and the snapshot hash:

# markspec.lock (excerpt — @generated by `markspec lock`, do not hand-edit)
[[upstream.dependency]]
id = "icd"
url = "git@github.com:acme/aeb-icd.git"
intent = "v2.1.0"
resolved = "tag:v2.1.0"
sha = "9f3c1a2e5b7d0c4f6a81b2d3e4f5069718293a4b"
snapshot = "sha256:b1946ac92492d2347c6235b4d2611184..."
locked-at = "2026-07-05T12:00:00Z"

Only markspec lock touches the network. Once the pin exists, check, compile, the LSP, and the MCP server all resolve entirely offline from the cached snapshot — see lock in the CLI guide for the first-lock / restore / update flows.

3. Trace across the repo boundary

Author a project entry whose trace value names an upstream display ID:

- [STK_BRK_0007] Deceleration request honored

  The braking system shall action a deceleration request within 50 ms of
  receiving it on the vehicle bus.

      Id: 01ARZ3NDEKTSV4RRFFQ69G5FAV
      Type: stakeholder-requirement
      Satisfies: ICD_BRK_0010

ICD_BRK_0010 is declared in the upstream ICD repo, never in this project — markspec lock hydrated it into the graph as a read-only, origin-tagged entry.

4. Verify

markspec check

exits 0. Satisfies: ICD_BRK_0010 resolves across the repo boundary with no warning. The upstream entry is a full resolution target for show, context, dependents, and report too:

markspec show ICD_BRK_0010 "docs/**/*.md"

prints the upstream entry with an Origin: line naming the upstream and its version. The Source: line gives the entry’s location in the upstream repo’s own tree — the path it compiled from, not a local cache path:

ICD_BRK_0010  Deceleration bus message contract
  Type: interface
  Shape: Authored
  Origin: icd@v2.1.0
  ...
  Source: docs/icd.md:12:1

5. Failure modes

SymptomDiagnosticFix
Satisfies: names an id no upstream declaresMSL-T014Fix the trace value. The warning names every upstream searched, e.g. not found in project or upstreams: icd.
A project entry reuses an upstream display ID or Id:MSL-R014Rename the project entry — upstream entries are read-only and win the collision.
The cached snapshot is missing or its hash moved (fresh clone, cleaned tree, CI)MSL-L212Run markspec lock — the restore flow re-fetches the pinned content without moving the pin.
A dependencies: pin resolved to a branch or bare sha rather than a tagMSL-L215Advisory by default; markspec check --strict promotes it to a hard error, so a release build cannot pass against an unbaselined dependency. Pin an exact tag to clear it.

MSL-T014 replaces the plain MSL-L006 “unresolved reference” warning only once the project declares any dependencies: or references: — a project with no declared upstreams keeps the MSL-L006 behavior.

dependencies: vs references:

Both pin offline-after-lock under .markspec/cache/upstreams/<id>/, but they model different relationships and are treated differently by coverage:

  • dependencies: — another project you build on (a git repository). Its entries participate in coverage like your own: a hydrated product requirement with no component coverage is a reported gap. Use this when you trace into the upstream and expect the traceability to be complete.
  • references: — a published site you cite (a compile-output snapshot). Its entries are traceability leaves — report coverage never reports one as an orphan or an unsatisfied gap, because a citation isn’t something your own project is expected to cover.

See Upstream entries resolve in the graph in the CLI guide for the authoritative coverage-treatment rule, and The projectRef shape for the full field reference shared by both lists.

Notes

  • Shallow fetch, not a clone. Acquisition is a git fetch at the resolved sha — no working clone, no history. Resolving an upstream from a forge release tarball (no git at all) is a planned fast-follow, not yet shipped.
  • Offline after lock. Only markspec lock reaches the network. check, compile, the LSP, and the MCP server read the pinned cache and never fetch.
  • Upstream entries are read-only and validation-exempt. No structural checks or prose lint run against them — that already happened in their own repo — but they remain full resolution targets for cross-repo trace edges. See Upstream entries resolve in the graph for the full rule.
  • Aggregate a whole program from one root. A root or program repo that depends on every member repo resolves the entire program’s cross-repo graph in one compile — see the Root/program project pattern in the CLI guide.
  • Cache the snapshots in CI. Key an actions/cache on markspec.lock so a job only re-acquires an upstream when its pin actually moves.