Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ All build tiers and v1 gates are `tested` (37 of 39 requirements): `model` → `
- **`nvm use default` puts a Node 24 on PATH** for `bun run mutate` / focused `stryker run` invocations.
- **`bun test <single-file>` may exit 1 with zero failures** — the per-file coverage floor (bunfig.toml) judges partially-imported files. Exit 1 with 0 fails = coverage floor, not a test failure; gate on full `bun test` runs.
- **Internal imports**: upward reaches (anything needing `../`) use `#src/…`/`#scripts/…` (package.json `imports`); same-directory and downward stay relative, explicit `.ts` extensions. Enforced by `test/import-style.test.ts` (D-013).
- **Never run `biome migrate` unattended** (D-021). On this repo's v1 config it rewrites `"rules": { "recommended": true }` as `"rules": { "preset": "none" }`, which deletes the rule set instead of preserving it: `biome check .` then exits 0 on code containing `any`, `==` and unused vars, so the lint gate dies silently and CI stays green. The correct v2 spelling is `"preset": "recommended"`. After any biome config change, re-verify with a planted violation and check the **exit code**, not the printed summary.
- **Never run `biome migrate` unattended** (D-021, D-023). On the v1 config it rewrote `"rules": { "recommended": true }` as `"rules": { "preset": "none" }`, which deletes the rule set instead of preserving it: `biome check .` then exits 0 on code containing `any`, `==` and unused vars, so the lint gate dies silently and CI stays green. The correct spelling is `"preset": "recommended"`, and `test/lint-gate.test.ts` now fails if it ever changes back. After any biome config change, re-verify with a planted violation and check the **exit code**, not the printed summary.
- **Biome's "safe" fixes are not all safe here** (D-023). `noUselessEscapeInRegex` unescaped the `\.` in `MQTT_EXTENSION_KEY`, which is a no-op to the regex engine but breaks D-019's character-for-character transcription of the upstream schema key that `test/upstream-drift.test.ts` compares byte-for-byte. It is suppressed inline with that reason. Read what `--write` changed before trusting it; the test suite caught this one, but a less-covered invariant would have slipped through.
- **TypeScript 7 ships `tsc` only** (D-022) — no `tsserver.js`, no programmatic `typescript` module API under `node_modules/typescript/lib`. Nothing in the repo imports it as a module, so gates and mutation runs are unaffected, but an editor set to "use workspace TypeScript version" finds no language server and silently falls back to its own bundled TypeScript. Expect the editor and the `typecheck` gate to be different compilers; when they disagree, `bun run typecheck` is the authority.
- **Dependency bumps: refresh ≠ range change.** Taking a newer build of an already-declared range is routine; requiring a version you previously did not is a decision. `bun update` conflates them — it rewrites `package.json` floors even for packages whose version did not move — so refresh with `bun update`, then `git checkout package.json && bun install` to keep the change lockfile-only. Range changes get their own entry in `DECISIONS.md`, their own PR, and a measurement (D-020, D-021).
- **CI (GitHub Actions)**: `.github/workflows/ci.yml` runs the gate set (`check-docs` → `lint` → `typecheck` → `demo-app:build` → full `bun test`) on PRs and main pushes; main pushes also upload `demo-app/dist/` + `coverage/` artifacts. Bun is pinned there (1.3.14) so the bunfig coverage-gate semantics stay as verified; bump the pin deliberately, in its own PR. A `main` ruleset requires the `gates` check (repo-admin bypass keeps direct pushes possible). Mutation testing stays out of CI (D-017).
16 changes: 16 additions & 0 deletions DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,3 +205,19 @@ Append-only. Each decision has a stable never-reused `D-###` id, what was decide
**Obligations**: none deferred.
**From**: the dependency review of 2026-08-01 (D-021), which measured the bump as viable and deferred it to its own PR for the deliberate decision.
**Folds into**: package.json, bun.lock, AGENTS.md (working notes)

### D-023: Take Biome 2; correct the migration by hand and pin the corrected config with a test
**Date**: 2026-08-01
**What**: Raise `@biomejs/biome` from `^1.9.0` to `^2.5.6`, discharging D-021's second deferred obligation. The migration is **not** taken as `biome migrate` produced it: its output is corrected by hand, and the correction is pinned by a new `test/lint-gate.test.ts` so it cannot silently revert. Also fixes the 34 findings the new rule set surfaces, and suppresses exactly one of them with a reason.
**Why**: Biome 2 is a straight improvement to the gate (the recommended set catches more, and the transport-isolation rule graduates from `nursery` to `style`, so it stops depending on a nursery rule staying available). The reason it needed its own PR rather than riding along with D-021's refreshes is that its migration path is actively unsafe here, and the code changes it forces are not mechanical noise — one of them is a real invariant.
**Measured (2026-08-01, installed toolchain)**:
- **`biome migrate` silently deletes the rule set.** On the v1 config it rewrites `"rules": { "recommended": true }` as `"rules": { "preset": "none" }`. Verified by planting rather than by reading the output: a file containing `any`, `==` and an unused `var` draws **zero** diagnostics under the migrated config and **three** (`noExplicitAny`, `noDoubleEquals`, `noUnusedVariables`) once `preset` is corrected to `"recommended"`. `biome check .` exits 0 in the broken state, so nothing about the symptom points at the cause.
- **The transport-isolation rule survives the group move and still enforces**: with the corrected config, `noRestrictedImports` (now `style/`, formerly `nursery/`) exits 1 on an `aedes` import outside `src/broker/` and exits 0 on the same file inside it.
- **34 findings on the existing tree**: 17 `assist/source/organizeImports`, 14 `lint/correctness/noUnsafeOptionalChaining`, 2 `lint/complexity/useOptionalChain`, 1 `lint/correctness/noUnusedFunctionParameters`, 1 `lint/complexity/noUselessEscapeInRegex`. All resolved; `biome check .` exits 0.
- **One of Biome's "safe" fixes was not safe.** `noUselessEscapeInRegex` rewrote `MQTT_EXTENSION_KEY` from `/^x-[\w\d\.\x2d_]+$/` to `/^x-[\w\d.\x2d_]+$/`. That is a no-op to the regex engine and a real defect here: D-019 transcribes that pattern **character-for-character** from the upstream `@asyncapi/specs` schema key, and `test/upstream-drift.test.ts` compares `.source` byte-for-byte against it. The full test suite caught it (1 fail, `431 pass`), the fix is reverted, and the rule is suppressed inline with that reason rather than the escape being re-removed by a future `--write`.
- **The 14 `noUnsafeOptionalChaining` sites were all the same defect**: `(x?.y as T).z`, a `?.` guard immediately dereferenced, so an absent value produced a `TypeError` instead of a clean assertion diff. Fixed by continuing the chain through the cast, `(x?.y as T | undefined)?.z`, chosen over a non-null assertion because that spelling is clean under both `tsc` and Biome while `x!.y` trips `style/noNonNullAssertion` — both alternatives were probed before picking.
**Mitigations / notes**: The corrected config is guarded by `test/lint-gate.test.ts`, which asserts `linter.rules.preset === "recommended"` and that the transport-isolation override is still `error` for `aedes`/`aedes-server-factory` in `**/src/**` and `off` in `**/src/broker/**`. It carries no `R-###` arrow tag, following `test/import-style.test.ts` (D-013): it pins a decision, not a requirement. Its negative control was verified — setting `preset` back to `"none"` makes it exit 1. It asserts config **shape**, not gate liveness end-to-end: probing liveness for real needs a violating file under `src/`, which would race `test/transport-isolation.test.ts`'s walk of that same directory, so liveness stays a manual check (recorded in AGENTS.md) and the automated guard covers the one regression actually observed. Separately, `createServer(config, caps)` in `src/control-plane/index.ts` has a genuinely unused first parameter; it is underscored to `_config` rather than removed, because dropping it changes an exported function's arity, which is a refactor and not part of a dependency bump. That remains available as a follow-up.
**Consequences for earlier entries**: discharges obligation (2) of D-021 and supersedes the warning in its Mitigations section, which anticipated the landmine but predates the guard test. D-021 obligation (1), `aedes` 1.x behind R-006/R-007, remains the only one open. D-019's character-for-character transcription is unchanged and now defended by an inline suppression as well as by its test.
**Obligations**: none blocking. One optional follow-up: remove the unused `_config` parameter from `createServer` in a change scoped to that refactor.
**From**: D-021's dependency review, which measured the bump as viable, recorded the `biome migrate` landmine, and deferred the work to its own PR.
**Folds into**: package.json, bun.lock, biome.json, test/lint-gate.test.ts, src/registry/index.ts, src/engine/index.ts, src/control-plane/index.ts, AGENTS.md (working notes), plus import ordering across 17 files and the 14 optional-chain sites
31 changes: 16 additions & 15 deletions biome.json
Original file line number Diff line number Diff line change
@@ -1,27 +1,28 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.0/schema.json",
"$schema": "https://biomejs.dev/schemas/2.5.6/schema.json",
"files": {
"ignore": [
"node_modules",
"fixtures",
"scripts",
".claude",
".stryker-tmp",
"reports",
"demo-app/dist",
".offbook"
"includes": [
"**",
"!**/node_modules",
"!**/fixtures",
"!**/scripts",
"!**/.claude",
"!**/.stryker-tmp",
"!**/reports",
"!**/demo-app/dist",
"!**/.offbook"
]
},
"linter": {
"enabled": true,
"rules": { "recommended": true }
"rules": { "preset": "recommended" }
},
"overrides": [
{
"include": ["src/**"],
"includes": ["**/src/**"],
"linter": {
"rules": {
"nursery": {
"style": {
"noRestrictedImports": {
"level": "error",
"options": {
Expand All @@ -36,8 +37,8 @@
}
},
{
"include": ["src/broker/**"],
"linter": { "rules": { "nursery": { "noRestrictedImports": "off" } } }
"includes": ["**/src/broker/**"],
"linter": { "rules": { "style": { "noRestrictedImports": "off" } } }
}
]
}
20 changes: 10 additions & 10 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
},
"devDependencies": {
"@asyncapi/specs": "^6.11.1",
"@biomejs/biome": "^1.9.0",
"@biomejs/biome": "2.5.6",
"@hughescr/stryker-bun-runner": "^1.3.8",
"@stryker-mutator/core": "^9.6.1",
"@types/react": "^19.1.0",
Expand Down
2 changes: 1 addition & 1 deletion src/cli/boot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import { compose } from "#src/compose/index.ts";
import { loadServices } from "#src/config/index.ts";
import {
GitRefResolver,
StaticManifestSource,
resolveServices,
StaticManifestSource,
writeLockfile,
} from "#src/ingestion/index.ts";
import type { Config, SpecInfo, SpecRegistry } from "#src/model/index.ts";
Expand Down
2 changes: 1 addition & 1 deletion src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import type {
import { DEFAULT_CONFIG } from "#src/model/index.ts";
import { buildRegistry } from "#src/registry/index.ts";
import type { Api } from "./client.ts";
import { CliError, api, resolveCtrlPort } from "./client.ts";
import { api, CliError, resolveCtrlPort } from "./client.ts";
import type { CheckStatus, DoctorCtx } from "./doctor.ts";
import { runDoctor } from "./doctor.ts";
import {
Expand Down
6 changes: 5 additions & 1 deletion src/control-plane/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,11 @@ export function diagnosticSummary(diags: Diagnostic[]): DiagnosticSummary {
return summary;
}

export function createServer(config: Config, caps: ControlPlaneCaps) {
// `_config` is unused today. Kept (underscored) rather than dropped because
// removing it changes an exported function's arity, which is a refactor and not
// part of a dependency bump — biome 2's noUnusedFunctionParameters is what
// surfaced it (D-023).
export function createServer(_config: Config, caps: ControlPlaneCaps) {
const app = new Hono();

// --- reads ---
Expand Down
2 changes: 1 addition & 1 deletion src/engine/dispatch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import type { Handler, SpecRegistry } from "#src/model/index.ts";
import {
type Registration,
createDispatchRegistry,
defaultDispatch,
precedence,
type Registration,
} from "./dispatch.ts";

// [utest->R-012]
Expand Down
2 changes: 1 addition & 1 deletion src/engine/faker.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { generate as jsfGenerate } from "json-schema-faker";
import type { JsonSchema } from "json-schema-faker";
import { generate as jsfGenerate } from "json-schema-faker";
import type { Channel, Config, Faker, Violation } from "#src/model/index.ts";
import { hashToInt } from "./prng.ts";

Expand Down
10 changes: 6 additions & 4 deletions src/engine/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import type {
SpecRegistry,
Violation,
} from "#src/model/index.ts";
import { createDispatchRegistry } from "./dispatch.ts";
import type { DispatchRegistry } from "./dispatch.ts";
import { createDispatchRegistry } from "./dispatch.ts";
import { createEngine } from "./index.ts";
import { hashToInt, mulberry32 } from "./prng.ts";

Expand Down Expand Up @@ -220,9 +220,11 @@ test("proactive path: subscribe with no L3 handler falls to the L1 floor and emi
expect(emitted.length).toBe(1);
expect(emitted[0]?.topic).toBe("state/d7");
expect(emitted[0]?.retain).toBe(true);
expect(["ok", "warn"]).toContain(
(emitted[0]?.payload as { status: string }).status,
);
const status = (emitted[0]?.payload as { status: string } | undefined)
?.status;
// String() so an absent payload fails readably as "undefined" rather than
// failing to typecheck against toContain's string parameter
expect(["ok", "warn"]).toContain(String(status));
});

test("passive mode fires no ticks (F10)", async () => {
Expand Down
4 changes: 2 additions & 2 deletions src/engine/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ export function createEngine(deps: EngineDeps): Engine {
const m = reg.match(topic);
// initial state is a toClient concept — a subscribe on a fromClient
// channel gets nothing from the mock
if (!m || m.channel.direction !== "toClient") return;
if (m?.channel.direction !== "toClient") return;
if (Object.keys(m.params).length > 0)
instances.materialize(m.channel.topic, m.params);
const sel = dispatch.select(topic, reg);
Expand Down Expand Up @@ -243,7 +243,7 @@ export function createEngine(deps: EngineDeps): Engine {
for (const params of paramList) {
const topic = bindAddress(address, params);
const m = isWildcardFilter(topic) ? undefined : reg.match(topic);
if (!m || m.channel.direction !== "toClient") {
if (m?.channel.direction !== "toClient") {
stampViolation(
{
origin: "mock",
Expand Down
2 changes: 1 addition & 1 deletion src/engine/reset.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ test("R-014: reset re-instantiates factories — handler instance state does not
await engine.idle();
const last = emitted.at(-1);
// a surviving instance would emit n ≈ 3.x; a fresh one emits n ≈ 1.x
expect((last?.[1] as { n: number }).n).toBeLessThan(2);
expect((last?.[1] as { n: number } | undefined)?.n).toBeLessThan(2);
});

// [utest->R-014] [utest->R-032]
Expand Down
Loading