diff --git a/AGENTS.md b/AGENTS.md index 6354f4c..723a781 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 ` 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). diff --git a/DECISIONS.md b/DECISIONS.md index 28f6dc6..31ec6f4 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -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 diff --git a/biome.json b/biome.json index 72a2593..da0bf75 100644 --- a/biome.json +++ b/biome.json @@ -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": { @@ -36,8 +37,8 @@ } }, { - "include": ["src/broker/**"], - "linter": { "rules": { "nursery": { "noRestrictedImports": "off" } } } + "includes": ["**/src/broker/**"], + "linter": { "rules": { "style": { "noRestrictedImports": "off" } } } } ] } diff --git a/bun.lock b/bun.lock index 8ff7cef..19e3a58 100644 --- a/bun.lock +++ b/bun.lock @@ -17,7 +17,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", @@ -101,23 +101,23 @@ "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - "@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="], + "@biomejs/biome": ["@biomejs/biome@2.5.6", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.5.6", "@biomejs/cli-darwin-x64": "2.5.6", "@biomejs/cli-linux-arm64": "2.5.6", "@biomejs/cli-linux-arm64-musl": "2.5.6", "@biomejs/cli-linux-x64": "2.5.6", "@biomejs/cli-linux-x64-musl": "2.5.6", "@biomejs/cli-win32-arm64": "2.5.6", "@biomejs/cli-win32-x64": "2.5.6" }, "bin": { "biome": "bin/biome" } }, "sha512-lxVNjv7UF6KfhMJfL9gaUHbWdJdHbsAj6OSmwSYNdhRuG67NxNQ4Xdvh3TUxsSK9sBzJBQhEJj3AopmmNJ5pSA=="], - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@1.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw=="], + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.5.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zMOLZP4oMrjh6m1zcSj1ud2awUPgTuMVbmQhYYWL7J8HwCnbHHBvTm7VBTRuY7epT5bez76IpKYQ11ZAqHFlnw=="], - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@1.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg=="], + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.5.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-JAC1VqzvO7Th5ZplU0G2uGfkZbxEe9uDDektPAhF0JLusoz1w+T4okp2bkykI0bbaO2vslKiRfj4gU43JaGreA=="], - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g=="], + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.5.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-6XsYwCFkp5sMxl85ffhgeGpGgs6A7dRYFnkceZ7WVxvycuTnGdD5xa534Z3xfrBQ0JCMK/mujT6ZNPJoghedwg=="], - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA=="], + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.5.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-eUa3jeeYvfMt19LBeh6E5PUZpxnTC4JqNWo+EDjTtQjAr2xLGnWaxACtVU1DQqmHYbvThlJzLX+ZsYgrqh2qVw=="], - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg=="], + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.5.6", "", { "os": "linux", "cpu": "x64" }, "sha512-Pop9VXCFUhFTMfFefZ39S+u2rOPyNp5iHlxbZRwXGACHLy2r0jjiRgJHmaEKJzL3SyxlVeGShXhvvElvWowonA=="], - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg=="], + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.5.6", "", { "os": "linux", "cpu": "x64" }, "sha512-2Vp13QdKysH3HIWLaYLhUUwbK+jbZonJD1K+Lr0d0RO4wH7mkYd43vJixEDm8cUWrowoRz4UUHF1nm9Ae7ym8A=="], - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@1.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg=="], + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.5.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-tDGshcm6BdkZOCGnTDX0Y8/U4IfBSlnUU7T56nNDuPEfed+aHg+u8G36NB43fJVl0Os6+QURXIE1yuD7AaEofA=="], - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="], + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.5.6", "", { "os": "win32", "cpu": "x64" }, "sha512-WN05KwXnTO/2J45RQPvzZMXf7tZUIofHoR35xIPfCo7pQ2RFidxI8sfb5mGsaTxdMmEOzHzOPRCdA5/fCpc7xQ=="], "@hughescr/stryker-bun-runner": ["@hughescr/stryker-bun-runner@1.3.8", "", { "dependencies": { "@stryker-mutator/api": "9.6.1", "smol-toml": "1.7.0", "tinyglobby": "0.2.17", "ws": "8.21.0" }, "peerDependencies": { "@stryker-mutator/core": "^9.0.0" } }, "sha512-WxDLdHQW/ZxrvhapBjbHj1P+s/hARwiD6tlIP6w3vmDaSDHBk0jVFwChj0T5BdWlz/ZttTtcBWuHCkgRiQu5zg=="], diff --git a/package.json b/package.json index 63977a2..33dcbde 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/cli/boot.ts b/src/cli/boot.ts index 9645bfe..8fd2b54 100644 --- a/src/cli/boot.ts +++ b/src/cli/boot.ts @@ -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"; diff --git a/src/cli/index.ts b/src/cli/index.ts index 0efcee7..476cb0a 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -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 { diff --git a/src/control-plane/index.ts b/src/control-plane/index.ts index 19cd646..c9f86f8 100644 --- a/src/control-plane/index.ts +++ b/src/control-plane/index.ts @@ -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 --- diff --git a/src/engine/dispatch.test.ts b/src/engine/dispatch.test.ts index a80e056..4e6275d 100644 --- a/src/engine/dispatch.test.ts +++ b/src/engine/dispatch.test.ts @@ -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] diff --git a/src/engine/faker.ts b/src/engine/faker.ts index d67cef0..4e907c6 100644 --- a/src/engine/faker.ts +++ b/src/engine/faker.ts @@ -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"; diff --git a/src/engine/index.test.ts b/src/engine/index.test.ts index baba7cd..c54a7fa 100644 --- a/src/engine/index.test.ts +++ b/src/engine/index.test.ts @@ -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"; @@ -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 () => { diff --git a/src/engine/index.ts b/src/engine/index.ts index 0c9c72f..9426f0a 100644 --- a/src/engine/index.ts +++ b/src/engine/index.ts @@ -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); @@ -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", diff --git a/src/engine/reset.test.ts b/src/engine/reset.test.ts index d1d61ae..6a6d220 100644 --- a/src/engine/reset.test.ts +++ b/src/engine/reset.test.ts @@ -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] diff --git a/src/engine/scheduler.test.ts b/src/engine/scheduler.test.ts index 504fca8..c11563d 100644 --- a/src/engine/scheduler.test.ts +++ b/src/engine/scheduler.test.ts @@ -238,7 +238,7 @@ test("default reporter: a throwing task without onTaskError surfaces via console } expect(calls.length).toBe(1); expect(calls[0]?.[0]).toBe("[offbook] scheduler task failed:"); - expect((calls[0]?.[1] as Error).message).toBe("task boom"); + expect((calls[0]?.[1] as Error | undefined)?.message).toBe("task boom"); expect(s.pending()).toEqual({ scheduled: 0, settled: true }); }); diff --git a/src/ingestion/index.test.ts b/src/ingestion/index.test.ts index 2eef8c3..36494e1 100644 --- a/src/ingestion/index.test.ts +++ b/src/ingestion/index.test.ts @@ -4,9 +4,9 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { GitRefResolver, - StaticManifestSource, resolveRepoUrl, resolveServices, + StaticManifestSource, serializeLockfile, } from "./index.ts"; diff --git a/src/model/index.test.ts b/src/model/index.test.ts index 1bbf6b1..d89e2b3 100644 --- a/src/model/index.test.ts +++ b/src/model/index.test.ts @@ -1,5 +1,4 @@ import { expect, test } from "bun:test"; -import { DEFAULT_CONFIG } from "./index.ts"; import type { Channel, Config, @@ -35,6 +34,7 @@ import type { ViolationKind, WhenClause, } from "./index.ts"; +import { DEFAULT_CONFIG } from "./index.ts"; // R-001 exhaustiveness guard. Every contracts.md §1–6 type must be present + exported from model/, // with the single documented exception of BrokerModule (homed in broker/ per build-plan §2). The diff --git a/src/model/spec-version.test.ts b/src/model/spec-version.test.ts index 21b371d..b72887d 100644 --- a/src/model/spec-version.test.ts +++ b/src/model/spec-version.test.ts @@ -1,9 +1,9 @@ // [utest->R-037] import { expect, test } from "bun:test"; import { - SUPPORTED_SPEC_VERSIONS, isSupportedSpecVersion, readSpecVersion, + SUPPORTED_SPEC_VERSIONS, } from "./spec-version.ts"; test("reads the asyncapi version from spec text without a parser", () => { diff --git a/src/registry/index.ts b/src/registry/index.ts index cf4a978..a5a9faa 100644 --- a/src/registry/index.ts +++ b/src/registry/index.ts @@ -11,9 +11,9 @@ import type { SpecRegistry, } from "#src/model/index.ts"; import { - SUPPORTED_SPEC_VERSIONS, isSupportedSpecVersion, readSpecVersion, + SUPPORTED_SPEC_VERSIONS, } from "#src/model/spec-version.ts"; const parser = new Parser(); @@ -99,6 +99,7 @@ export const MQTT_OPERATION_KEYS = new Set([ // character-for-character from that key, so the drift test compares source // strings rather than approximating the intent. Reading only `properties` meant // a spec-legal `x-vendor-thing` was reported as an unknown key (D-019). +// biome-ignore lint/complexity/noUselessEscapeInRegex: the `\.` is redundant to the regex engine but NOT to this constant's purpose — `.source` is compared byte-for-byte against the upstream schema key in test/upstream-drift.test.ts, so unescaping it breaks the character-for-character transcription D-019 relies on (biome 2's "safe" fix does exactly that; D-023) export const MQTT_EXTENSION_KEY = /^x-[\w\d\.\x2d_]+$/; // MQTT 5 only, per the "MQTT Versions" column of the binding spec. offbook is diff --git a/src/scenarios/index.test.ts b/src/scenarios/index.test.ts index 6130247..daf60ff 100644 --- a/src/scenarios/index.test.ts +++ b/src/scenarios/index.test.ts @@ -20,7 +20,7 @@ import type { SpecRegistry, Violation, } from "#src/model/index.ts"; -import { type ScenarioRuntime, createScenarioRuntime } from "./index.ts"; +import { createScenarioRuntime, type ScenarioRuntime } from "./index.ts"; import { matchTopic } from "./matcher.ts"; const stateSchema = { @@ -213,9 +213,9 @@ describe("reactive dispatch (the l2 §0 running example)", () => { await s.engine.idle(); // only the topic-only fallback fired, once expect(s.emitted).toHaveLength(1); - expect((s.emitted[0]?.payload as Record).status).toBe( - "accepted", - ); + expect( + (s.emitted[0]?.payload as Record | undefined)?.status, + ).toBe("accepted"); s.emitted.length = 0; s.engine.onInbound(inbound("command/t1/set", { mode: "heat", target: 21 })); @@ -289,9 +289,9 @@ describe("trigger (POST /trigger seam)", () => { expect(r).toEqual({ name: "device-offline", stepCount: 1 }); await s.engine.idle(); expect(s.emitted[0]?.topic).toBe("state/t9"); - expect((s.emitted[0]?.payload as Record).status).toBe( - "offline", - ); + expect( + (s.emitted[0]?.payload as Record | undefined)?.status, + ).toBe("offline"); }); test("an unknown name is undefined and fires nothing", async () => { @@ -362,7 +362,7 @@ describe("hot-reload & passive freeze (l2 §8, G24)", () => { const s = await setup({ "50-counter.yaml": COUNTERIZED }); s.runtime.trigger("counterized"); await s.engine.idle(); - expect((s.emitted[0]?.payload as { n: number }).n).toBe(1); + expect((s.emitted[0]?.payload as { n: number } | undefined)?.n).toBe(1); writeFileSync( join(s.dir, "05-added.yaml"), @@ -380,7 +380,7 @@ describe("hot-reload & passive freeze (l2 §8, G24)", () => { s.runtime.trigger("counterized"); await s.engine.idle(); // swap touched definitions only — the per-scenario counter continued - expect((s.emitted[0]?.payload as { n: number }).n).toBe(2); + expect((s.emitted[0]?.payload as { n: number } | undefined)?.n).toBe(2); }); test("watch() hot-reloads on file change in autonomous mode", async () => { @@ -452,9 +452,9 @@ describe("emit-time recheck provenance (G10)", () => { await s.engine.idle(); // step 0 (templated target) dropped; step 1 (heating) still emitted expect(s.emitted).toHaveLength(1); - expect((s.emitted[0]?.payload as Record).status).toBe( - "heating", - ); + expect( + (s.emitted[0]?.payload as Record | undefined)?.status, + ).toBe("heating"); expect(s.violations).toHaveLength(1); const v = s.violations[0]; expect(v?.origin).toBe("mock"); diff --git a/src/scenarios/index.ts b/src/scenarios/index.ts index a1b909d..2dd0c94 100644 --- a/src/scenarios/index.ts +++ b/src/scenarios/index.ts @@ -23,7 +23,7 @@ import type { import { fillRequired } from "./fill.ts"; import { type LoadedScenario, loadScenarios } from "./loader.ts"; import { matchTopic, payloadMatches, resolvePath } from "./matcher.ts"; -import { OMIT, type TemplateRef, seededUuid, substitute } from "./template.ts"; +import { OMIT, seededUuid, substitute, type TemplateRef } from "./template.ts"; // What the runtime needs back from the engine — structurally satisfied by // Engine (engine/index.ts); the composition root wires the cycle via the diff --git a/src/scenarios/loader.ts b/src/scenarios/loader.ts index feb64a4..ac069ed 100644 --- a/src/scenarios/loader.ts +++ b/src/scenarios/loader.ts @@ -16,7 +16,7 @@ import type { } from "#src/model/index.ts"; import { fillRequired, schemaHasPath } from "./fill.ts"; import { comparePatterns, resolvePath } from "./matcher.ts"; -import { TEMPLATE_RE, parseRef, scanValue } from "./template.ts"; +import { parseRef, scanValue, TEMPLATE_RE } from "./template.ts"; export interface LoadedScenario { scenario: Scenario; diff --git a/src/scenarios/template.test.ts b/src/scenarios/template.test.ts index 1e9cbd1..b3bd547 100644 --- a/src/scenarios/template.test.ts +++ b/src/scenarios/template.test.ts @@ -6,11 +6,11 @@ import { describe, expect, test } from "bun:test"; import { hashToInt, mulberry32 } from "#src/engine/prng.ts"; import { OMIT, - type TemplateRef, parseRef, scanValue, seededUuid, substitute, + type TemplateRef, } from "./template.ts"; describe("parseRef", () => { diff --git a/test/ci-settlement.test.ts b/test/ci-settlement.test.ts index ddd111c..82f9e7d 100644 --- a/test/ci-settlement.test.ts +++ b/test/ci-settlement.test.ts @@ -102,7 +102,9 @@ test("CI flow (passive/fast-virtual): reset → publish → pending?wait → val state: StateEntry[]; }; const final = state.find((e) => e.topic === "state/thermostat-1"); - expect((final?.payload as { status: string }).status).toBe("heating"); + expect((final?.payload as { status: string } | undefined)?.status).toBe( + "heating", + ); // 4. the violation slice since the checkpoint: a clean publish produced // no client violations (the offbook check gate would pass) diff --git a/test/demo-app.test.ts b/test/demo-app.test.ts index 363b6fc..7a4cbbf 100644 --- a/test/demo-app.test.ts +++ b/test/demo-app.test.ts @@ -38,7 +38,7 @@ const SAMPLE_LOG = [ test("parseFingerprintLines: filters by clientId, groups by kind, survives junk", () => { const bundle = parseFingerprintLines(SAMPLE_LOG, "demo-app-x1"); expect(bundle?.connect?.protocolLevel).toBe(4); - expect((bundle?.connect?.ws as { path: string }).path).toBe("/"); + expect((bundle?.connect?.ws as { path: string } | undefined)?.path).toBe("/"); expect(bundle?.subscribes).toEqual([ { clientId: "demo-app-x1", topic: "state/#", qos: 1 }, ]); diff --git a/test/demo-serve.test.ts b/test/demo-serve.test.ts index be5c984..b86caa5 100644 --- a/test/demo-serve.test.ts +++ b/test/demo-serve.test.ts @@ -55,8 +55,10 @@ test("bootDemo composes the bundled spec + chain scenarios; a heat command chain state: StateEntry[]; }; const final = state.state.find((e) => e.topic === "state/thermostat-1"); - expect((final?.payload as { status: string }).status).toBe("heating"); - expect((final?.payload as { target: number }).target).toBe(23); + expect((final?.payload as { status: string } | undefined)?.status).toBe( + "heating", + ); + expect((final?.payload as { target: number } | undefined)?.target).toBe(23); } finally { await composed.stop(); } diff --git a/test/gate-determinism.test.ts b/test/gate-determinism.test.ts index f5b3d66..71d02fd 100644 --- a/test/gate-determinism.test.ts +++ b/test/gate-determinism.test.ts @@ -124,7 +124,9 @@ test("same seed ⇒ byte-identical F9 violation stream + final retained state ac expect(second.state).toEqual(first.state); // the chain's final word won both runs (ordering, not just membership) const final = first.state.find((e) => e.topic === "state/thermostat-1"); - expect((final?.payload as { status: string }).status).toBe("heating"); + expect((final?.payload as { status: string } | undefined)?.status).toBe( + "heating", + ); } finally { process.chdir(prevCwd); const leftover = await readRunfile(runDir); diff --git a/test/gate-validation.test.ts b/test/gate-validation.test.ts index 7785929..67c2500 100644 --- a/test/gate-validation.test.ts +++ b/test/gate-validation.test.ts @@ -129,7 +129,9 @@ test("qos-retain: the binding tier reaches the wire (qos 2 + retained); an off-t expect(flagged[0]?.errors?.[0]?.keyword).toBe("type"); // raw delivery even off-spec: the retained store now holds the bad payload const after = (await fx.state()).find((e) => e.topic === "presence/p-1"); - expect((after?.payload as { online: unknown }).online).toBe("yes"); + expect((after?.payload as { online: unknown } | undefined)?.online).toBe( + "yes", + ); }); test("qos-overrides: tier-2 topicOverrides beats the tier-3 per-service default on the wire (F14), schema bar intact", async () => { diff --git a/test/import-style.test.ts b/test/import-style.test.ts index 054bf09..2e669d8 100644 --- a/test/import-style.test.ts +++ b/test/import-style.test.ts @@ -1,5 +1,5 @@ import { expect, test } from "bun:test"; -import { readFileSync, readdirSync, statSync } from "node:fs"; +import { readdirSync, readFileSync, statSync } from "node:fs"; import { join } from "node:path"; function walk(dir: string): string[] { diff --git a/test/lint-gate.test.ts b/test/lint-gate.test.ts new file mode 100644 index 0000000..dfb4e8c --- /dev/null +++ b/test/lint-gate.test.ts @@ -0,0 +1,53 @@ +import { expect, test } from "bun:test"; +import { readFileSync } from "node:fs"; + +// Guards the biome config against the one way it is known to die quietly (D-023). +// Not tied to an R-### — like test/import-style.test.ts (D-013), this pins a +// decision, not a requirement. +// +// `biome migrate` rewrites `"recommended": true` (v1) as `"preset": "none"` +// (v2), which does not port the rule set — it deletes it. The failure is silent +// in the worst way: `bun run lint` still exits 0, CI stays green, and the repo +// lints nothing. Measured on the 1.9.4 -> 2.5.6 bump: a file containing `any`, +// `==` and an unused `var` drew zero diagnostics under the migrated config and +// three once `preset` was corrected. So the config is asserted here rather than +// trusted, because the symptom of it being wrong is that nothing complains. +const config = JSON.parse(readFileSync("biome.json", "utf8")) as { + linter: { enabled: boolean; rules: { preset: string } }; + overrides: Array<{ + includes: string[]; + linter: { rules: { style: { noRestrictedImports: unknown } } }; + }>; +}; + +test("the linter is enabled and the recommended rule set is actually on", () => { + expect(config.linter.enabled).toBe(true); + // "none" is what `biome migrate` writes, and it disables everything. + expect(config.linter.rules.preset).toBe("recommended"); +}); + +// The R-030 transport-isolation lint rule, asserted structurally so a config +// edit cannot drop it without a test failing. The regex gate in +// test/transport-isolation.test.ts is the independent second layer: that one +// catches an actual offending import, this one catches the rule going missing. +test("the transport-isolation rule is configured for src/ and exempted for src/broker/", () => { + const src = config.overrides.find((o) => o.includes.includes("**/src/**")); + const broker = config.overrides.find((o) => + o.includes.includes("**/src/broker/**"), + ); + expect(src).toBeDefined(); + expect(broker).toBeDefined(); + + const rule = src?.linter.rules.style.noRestrictedImports as { + level: string; + options: { paths: Record }; + }; + expect(rule.level).toBe("error"); + expect(Object.keys(rule.options.paths).sort()).toEqual([ + "aedes", + "aedes-server-factory", + ]); + + // broker/ is the one place allowed to import a transport package + expect(broker?.linter.rules.style.noRestrictedImports).toBe("off"); +}); diff --git a/test/spikes/jsf-fidelity.test.ts b/test/spikes/jsf-fidelity.test.ts index b09c314..befd72c 100644 --- a/test/spikes/jsf-fidelity.test.ts +++ b/test/spikes/jsf-fidelity.test.ts @@ -1,6 +1,6 @@ import { expect, test } from "bun:test"; import { readdirSync } from "node:fs"; -import { SPIKE_FIXTURES, measureFixture } from "#scripts/spike-jsf-fidelity.ts"; +import { measureFixture, SPIKE_FIXTURES } from "#scripts/spike-jsf-fidelity.ts"; // [stest->R-027] // R-027 tripwire: pins the measured per-fixture recheck-failure counts so a diff --git a/test/transport-isolation.test.ts b/test/transport-isolation.test.ts index 6cbce52..c2a72e1 100644 --- a/test/transport-isolation.test.ts +++ b/test/transport-isolation.test.ts @@ -5,7 +5,7 @@ // exact-name alternation below deliberately does not match it. // [stest->R-030] import { expect, test } from "bun:test"; -import { readFileSync, readdirSync, statSync } from "node:fs"; +import { readdirSync, readFileSync, statSync } from "node:fs"; import { join } from "node:path"; function walk(dir: string): string[] { diff --git a/test/upstream-drift.test.ts b/test/upstream-drift.test.ts index a2461de..f940728 100644 --- a/test/upstream-drift.test.ts +++ b/test/upstream-drift.test.ts @@ -9,7 +9,7 @@ // [stest->R-037] // [stest->R-039] import { expect, test } from "bun:test"; -import { existsSync, readFileSync, readdirSync, statSync } from "node:fs"; +import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; import { join } from "node:path"; import specs from "@asyncapi/specs"; import mqttOperationBinding from "@asyncapi/specs/bindings/mqtt/0.2.0/operation.json" with {