Add a segment
Add a segment to an existing system #
Diátaxis form: how-to. Vocabulary in this guide followsdocs/concepts/glossary.md. A segment composes the work for one SUT and aggregates outcomes into one compositeResult. Children are usually probes (src/components/<x>/<verb>.tsfor component probes, orsrc/systems/<sut>/ready.tsfor the SUT's own readiness) or scenarios (src/systems/<sut>/<scenario>.ts). For the probe how-to seeadd-a-component.md↗; for scenarios seeadd-a-scenario.md↗. For the why behind step IDs seedocs/concepts/step-ids.md↗. For the purity contract every segment follows, see ADR-0012. For the parallel-by-default rule used below, see ADR-0015.
When to use this guide #
You have a system (src/systems/<sut>/) and you want to add a coherent group of probes or a scenario under a new top-level number.
snappy today has two segments: segment 1 is preflight.ts (component probes + SUT readiness, parallel) and segment 2 is activation.ts (the domain-activation scenario, sequential). Segment file names are domain words (preflight, activation) — not layer words.
Picking concurrency: parallel by default #
Per ADR-0015, a segment declares its concurrency mode by which composition helper it uses:
parallelize(name, factories)— children run with
Promise.all. Wall time is bounded by the slowest child, not the sum. Default for independent children (most segments).
sequentialize(name, factories)— children run one after the
next. Use when a child consumes another's output, or must observe the system in a known state established by a prior child.
Rule of thumb: can I write this segment as N independent factories with no shared state? If yes → parallelize. If no → sequentialize.
Recipe (parallel — the common case) #
- Create
src/systems/<system>/<segment-name>.ts. Use semantic
kebab-case for the filename (preflight.ts, data-probes.ts), not a numbered prefix. One segment per file.
- Pick the next top-level step number (
2,3, …). The number
lives at the call site in src/systems/<system>/index.ts, which is the declaration-order manifest. Don't reuse numbers from deleted segments — see docs/concepts/step-ids.md ↗.
Note: under parallelize, step IDs no longer reflect physical execution order — they're the canonical declaration order (the order the result tree presents in postmortems). The actual timeline lives on each Result.context.observedStartedAt / observedCompletedAt per ADR-0013.
- Compose the segment from probes. Don't put probe logic inline.
The segment is a pure function: it returns a composite Result via parallelize or sequentialize. It does not throw on any child outcome.
```ts // src/systems/snappy/data-probes.ts import as ch from "@components/clickhouse/count.ts"; import as mongo from "@components/mongodb/count.ts"; import { targets } from "@config/targets.ts"; import { parallelize, type Result } from "@lib/result.ts"; import { step } from "@lib/step.ts";
const NAME = "snappy.data-probes";
export async function runDataProbes(): Promise<Result> { return parallelize(NAME, [ () => step("2.1", "count mongo domains", () => mongo.count(targets.mongoUri, "domains") ).then((s) => s.value),
() => step("2.2", "count clickhouse rows", () => ch.count(targets.clickhouseUrl, "domain") ).then((s) => s.value), ]); } ```
Each factory () => step(...) is invoked by parallelize — that's what gives the helper control over execution order.
- Wire it into the system's
index.ts. The system flow is itself a
pure function; it composes segment Results into a top-level composite. Pick parallelize here too unless one segment depends on another (preflight + data-probes + behavioural-tests are typically dependent — preflight establishes reachability before the others run — so sequentialize is correct at this layer):
```ts // src/systems/snappy/index.ts import { sequentialize, type Result } from "@lib/result.ts"; import { step } from "@lib/step.ts";
const NAME = "snappy";
export async function runSnappyApi(): Promise<Result> { return sequentialize(NAME, [ () => step("1", "preflight", () => runPreflight()).then((s) => s.value), () => step("2", "data probes", () => runDataProbes()).then((s) => s.value), ]); } ```
- Don't throw — aggregate. Probes return
Result; segments
compose their children with parallelize / sequentialize. The top-level runner inspects the root and sets the exit code via exitCodeFor(result). See ADR-0012.
- Run
make runand verify:- the new step IDs appear in the result tree in **declaration
order** (1.1, 1.2, …) regardless of which child finished first;
- the parent segment's
observedDurationMsis approximately
the max of its children's durations under parallelize, or the sum under sequentialize;
- the exit code matches the worst child outcome.
Then make check.
Step IDs across segments. The IDs you write instep("1.1", ...)/step("2.1", ...)calls are authoritative everywhere they appear: log lines, the trace, and the rendered summary table.step()stamps the assigned ID onto the returnedResult.context.stepId; the summary'sflattenLeaves()reads it from there. Position-based inference is a fallback only used for Results that didn't go throughstep().
Recipe (sequential — when there's a real dependency) #
Identical to the parallel recipe, with sequentialize substituted for parallelize. Children run in declaration order; each waits for the previous one to settle. Use when:
- a probe needs the system in a state another probe established
(e.g. "after writing a doc, read it back"),
- you want to guarantee one connection at a time to a rate-limited
external system,
- correctness depends on order in any other observable way.
If you reach for sequentialize and can't write down the dependency in one sentence, the segment is probably parallel.