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.
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 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) andseniority(configuration). - Three compose them:
burgee(the CLI framework),flagstaff(rendering) andcaique(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
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 |
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,
by holds zero external runtime dependencies across the family in
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.
A README, a description or a docs page that claims a dependency count the manifest
contradicts fails
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,
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
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 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
reads the band
.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:
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 } }));6 3
2Where the rules live
- Spec: cli-output-stack U1 and U6 (nothing from outside the family, and the tier order); decisions D-111 (nothing from outside, peers included), D-180 (the mechanism table) and D-181 (roundel owns colour and interactivity).
- CONTRIBUTING.md — what the checks hold a change to.
- Weight, per subpath — what each entry point of each layer costs.
Concepts
How the burgee family works, on its own terms: the ideas every package shares, the exact rules behind them, and the tests that hold each rule.
The output policy
One function decides where output is going and how much colour it may carry: roundel/policy's outputMode() and colorLevel(), and roundel/terminal's interactive(). The packages that draw ask them rather than reading the process themselves.