Add a component probe
Add a probe against a component #
Diátaxis form: how-to. Recipe for a competent reader who already knows the layout (seedocs/concepts/architecture.md↗) and the vocabulary (docs/concepts/glossary.md). A component is a service/dependency the SUT relies on (S3, MongoDB, Loki, …). A probe is one function asking one question of one component. This guide is for adding a probe against a component. For driving the SUT through its own API, seeadd-a-scenario.md↗. For wiring probes and scenarios into a run, seeadd-a-segment.md↗.
Probe-author rules #
Two non-negotiable rules every probe author follows. Both come from DDIA Ch. 12 (Stream Processing) ↗ via PRD-06 §06.3.3 and are enforced (mechanically or by review) in make standards.
- A probe writes to at most ONE external system. Never two.
If a check needs to assert agreement between Mongo and ClickHouse, that's a cross-store verifier, not a probe — it goes in src/systems/<sut>/<scenario>.ts as a read-only verify phase (see ADR-0026). The dual-writes anti-pattern is the leading cause of subtle data-corruption bugs in distributed systems; ban it at the author level.
- Declare consistency assumed in the probe header, in the
format // Consistency assumed: <model> on <boundary> — linearizable, read-your-writes, eventual, monotonic-reads, or read-only. The contract is in event-shape.md §"Consistency-assumed convention" ↗. Rule 38 (make standards) enforces this mechanically.
When to use this guide #
You want SQA to probe a new aspect of an existing component, or a new component entirely.
Examples:
- "I want to count documents in MongoDB" → new probe on
mongodb - "I want to list S3 keys under a prefix" → new probe on
s3 - "I want to add Postgres support" → new component folder + first
probe
Recipe — new probe on an existing component #
- Pick a verb for the file name (
list.ts,count.ts,verify.ts).
One probe per file.
- Create
src/components/<system>/<verb>.ts. Export one function
that returns a Result envelope from @lib/result.ts. Take primitive arguments (URLs, IDs); never reach into env or targets from a probe. Per ADR-0012, the function is pure: no throws, no side effects beyond logging, same input → same output shape, always.
```ts // src/components/s3/list.ts import { logger } from "@lib/logger.ts"; import { error, fail, pass, type Result } from "@lib/result.ts";
const NAME = "s3.list";
export async function list( endpoint: string, bucket: string, prefix = "" ): Promise<Result> { try { const result = await /* the actual call */; if (!result.ok) { const ctx = { endpoint, bucket, prefix, status: result.status }; logger.warn(ctx, [${NAME}] non-2xx); return fail(NAME, non-2xx: ${result.status}, ctx); } return pass(NAME, { endpoint, bucket, prefix }); } catch (err) { logger.error({ err }, [${NAME}] probe failed); return error(NAME, err); } } ```
- Log warnings (
logger.warn) forfailoutcomes and errors
(logger.error) for error outcomes. Tag log messages with [<system>.<verb>] so they're greppable.
- Timing: don't add stopwatches inside probes. The
step(...) wrapper attaches observedStartedAt, observedCompletedAt, and observedDurationMs to returned Result envelopes per ADR-0013. HTTP probes may additionally include optional server-reported evidence by calling readHttpTiming(response) from @lib/http-timing.ts and spreading it into Result.context. observedDurationMs is authoritative; serverReportedDurationMs and serverTiming are diagnostic only.
- Return a
Result, never throw. The five outcomes are:pass— system answered correctly.warn— degraded but not blocking (use sparingly).fail— system answered, answer was wrong.error— couldn't ask: network failure, timeout, parse failure.skip— deliberately not run (env not configured, etc.).
The enclosing segment composes children via aggregate(name, children) from @lib/result.ts. Segments are also pure — they don't decide to throw; they aggregate.
- Run
make check— type, lint, and format must pass.
Recipe — new component entirely #
- Create the folder:
src/components/<new-system>/. - Add the first probe file (usually
ready.ts). - Add runtime env for any target coordinate the probe needs:
edit src/config/env.ts to validate the variable, then map it in src/config/targets.ts. Do not add committed target presets; SQA probes external interfaces, so production / staging / development / localhost coordinates all come from operator-controlled env (see ADR-0017).
Cover-test before merging #
Pretend the next reader knows the codebase but not your change. Run the pre-merge checklist in CONTRIBUTING.md ↗. The two questions that catch most issues:
- Did you add a comment that restates the code? If yes, delete it.
- Are the failure modes obvious from the log message alone? If not,
add the missing field to the structured log payload.