Metadata-Version: 2.4
Name: throughline-compose
Version: 0.19.0
Summary: Compose one requirements graph from many reusable throughline sources: importer-assigned namespaces, source-native UIDs, and a union checked by throughline's own validator. Ships the tl-compose CLI, a strict superset of tl.
Author-email: Henry J Grech-Cini <henry.grechcini@gmail.com>
License-Expression: Apache-2.0
Project-URL: Homepage, https://github.com/rhodium-org/throughline-compose
Project-URL: Repository, https://github.com/rhodium-org/throughline-compose
Project-URL: Issues, https://github.com/rhodium-org/throughline-compose/issues
Keywords: requirements,requirements-management,traceability,composition,git-native,idd,intent-driven-development,throughline
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Documentation
Classifier: Topic :: Software Development :: Quality Assurance
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: NOTICE
Requires-Dist: throughline>=3.3.0
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Dynamic: license-file

# throughline-compose

**Compose one requirements graph from many reusable
[throughline](https://github.com/rhodium-org/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 <!-- tl:count.inline type == 'user_requirement' -->12<!-- tl:end --> user requirements and <!-- tl:count.inline type == 'system_requirement' -->43<!-- tl:end --> system requirements 
under [`idd/vision/`](https://github.com/rhodium-org/throughline-compose/tree/main/idd/vision), [`idd/goals/`](https://github.com/rhodium-org/throughline-compose/tree/main/idd/goals),
[`idd/user-requirements/`](https://github.com/rhodium-org/throughline-compose/tree/main/idd/user-requirements),
[`idd/system-requirements/`](https://github.com/rhodium-org/throughline-compose/tree/main/idd/system-requirements), and [`idd/non-goals/`](https://github.com/rhodium-org/throughline-compose/tree/main/idd/non-goals), and
published to [`idd/docs/spec.md`](https://github.com/rhodium-org/throughline-compose/blob/main/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](https://github.com/rhodium-org/throughline-compose/blob/main/idd/system-requirements/SR-0007.yml)). Each source
> resolves from either a local `path` or a pinned git `url` + `ref` into a per-user
> cache ([SR-0006](https://github.com/rhodium-org/throughline-compose/blob/main/idd/system-requirements/SR-0006.yml)). 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](https://github.com/rhodium-org/throughline-compose/blob/main/idd/system-requirements/SR-0002.yml)).
  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](https://github.com/rhodium-org/throughline-compose/blob/main/idd/system-requirements/SR-0001.yml)).
  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](https://github.com/rhodium-org/throughline-compose/blob/main/idd/system-requirements/SR-0006.yml)):

```toml
# 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](https://github.com/rhodium-org/throughline-compose/blob/main/idd/system-requirements/SR-0045.yml)).
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](https://github.com/rhodium-org/throughline-compose/blob/main/idd/user-requirements/UR-0005.yml)):

```toml
[[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](https://github.com/rhodium-org/throughline-compose/blob/main/idd/system-requirements/SR-0015.yml)). 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`](https://github.com/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](https://github.com/rhodium-org/throughline-compose/blob/main/idd/user-requirements/UR-0002.yml)). The architecture keeps that
honest:

- **`tl-compose` is a strict superset of `tl`** ([SR-0003](https://github.com/rhodium-org/throughline-compose/blob/main/idd/system-requirements/SR-0003.yml)).
  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](https://github.com/rhodium-org/throughline-compose/blob/main/idd/system-requirements/SR-0025.yml)),
  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](https://github.com/rhodium-org/throughline-compose/blob/main/idd/system-requirements/SR-0004.yml)).
  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](https://github.com/rhodium-org/throughline-compose/blob/main/idd/system-requirements/SR-0026.yml)).
  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](https://github.com/rhodium-org/throughline-compose/blob/main/idd/system-requirements/SR-0005.yml)). 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:

```toml
[[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](https://github.com/rhodium-org/throughline-compose/blob/main/idd/system-requirements/SR-0035.yml)).

Composition deliberately lives here, not in the throughline core
([NG-0001](https://github.com/rhodium-org/throughline-compose/blob/main/idd/non-goals/NG-0001.yml)) — the core stays a single-purpose, offline tool
over one graph, consumed here as a library.

## Working here

```sh
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](https://orcid.org/0009-0007-1565-7530)).
Copyright © 2026 Henry J Grech-Cini. Released under the Apache License 2.0 — see
[`LICENSE`](https://github.com/rhodium-org/throughline-compose/blob/main/LICENSE) and [`NOTICE`](https://github.com/rhodium-org/throughline-compose/blob/main/NOTICE).
