Multi-Repository Composition¶
GUIDE · MULTI-REPO
Large programs partition a system model across organizational boundaries — a prime integrator assembles subcontractor models, each maintained in a separate repository. Multi-repository composition lets one Syscribe model import namespaces from peer repos and resolve cross-repo cross-references at analysis time, without physically merging them (§14).
Everything here is opt-in: a model with no [repos] table behaves exactly as a single repository, and none of these rules, commands, or git probes run.
The capability has three layers:
- Declaration — which peer repos exist (
[repos]in.syscribe.toml). - Import — which of their namespaces to mount (
repoImports:on a package). - Resolution & gating — cross-repo references resolve, and reproducibility is enforced (
E510–E515,W510–W512).
1. Declaring peer repos — [repos]¶
Peer repositories are declared in the model-root .syscribe.toml:
[repos]
avionics = { path = "../avionics-model", root = "model/" }
brakes = { path = "../brakes-subsystem", root = "model/", ref = "v2.1.0" }
shared = { path = "../shared-library", root = "model/", ref = "main" }
Each table key is the repo alias used in import declarations. Fields:
| Key | Required | Description |
|---|---|---|
path | yes | Filesystem path to the repo root, relative to this model's .syscribe.toml. |
root | no (default model/) | Where the Syscribe model lives inside the repo. |
ref | no | Git tag/branch/SHA the composition is pinned to. Absent ⇒ "use whatever is on disk" (raises W510). |
At load time the tool resolves <config-dir>/<path>/<root>, walks that peer model, and indexes its element qualified names and exported stable IDs. A peer is loaded read-only.
2. Mounting a namespace — repoImports:¶
A package _index.md mounts a sub-tree from a peer repo:
---
type: Package
name: Integration
repoImports:
- repo: brakes # alias from [repos] (else E513)
qname: BrakeSystem # element/package in that repo (else E514)
as: Brakes # local mount name (defaults to last qname segment)
---
This mounts the peer's BrakeSystem at Integration::Brakes. A reference written through the mount — <package>::<as>::X — denotes the peer's <qname>::X (Integration::Brakes::REQ-BRK-001 → the peer's BrakeSystem::REQ-BRK-001) for every cross-reference field that resolves across repos: verifies:, derivedFrom:, satisfies:, allocatedTo:, supertype:, typedBy: (incl. inline features:), subsets:, redefines: — and, for hierarchical product lines (§7), subConfigurations: entries and the <FeatureDef> part of cross-tier parameterBindings: keys. The peer-native qualified name and the global stable id keep working. A mount on the model-root package is just <as>; when two mounts nest, the longer (more specific) one applies. A mounted reference that names nothing in that peer is E512.
# Integration/TC-INT-002.md — all three forms reach the same peer requirement
verifies:
- Integration::Brakes::REQ-BRK-001 # through the mount point
- BrakeSystem::REQ-BRK-001 # peer-native qualified name
- REQ-BRK-001 # global stable id
| Field | Required | Description |
|---|---|---|
repo | yes | Alias matching a key in [repos]. |
qname | yes | Qualified name of the element/package to import, relative to the peer model root (or its unique trailing ::-segments). |
as | no | Local alias; defaults to the last segment of qname. |
3. Cross-repo resolution¶
Trace references — verifies:, derivedFrom:, satisfies:, allocatedTo: — and the structural supertype:/typedBy:/subsets:/redefines: resolve by searching the local model first, then each loaded repo in declaration order. A reference resolves by:
- global stable ID (
REQ-*,TC-*, …) — the stable-ID namespace is global across the whole composition, soverifies: REQ-PEER-001finds the peer's requirement with no mount prefix; or - qualified name (exact, or the trailing
::-segment of a peer qname); or - mount path —
<package>::<as>::X, the peer's<qname>::X(§2).
# local TestCase verifying a requirement owned by the avionics repo
id: TC-INT-001
type: TestCase
name: "Integrated navigation fix accuracy"
status: draft
testLevel: L4
verifies:
- REQ-AVI-NAV-014 # resolves in the avionics peer — clean, no E512
Cross-repo supertype:/typedBy: links are permitted for structural reuse (e.g. a shared PartDef). Integrity-level propagation does not cross repo boundaries — each repo authors its own safety case independently.
4. Reproducibility — git refs and submodules¶
[repos] is complementary to git submodules, not a replacement: a submodule provides the pinned checkout on disk, and [repos] adds the model-level resolution above plus two reproducibility gates on top. A peer can equally be a sibling checkout or a monorepo path.
W511— checkout drift. When a repo has aref:, validation compares the peer work tree'sHEADwith the commit theref:resolves to and warns when they differ ("your checkout has moved off the pinned ref").W512— gitlink/ref disagreement. Whenpathis a git submodule of the composing model's repo, validation compares the commit theref:resolves to against the gitlink the parent repo records (git ls-tree HEAD <path>) and warns when.syscribe.tomldisagrees with.gitmodules.
Both are opt-in advisories by default and become a hard CI gate with --deny:
# fail CI unless every peer is at its pinned, consistent snapshot
syscribe -m model/ validate --deny W510 --deny W511 --deny W512
Neither is ever raised when the comparison can't be made — git unavailable, the peer is not a work tree, the ref: does not resolve, the path is absent, or (for W512) path is not a submodule — so they never false-flag a non-reproducible-by-design layout.
5. CLI¶
syscribe -m model/ repos [list] # configured repos: path, ref, on-disk + sync status
syscribe -m model/ repos status # whether each pinned repo is at its ref; exit 2 on drift
syscribe -m model/ repos sync [--all | <alias>] # git fetch + checkout <ref> (submodule-aware checkout via the repo)
repos status and repos list read the same drift state the validator computes, so the CLI and W511 never disagree.
6. Validation rules¶
| Code | Condition |
|---|---|
E510 | Circular repo import — a repo transitively imports back into this model. |
E511 | repos.<alias>.path is absent on disk and no ref: is configured. |
E512 | A cross-repo verifies/derivedFrom/satisfies/allocatedTo/supertype/typedBy/subsets/redefines reference resolves in neither the local model nor any loaded repo (reported instead of the field's own unresolved-reference code, e.g. E102/E110). |
E513 | repoImports[].repo names an alias not present in [repos]. |
E514 | repoImports[].qname does not resolve to any element in the named repo. |
E515 | Two repos export the same stable ID — the local model and a peer, or two different peers (the id namespace is global). |
W510 | A repo has no ref: — composition is not pinned to a reproducible snapshot. |
W511 | A peer repo's HEAD has drifted from its configured ref:. |
W512 | A submodule peer's ref: disagrees with the parent's .gitmodules gitlink. |
See the Rule Reference for the canonical table, and §14 of the Full Specification for the normative definition.
7. Building a product line of product lines — subConfigurations:¶
[repos]/repoImports: above let one model reference elements in a peer repo. A Configuration can additionally consolidate another repo's already-configured product line into its own via subConfigurations: — a prime integrator assembling a vehicle line from a battery-pack line, itself assembled from a cell-chemistry line, each maintained independently. This is a distinct, opt-in capability layered on top of everything above it (§9.7/§14.7 in the spec); see the Variability guide §5 for the full treatment and examples/hple-multitier/ for a worked example.
A subConfigurations: entry and the <FeatureDef> part of a cross-tier parameterBindings: key may be written through a repoImports: mount path, exactly like the reference fields in §2 (GH #146):
# pack/model/Vendor/_index.md mounts the cell repo:
# repoImports: [{repo: cell, qname: Configurations, as: CellConfs},
# {repo: cell, qname: Features, as: CellFeatures}]
type: Configuration
id: CONF-PACK-001
name: "Standard pack"
status: approved
featureModel: Features
subConfigurations: Vendor::CellConfs::CONF-CELL-001 # -> the cell repo's Configurations::CONF-CELL-001
parameterBindings:
Vendor::CellFeatures::Cell.capacityAh: 50 # -> the cell repo's Features::Cell.capacityAh
A mount path that names nothing in its peer is E516. Each tier's keys resolve through that tier's own mounts, so a higher tier sees a parameter the pack tier closed through Vendor::CellFeatures as already bound (E523 if it binds it again; not reported open by W513).