Skip to main content

throughline-compose

Compose one requirements graph from many reusable throughline sources — a house style guide, a platform standard, a regulatory baseline — alongside the requirements you write yourself, and work the combined graph as one.

This repository is itself a throughline project: its own design is captured as a grounded IDD spine of 12 user requirements and 46 system requirements under idd/vision/, idd/goals/, idd/user-requirements/, idd/system-requirements/, and idd/non-goals/, and published to idd/docs/spec.md. The graph is gated by tl-compose -C idd check --strict and the document by tl-compose -C idd docs --check; these two counts are rendered from the live spine by the tl:count directive, so they cannot drift.

Status: alpha. The composition engine is built. tl-compose -C idd check composes the declared [[sources]] into a union graph and validates it, and tl-compose -C idd docs renders the published document over that same union, resolving borrowed (namespace:UID) targets (SR-0007). Each source resolves from either a local path or a pinned git url + ref into a per-user cache (SR-0006). Still pending: the tl-compose source add/update/pin subcommands for managing source declarations from the CLI (today you edit the [[sources]] tables by hand).

The idea

A team should be able to adopt standard requirement sets by reference, not by copy-paste, and receive upstream revisions without ever forking. Two identity rules make that safe:

  • Imported items keep their source-native UID (SR-0002). Composition never renumbers or copies. Canonical identity is the pair (source-namespace, UID), so the same SR-0001 may legitimately exist in two sources without collision — the immutable-UID rule is never violated.
  • The composer controls the namespaces (SR-0001). The consumer's own throughline.toml binds each source to a short name it chooses (import X as Y). A qualified reference like gds:SR-0001 denotes a borrowed item; a bare UID is always local. Renaming a namespace is a local-only change with a bounded blast radius.

Declaring sources

A consumer names the sources it composes in an array of [[sources]] tables in its own throughline.toml. Each entry binds a namespace to one source, located either by a pinned git url or by a local path (SR-0006):

# Adopt a published standard by reference, pinned to an edition.
[[sources]]
namespace = "asvs"
url = "https://github.com/rhodium-org/throughline-asvs"
ref = "v4.0.3"                       # a git tag (normal form), branch, or commit SHA

# Develop a source and its consumer side by side.
[[sources]]
namespace = "house-style"
path = "../house-style"              # a directory relative to this project
  • url + ref is the durable, shareable form. The ref pins the exact edition — normally a release tag, but any git ref (branch or commit SHA) works. tl-compose fetches the source from its origin on first use into a per-user cache that lives outside any project tree ($TL_COMPOSE_CACHE, else $XDG_CACHE_HOME, else ~/.cache/throughline-compose/sources/), keyed by (url, ref). A cached source is reused rather than cloned again, but the reuse is checked. A ref that is a commit id is reused with no network call. A tag or a branch can be moved by the origin, so the ref is looked up there and the source is refetched if it now points somewhere else — one ref query, no download, when nothing has moved. An origin that cannot be reached fails the run rather than quietly falling back to whatever was cached; set TL_COMPOSE_OFFLINE=1 to skip the lookup and compose from the cache as it stands, which also makes an uncached source an error rather than a fetch. Nothing is vendored into your repo, so your own item scan never ingests a borrowed graph.
  • path is for local development. A directory, relative to the consumer, for working on a source alongside the project that consumes it.
  • The two are mutually exclusive, and a url must carry a ref. Declaring both path and url, or a url with no ref, is rejected at check time — a dependency can never silently track a moving default. (A ref alongside a path is likewise rejected: a ref only pins a url.)

Transitive sources

Composing a source composes what it composes (SR-0045). If a source you adopt itself declares sources — say house-style composes asvs — then asvs reaches your union too, and so does everything asvs declares, to any depth. Each arrives under the label its declaring source gave it, at the edition that source pinned; you never restate a ref you do not control. The check summary lists every bound namespace with the path that carried it in:

tl-compose check · 2 source(s) composed: house-style (path ../house-style) [3f1c…], asvs (https://…@v5.0.0) [a91e…] via house-style

Your one lever over a transitive label is an alias on the declared source that carries it (UR-0005):

[[sources]]
namespace = "house-style"
path = "../house-style"
alias = { asvs = "owasp" }            # house-style's `asvs` is bound in your union as `owasp`

An alias applies throughout that source's subtree, and every reference the source wrote against its own label (asvs:SR-0001) resolves to the aliased namespace. A source's references only ever resolve through its own declarations — a label you happen to reuse for a different source can never capture them. Your own items may cite any bound namespace, transitive or direct, by its bound label.

The same edition reaching your union under two labels is bound once, under the first label bound, and the summary says which label was folded into which.

A namespace bound to two different editions fails fast — never a silent merge or an arbitrary winner (SR-0015). If you declare asvs at one ref and a source carries asvs at another, or two sources pin the same standard differently, the compose stops and names both the why (the same namespace reaches your union at two editions, and the path each came by) and the fix (pin asvs yourself to the one edition you intend, or set an alias on the declared source carrying one of them so both compose side by side).

The reexport key of earlier releases is withdrawn: in your own throughline.toml it is refused with a pointer to alias; inside a source's it is ignored with a note, so a published edition that used it stays composable.

Moving to a new upstream edition is a one-line change to the ref; the borrowed graph is never edited. See rhodium-org/idd-example for a complete worked consumer that adopts throughline-asvs this way.

One tool, one set of guarantees

In a composed project you drive everything through tl-compose, never tl directly (UR-0002). The architecture keeps that honest:

  • tl-compose is a strict superset of tl (SR-0003). Local-graph commands are forwarded to the throughline library unchanged; the union-aware ones — check, docs, query, trace, new, link, ratify, migrate and context — are overridden to compose, validate, list and render the combined graph. tl-compose context names them from the dispatch table itself (SR-0025), so trust that over any list written by hand, including this one. (The source subcommands for editing declarations are the remaining superset surface — see the status note above.) The core command set is obtained programmatically, so the two surfaces cannot drift apart.
  • Composition reuses throughline unchanged (SR-0004). It merges the sources into one in-memory Project and runs throughline's existing validate, Index, and fingerprint over that union — no second validation engine. A composed graph is exactly as sound as a native one.
  • Adopting a source costs one declaration, never a copy of its model (SR-0026). A borrowed item is reported against only where you can act: its statuses, attributes, link vocabulary and grounding are the owning graph's business, so no finding reaches you whose remedy is a commit in someone else's repo. Your own items are judged under your model in full, and the seam strictly — a reference into a source must resolve, a stamp you recorded on a borrowed clause must still match it, and a chain that grounds inside a source counts as grounded. Adopting a source, or moving its pin, therefore stays a [[sources]] change: nothing to restate in your own throughline.toml, and nothing to re-restate when the edition moves.
  • Bare tl check fails fast on unresolved cross-source refs (SR-0005). If you run core tl in a composed repo by habit, a namespace-qualified reference it cannot resolve makes it stop and point you at tl-compose — never a false clean result. Free external references (a URL, a linked standard) stay opaque, as intended.

Widening the seam for a rule you can answer

The seam above keys on the rule name, and a rule name does not say where the remedy for a finding lies. For nearly every rule the remedy is the owning graph's — exactly what SR-0026 suppresses. But a coverage rule you declared yourself is answered by authoring an item in your own project, so suppressing it hides an obligation you could have met.

Only you know which of the two it is, so you say so:

[[rules.coverage]]
filter = "type == 'system_requirement' and status == 'ratified'"
needs  = "incoming:covers"

[seam]
report_on_borrowed = ["coverage"]

Without the [seam] block that rule is silently inert over borrowed items — the finding names a UID you do not own, so it is dropped and check reports clean. With it, a borrowed requirement nothing covers is reported in your own vocabulary (spec:SR-0009) and --strict fails the build.

Two deliberate limits:

  • It only ever widens. There is no syntax for switching a built-in seam rule off. Those are what keep the assembled union coherent — a dangling cross-source reference, a UID collision, a stamp that no longer matches — and silencing one would hide the unresolved reference UR-0002 forbids outright.
  • An unknown rule name is refused when the config is read, not ignored. The defect this closes is a rule that never fires; a typo accepted quietly would reproduce it exactly.

The default is unchanged — a project that declares nothing behaves as it always has, so no existing consumer inherits findings by upgrading (SR-0035).

Composition deliberately lives here, not in the throughline core (NG-0001) — the core stays a single-purpose, offline tool over one graph, consumed here as a library.

Working here

pip install .                  # pulls throughline transitively; installs tl and tl-compose
tl-compose -C idd context      # agent brief, generated from idd/throughline.toml
tl-compose -C idd check --strict # gate the whole graph
tl-compose -C idd docs --check # gate published-document freshness
tl-compose -C idd docs         # regenerate idd/docs/spec.md from the graph

License

Created by Dr Henry J Grech-Cini (ORCID 0009-0007-1565-7530). Copyright © 2026 Henry J Grech-Cini. Released under the Apache License 2.0 — see LICENSE and NOTICE.

Metadata

Release files for throughline-compose 0.21.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for throughline-compose 0.21.0
File Size Uploaded
throughline_compose-0.21.0.tar.gz 98.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for throughline-compose 0.21.0
File Interpreter ABI Platform
throughline_compose-0.21.0-py3-none-any.whl Python 3 none any Details

Total release size: 159.8 kB

Release files / throughline_compose-0.21.0.tar.gz

Download URL throughline_compose-0.21.0.tar.gz
Size 98.8 kB
Tags Source
SHA-256 checksum
How to use checksums
983b1656b11371f12f6a973be0a8dd61306d1b95607fd3c56782579335c70001
BLAKE2b-256 checksum
How to use checksums
75ee18cac17b387cbd3fbb33f50a110a485e807303e3ee16cdea79cee74c3322
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 17, 2026.

Transparency log

Release files / throughline_compose-0.21.0-py3-none-any.whl

Download URL throughline_compose-0.21.0-py3-none-any.whl
Size 61.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8f8384729b7f7a8613aecf2be31a5b869e331ab15d40bc23c9e1b78c056cb9f4
BLAKE2b-256 checksum
How to use checksums
edbbc2839655ca23f13e773dc928322649fac56950384935ec21b61e8767fe16
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 17, 2026.

Transparency log

Release history Release notifications | RSS feed

0.22.0

2 release files

This release

0.21.0 This release

2 release files

0.20.0

2 release files

0.19.0

2 release files

0.18.0

2 release files

0.17.0

2 release files

0.16.1

2 release files

0.16.0

2 release files

0.15.0

2 release files

0.14.0

2 release files

0.13.0

2 release files

0.12.1

2 release files

0.12.0

2 release files

0.9.0

2 release files

0.8.0

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.5.0

2 release files

0.4.5

2 release files

0.4.4

2 release files

0.4.3

2 release files

0.4.2

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.2

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page