# The family and its layers

> One job per package: six leaves that depend on nothing, three packages that compose them, dependencies that point one way, nothing installed from outside the family, and the locks that hold each rule.

Source: https://burgee.interlace.tools/docs/concepts/family

## What it is

burgee is one package of ten published from this repository. Each owns one job and replaces
the incumbents that do that job today; the [package map](/docs/packages) lists them. Nine
have an API. The tenth, `controlroom`, is reserved at 0.0.1 with no API yet.

The nine split into two layers:

- **Six leaves** depend on nothing: `bellpull` (subprocesses), `closeout` (exit handlers and
  terminal restore), `linegauge` (text width), `paratext` (terminal escapes beyond colour),
  `roundel` (colour and the [output policy](/docs/concepts/output-policy)) and `seniority`
  (configuration).
- **Three compose them**: `burgee` (the CLI framework), `flagstaff` (rendering) and `caique`
  (prompts).

## Why it exists

A program can adopt one layer without the others: a CLI that only needs text measured
installs `linegauge` and nothing else. That is only true while the split is real. The
moment `burgee` measures a string's width itself, or reaches for `chalk` instead of
`roundel`, the layers become a directory layout. The rules below are what keep it real, and
each one is a test rather than a convention.

## The graph

{/* layers:start */}

![The family's dependency graph: burgee depends on bellpull, closeout, linegauge, roundel, seniority; caique depends on closeout, linegauge, paratext, roundel; flagstaff depends on closeout, linegauge, paratext, roundel. The 6 leaves depend on nothing.](/concepts/family-layers.svg)

3 packages compose, 6 are leaves, and 1 is reserved: 13 dependency edges inside the family, every one from a package that composes to a leaf, and none outside it. Generated by `npm run layers:page` from each public package's `package.json`; do not edit by hand.

| Package | Layer | Depends on | Used by | Outside the family |
| :--- | :--- | :--- | :--- | :--- |
| `burgee` | composes | `bellpull`, `closeout`, `linegauge`, `roundel`, `seniority` | — | nothing |
| `caique` | composes | `closeout`, `linegauge`, `paratext`, `roundel` | — | nothing |
| `flagstaff` | composes | `closeout`, `linegauge`, `paratext`, `roundel` | — | nothing |
| `bellpull` | leaf | nothing | `burgee` | nothing |
| `closeout` | leaf | nothing | `burgee`, `caique`, `flagstaff` | nothing |
| `linegauge` | leaf | nothing | `burgee`, `caique`, `flagstaff` | nothing |
| `paratext` | leaf | nothing | `caique`, `flagstaff` | nothing |
| `roundel` | leaf | nothing | `burgee`, `caique`, `flagstaff` | nothing |
| `seniority` | leaf | nothing | `burgee` | nothing |
| `controlroom` | reserved, no API yet | nothing | — | nothing |

{/* layers:end */}

## The rules

**1. Nothing from outside the family.** A published package's `dependencies`,
`peerDependencies` and `optionalDependencies` name only packages published from this
repository. What a caller installs comes from one repository and one supply chain.
Pinned by `installs nothing from outside this repository — dependencies, peers or optional
(D-111)` in [`package-shape-lock.test.ts`](https://github.com/ofri-peretz/burgee/blob/main/scripts/package-shape-lock.test.ts),
by `holds zero external runtime dependencies across the family` in
[`layer-boundaries-lock.test.ts`](https://github.com/ofri-peretz/burgee/blob/main/scripts/layer-boundaries-lock.test.ts),
and, against a real install of each package on its own, by `installs nothing but itself and
the in-family packages it declares` in
[`independence-install-lock.test.ts`](https://github.com/ofri-peretz/burgee/blob/main/scripts/independence-install-lock.test.ts).
A README, a description or a docs page that claims a dependency count the manifest
contradicts fails
[`dependency-claim-lock.test.ts`](https://github.com/ofri-peretz/burgee/blob/main/scripts/dependency-claim-lock.test.ts).

**2. Dependencies point one way.** `package-shape-lock` reads the family bottom-up —
`linegauge`, `seniority`, `bellpull`, `closeout`, `paratext`, `roundel`, `flagstaff`,
`caique`, `controlroom`, `burgee` — and a package may depend only on one earlier in that
order: `has no external runtime dependencies, and same-repo ones only point up the family
(K1, U1, U6)`. Five of the leaves are its foundation tier, graded by `%s depends on nothing:
it is the floor`.

**3. Never leaf to leaf.** A leaf that depended on another leaf would stop being a leaf, and
every package that uses it would sit a third tier up. The generator of the graph above refuses
that: every edge must end at a package that depends on nothing in the family, and
`npm run layers:page -- --check` runs in the `fast` job the required Quality Gate reads.
paratext is the case that shows the cost: it is a leaf, so its hyperlink detection keeps a
declared fork of supports-color rather than importing roundel.

**4. No package depends on an incumbent a sibling replaces.** The list of what each layer
replaces is `LAYERS` in
[`compat-oracle/src/demand.ts`](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/src/demand.ts),
and that list *is* the definition of each layer's job: needing `string-width` is the same
thing as needing `linegauge`. `layer-boundaries-lock` derives the rule from it — `%s depends
on no incumbent another layer replaces` — so the job table is written once.

**5. A sibling owns each mechanism.** A dependency is the easy half to check. The expensive
half is writing a sibling's code by hand, which leaves no dependency behind.
[`inline-implementation-lock.test.ts`](https://github.com/ofri-peretz/burgee/blob/main/scripts/inline-implementation-lock.test.ts)
scans every package's source for 17 shapes that can only be a layer's job, and fails when a
file outside the owner has one:

| Owner | Mechanisms it owns |
| :-- | :-- |
| `bellpull` | spawning a subprocess; resolving an executable across platforms; looking a command up on `PATH` |
| `closeout` | registering a signal handler; hiding or showing the cursor; switching raw mode; hooking process exit |
| `roundel` | reading a colour variable (`NO_COLOR`, `FORCE_COLOR`, `COLORTERM`, `CLICOLOR`); deciding whether this is CI |
| `seniority` | walking up the directory tree; naming a `.env` file |
| `caique` | decoding keypresses |
| `flagstaff` | carrying spinner frames |
| `linegauge` | matching ANSI escapes with a regex; counting painted rows by their newlines; measuring, wrapping or stripping text by hand |
| `paratext`, `linegauge`, `roundel`, `closeout` | spelling a CSI or OSC sequence |

A drop-in façade may reproduce an incumbent's API and behaviour; it may not reimplement the
mechanism a layer owns ([Drop-ins](/docs/concepts/drop-ins) has the rule). The files that
still do are rows in the lock's `KNOWN` table, each with its reason, and the table may only
shrink: `only goes down` and `keeps the list honest — an entry that no longer offends must be
removed`. The lock says plainly what it cannot see: measuring display text with `.length`
looks like any other `.length`, and still needs a reader.

**6. Every layer is used.** A layer nothing else uses has never been proven to fit the stack.
[`composition-lock.test.ts`](https://github.com/ofri-peretz/burgee/blob/main/scripts/composition-lock.test.ts)
reads the band
[`.sdlc/bands/composition.json`](https://github.com/ofri-peretz/burgee/blob/main/.sdlc/bands/composition.json):
the count of edges inside the family may only go up (`has at least as many edges as the last
time this was recorded`), and every package but burgee is used by another or is listed as
`awaiting` one with a reason (`%s is used by another package, or is declared as awaiting
one`). A package that gains a consumer must leave that list. Today `flagstaff`, `caique` and
`controlroom` are awaiting: burgee renders no spinner, box or table, and draws no prompt, so
an edge to either would be manufactured.

## Run it

Each leaf installs and loads on its own. This file imports two leaves and nothing that
composes them:

```js title="leaves.mjs"
import { width } from 'linegauge';
import { colorLevel } from 'roundel/policy';

console.log(width('古池や'), width('\u001B[31mred\u001B[39m'));
console.log(colorLevel({ env: { FORCE_COLOR: '2' }, isTTY: { stdout: false } }));
```

```text title="node leaves.mjs"
6 3
2
```

## Where the rules live

- Spec: [cli-output-stack U1 and U6](https://github.com/ofri-peretz/burgee/blob/main/.sdlc/intents/cli-output-stack/spec.md)
  (nothing from outside the family, and the tier order); decisions
  [D-111](https://github.com/ofri-peretz/burgee/blob/main/.sdlc/decisions/D-111.md) (nothing
  from outside, peers included), [D-180](https://github.com/ofri-peretz/burgee/blob/main/.sdlc/decisions/D-180.md)
  (the mechanism table) and [D-181](https://github.com/ofri-peretz/burgee/blob/main/.sdlc/decisions/D-181.md)
  (roundel owns colour and interactivity).
- [CONTRIBUTING.md](https://github.com/ofri-peretz/burgee/blob/main/CONTRIBUTING.md) — what
  the checks hold a change to.
- [Weight, per subpath](/docs/weight) — what each entry point of each layer costs.
