From c6c73dde0d09a4ff51e40b73a325b8b4b5d11656 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Sun, 25 Oct 2026 10:26:07 +0100 Subject: [PATCH 1/6] build(deps): Fallow moves from 3.6.0 to 3.35.0 --- package.json | 2 +- pnpm-lock.yaml | 85 ++++++++++++++++++++++++++++---------------------- 2 files changed, 49 insertions(+), 38 deletions(-) diff --git a/package.json b/package.json index 901240856..0d94b3bae 100644 --- a/package.json +++ b/package.json @@ -100,7 +100,7 @@ "@types/ws": "8.18.1", "@vitejs/plugin-react": "^6.1.1", "dependency-cruiser": "18.4.0", - "fallow": "^3.6.0", + "fallow": "^3.35.0", "lefthook": "^2.1.10", "oxfmt": "^0.59.0", "oxlint": "^1.74.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2feacdada..1c9399a89 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -290,8 +290,8 @@ importers: specifier: 18.4.0 version: 18.4.0 fallow: - specifier: ^3.6.0 - version: 3.6.0 + specifier: ^3.35.0 + version: 3.35.0 lefthook: specifier: ^2.1.10 version: 2.1.10 @@ -582,43 +582,43 @@ packages: resolution: {integrity: sha512-y20e+e6dFYkOUUJLVUZTsJRuTiXZaUQ32WD+R/ux/HBybbTx4ge7cNINcua0pU8+SNkKuRbOF12mBmzuzM8n5w==} engines: {node: '>= 14.17.0'} - '@fallow-cli/darwin-arm64@3.6.0': - resolution: {integrity: sha512-DnOEQQD7QnMs8mjasPkgzh9jpTbpoUb0jJE1nkV9ZL+x2DkFWSMKYuJ9rdgWdWx7OLKmwgtPMgW1wsRKG0ICLw==} + '@fallow-cli/darwin-arm64@3.35.0': + resolution: {integrity: sha512-8WJjjyQzX3DPD995516todTtFfagGTS0r9ACzNCqLBOcV/6ftnOxdGuNwwH3yz4g2ZKPrPDK0ml/NbJeUhN72g==} cpu: [arm64] os: [darwin] - '@fallow-cli/darwin-x64@3.6.0': - resolution: {integrity: sha512-5Gq8fuaFLZ86v2I5B2/L44Nty36AKdCkW48r2cziRtw54xS1uTpi2MYrmNS+as8gu1q4fb7Ei2mwAIz4F/nNtQ==} + '@fallow-cli/darwin-x64@3.35.0': + resolution: {integrity: sha512-Bkr69eim2UwmzzPWyyzD5lREBnnuBcAMLVxU8gIsTArGg3ab9wiNujqVi8KkhnuTDuyBL4HQ5sxumMNA1VfSgA==} cpu: [x64] os: [darwin] - '@fallow-cli/linux-arm64-gnu@3.6.0': - resolution: {integrity: sha512-AP8DdPxX4yLeC2BQh8rBNv466BswuY8s0zQHihcL5RAsc+C6NiAJR3XAMRHl2BOaIlxWeptPAUpcGLeCs4Or1Q==} + '@fallow-cli/linux-arm64-gnu@3.35.0': + resolution: {integrity: sha512-+B8lTPg0z8G5HaM+nSNQjxtLtW66ZVu9EOTrOzLn04YghjmZas1uxul+IkWBAFIOYhXFBvbtafMuETuKCaBUAg==} cpu: [arm64] os: [linux] - '@fallow-cli/linux-arm64-musl@3.6.0': - resolution: {integrity: sha512-Q9ecwQOECRwtFzuPwNmsxha2V/UsP/Y4vRKEogdgydawywzIHkM2t85C9D1YIK+VNl/bpQtLApLsndxxkpC1xQ==} + '@fallow-cli/linux-arm64-musl@3.35.0': + resolution: {integrity: sha512-aGk3tS+AMqiClIq2BgU/QCDHxAokUXtyDAmiAIRW05nWi6ghT/Kmyn85CMgAJA56DypYFz7Zujx/F5JYJ70yIA==} cpu: [arm64] os: [linux] - '@fallow-cli/linux-x64-gnu@3.6.0': - resolution: {integrity: sha512-ckRpZ0svhBGK67Vsg3E+SCDIJQsQ73ArGLfmma9PLz3ZxPoaw028dk8roQv9eShyZeirWTRyX3DppVh1Q4F7ag==} + '@fallow-cli/linux-x64-gnu@3.35.0': + resolution: {integrity: sha512-oUm4F8IvlP26IxLbIi51D40ewOr+4skkJXcU3XzjTOTEffLOXtrqbgCpdy8JLYqgIUAtFWeMzEbw4EU+rKA59Q==} cpu: [x64] os: [linux] - '@fallow-cli/linux-x64-musl@3.6.0': - resolution: {integrity: sha512-Saz6+KfzorXxujc2WTsKDxr+pOpm3h/sYqe8buAPG/fYfPGeu+YvQem7TzKlf4uB2TKWHl7V51dP7gehGFwF7g==} + '@fallow-cli/linux-x64-musl@3.35.0': + resolution: {integrity: sha512-6oyAyp2QbICW/xhCxSxvyfCWJDoVKGmPblFsfCb/h+11VSI/D1HyP8C8kWGS/rkMDgmJUlrUC8L0MVnfphbRLA==} cpu: [x64] os: [linux] - '@fallow-cli/win32-arm64-msvc@3.6.0': - resolution: {integrity: sha512-kcM1lgxU4tMcl48nP60srxvYHR5utvFycTpDPbM4PrSrtPJOWLghJwXpjsJWlLJkBEaUhAp7LzBQHFTvEFpM0g==} + '@fallow-cli/win32-arm64-msvc@3.35.0': + resolution: {integrity: sha512-ZMI/4/jn2GVtc9bF+ltvRmz2n53tP0eU59uKQrAfLWd92bEUKNEtuQgqHyuc/mmZKpouXsl+pWvN7UWRwGc9MA==} cpu: [arm64] os: [win32] - '@fallow-cli/win32-x64-msvc@3.6.0': - resolution: {integrity: sha512-piMKRxPWev6T4hUFlUJ2dD2sMm5Wc62jm4El/yY2SAmHiwychGcyzYsWh4qQg5r8zPWIkE8wIfXlWXsLKtU7nw==} + '@fallow-cli/win32-x64-msvc@3.35.0': + resolution: {integrity: sha512-oFK9jkBtg/b2+WiL3FBWHRDdDGcfIkPm9hVMTpDWr4OZ2vRCJUXOkGoOTc5N9j4RY9zleY5408TncJC4/xOe0w==} cpu: [x64] os: [win32] @@ -2222,8 +2222,13 @@ packages: resolution: {integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==} engines: {'0': node >=0.6.0} - fallow@3.6.0: - resolution: {integrity: sha512-CpoOw4Fs35NgF/Md+OTZE9PrsReQp3mbBEqDDd6HATh1gflXAQfDJOk3M8xA0cnIZIfXXAu5fwojxwTXn9S2MA==} + fallow-type-aware@3.35.0: + resolution: {integrity: sha512-NEZglAwuDDHsemMRySSxFrD36zYXerg+TVSfZofVj5PjcYpDPyS6T02CZZyWm0RWjuT6NKF1n8AIm0fFy7BykA==} + engines: {node: '>=20'} + hasBin: true + + fallow@3.35.0: + resolution: {integrity: sha512-LrP5DXpnsHyRMAv68eMV4BMmxfKCw60mYUMhsUZh3PafJtT+2JKNWfpQ0vJ0FrxBjIsTMUVXqFA+ytCQjLXN3Q==} engines: {node: '>=22'} hasBin: true @@ -4062,28 +4067,28 @@ snapshots: tough-cookie: 5.1.2 tunnel-agent: 0.6.0 - '@fallow-cli/darwin-arm64@3.6.0': + '@fallow-cli/darwin-arm64@3.35.0': optional: true - '@fallow-cli/darwin-x64@3.6.0': + '@fallow-cli/darwin-x64@3.35.0': optional: true - '@fallow-cli/linux-arm64-gnu@3.6.0': + '@fallow-cli/linux-arm64-gnu@3.35.0': optional: true - '@fallow-cli/linux-arm64-musl@3.6.0': + '@fallow-cli/linux-arm64-musl@3.35.0': optional: true - '@fallow-cli/linux-x64-gnu@3.6.0': + '@fallow-cli/linux-x64-gnu@3.35.0': optional: true - '@fallow-cli/linux-x64-musl@3.6.0': + '@fallow-cli/linux-x64-musl@3.35.0': optional: true - '@fallow-cli/win32-arm64-msvc@3.6.0': + '@fallow-cli/win32-arm64-msvc@3.35.0': optional: true - '@fallow-cli/win32-x64-msvc@3.6.0': + '@fallow-cli/win32-x64-msvc@3.35.0': optional: true '@floating-ui/core@1.8.0': @@ -5564,18 +5569,24 @@ snapshots: extsprintf@1.3.0: {} - fallow@3.6.0: + fallow-type-aware@3.35.0: + dependencies: + typescript: 7.0.2 + optional: true + + fallow@3.35.0: dependencies: detect-libc: 2.1.2 optionalDependencies: - '@fallow-cli/darwin-arm64': 3.6.0 - '@fallow-cli/darwin-x64': 3.6.0 - '@fallow-cli/linux-arm64-gnu': 3.6.0 - '@fallow-cli/linux-arm64-musl': 3.6.0 - '@fallow-cli/linux-x64-gnu': 3.6.0 - '@fallow-cli/linux-x64-musl': 3.6.0 - '@fallow-cli/win32-arm64-msvc': 3.6.0 - '@fallow-cli/win32-x64-msvc': 3.6.0 + '@fallow-cli/darwin-arm64': 3.35.0 + '@fallow-cli/darwin-x64': 3.35.0 + '@fallow-cli/linux-arm64-gnu': 3.35.0 + '@fallow-cli/linux-arm64-musl': 3.35.0 + '@fallow-cli/linux-x64-gnu': 3.35.0 + '@fallow-cli/linux-x64-musl': 3.35.0 + '@fallow-cli/win32-arm64-msvc': 3.35.0 + '@fallow-cli/win32-x64-msvc': 3.35.0 + fallow-type-aware: 3.35.0 fast-deep-equal@3.1.3: {} From 8a474e01fde4fe0bba2375276873f3de8574fcd7 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Sun, 25 Oct 2026 10:32:54 +0100 Subject: [PATCH 2/6] chore(fallow): tsc's arguments are no entry points, the device profiles are data tables, and the agent scripts' limits are cyclomatic 12 and cognitive 15 --- .fallowrc.json | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/.fallowrc.json b/.fallowrc.json index ee759eda4..a3f51def6 100644 --- a/.fallowrc.json +++ b/.fallowrc.json @@ -22,6 +22,10 @@ // website/ is its own package (its own package.json, lockfile and pnpm root), built by Astro // and deployed to GitHub Pages; nothing in the product imports it. "ignorePatterns": ["website/**"], + // why: tsc's file arguments in the package.json scripts are tsconfig files and the + // --tsBuildInfoFile caches under node_modules/.cache/tsc, never code that runs, so none is an + // entry point; without this Fallow warns on every run that it skipped the three caches. + "ignoreCommandEntries": ["tsc"], "entry": [ "src/cli/main.ts", "src/daemon/main.ts", @@ -80,7 +84,10 @@ // of that. The two are expected to diverge, not be merged; see fallow's own // guidance for code-duplication ("leave it duplicated when the similarity is // accidental and likely to diverge"). - "ignore": ["e2e/fake-driver/fake-driver.ts"] + // why: src/drivers/android/profiles/ holds one data table per emulator profile, the hw.* + // keys its AVD gets; the tables are meant to look alike, and sharing their common rows + // would hide which value each profile sets. + "ignore": ["e2e/fake-driver/fake-driver.ts", "src/drivers/android/profiles/**"] // Common additions (uncomment to enable): // "ignore": [ // "**/lib/**", // for repos that publish transpiled output to lib/ @@ -97,8 +104,10 @@ "thresholdOverrides": [ { "files": [".agents/scripts/*.mjs"], - "maxCrap": 100, - "reason": "Glue scripts for the delivery pipeline: git and transcript walks, option parsing, a Markdown shape check. They have no coverage pipeline, so CRAP is complexity squared; each is run against real PRs and transcripts when it changes, and none holds product logic." + "maxCyclomatic": 12, + "maxCognitive": 15, + "maxCrap": 156, + "reason": "Glue scripts for the delivery pipeline: git and transcript walks, option parsing, a Markdown shape check. Their tests spawn them rather than import them and there is no coverage pipeline, so Fallow counts 0% coverage and CRAP is cyclomatic squared plus cyclomatic. Cyclomatic 12 and cognitive 15 are the real limits; maxCrap 156 is the CRAP of a cyclomatic-12 function at 0% coverage (12 * 12 + 12), so CRAP never fails a function the cyclomatic ceiling passes." }, { "files": ["src/agent-device/host/agent-device-supervisor.test.ts"], From d7d55b8c9814cca0d769c42ecac29f41ab3b5fec Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Sun, 25 Oct 2026 10:36:35 +0100 Subject: [PATCH 3/6] chore(fallow): the 29 stale suppressions go, dead exports lose their export, the gateway's double drops cancelLease, and every suppression names its reason --- .fallowrc.json | 5 ++++- e2e/helpers/agent-device-targets.ts | 4 ++-- e2e/helpers/index.ts | 4 ++-- e2e/helpers/physical-presence.ts | 6 +++--- e2e/real-proxy/lane.ts | 4 ++-- e2e/real-proxy/ports.ts | 2 +- src/agent-device/gateway.ts | 1 - src/agent-device/host/host.ts | 1 + src/agent-device/host/upstream.ts | 2 -- src/agent-device/testing.ts | 1 - src/agent-device/worker.ts | 1 - src/core/component-installer.ts | 2 -- src/core/quarantine-coordinator.ts | 1 - src/core/reclaim-coordinator.ts | 1 - src/core/registry.ts | 1 - src/daemon/main.ts | 1 - src/gateway/index.ts | 5 +---- src/gateway/test-support.ts | 19 +++++++------------ src/leasing/acquisition-planner.ts | 1 - src/leasing/lease-lifecycle.ts | 2 -- src/leasing/lease-release-coordinator.ts | 4 ---- src/mcp/main.ts | 1 - src/mcp/test-support.ts | 2 -- src/ports/filesystem.ts | 1 - src/ports/host-info.ts | 1 - src/ports/service-manager.ts | 3 --- src/stream/gateway.ts | 1 - src/stream/worker.ts | 1 - 28 files changed, 23 insertions(+), 55 deletions(-) diff --git a/.fallowrc.json b/.fallowrc.json index a3f51def6..4263bccb0 100644 --- a/.fallowrc.json +++ b/.fallowrc.json @@ -98,7 +98,10 @@ }, "rules": { "unused-dependencies": "warn", - "stale-suppressions": "error" + "stale-suppressions": "error", + // why: a suppression is a claim that Fallow cannot see a use; the `-- ` says which + // use, so a reader can check it and a stale one can be judged. + "require-suppression-reason": "error" }, "health": { "thresholdOverrides": [ diff --git a/e2e/helpers/agent-device-targets.ts b/e2e/helpers/agent-device-targets.ts index 84274465b..5bf98abf7 100644 --- a/e2e/helpers/agent-device-targets.ts +++ b/e2e/helpers/agent-device-targets.ts @@ -57,14 +57,14 @@ export const ANDROID: Target = { ], }; -export interface DeviceRow { +interface DeviceRow { readonly id: string; readonly driverDeviceId: string; // why: the address agent-device names the device by (an iOS UDID, an Android adb serial); on Android it is not the driver's device id. readonly address: string; readonly state: string; } -export interface LeaseRow { +interface LeaseRow { readonly id: string; readonly deviceId: string; readonly ownerId: string; diff --git a/e2e/helpers/index.ts b/e2e/helpers/index.ts index 448d064bb..39f2b6391 100644 --- a/e2e/helpers/index.ts +++ b/e2e/helpers/index.ts @@ -22,7 +22,7 @@ export { DEVICE_READY_MS, waitFor } from "./wait.js"; export { events } from "./events.js"; export type { RecordedEvent } from "./events.js"; export { waitForDeviceState, waitForLeaseCount } from "./status.js"; -export { SCREEN_TAGS, readScreenFrames, type ScreenFrame } from "./screen-frames.js"; +export { SCREEN_TAGS, readScreenFrames } from "./screen-frames.js"; export { REPO_ROOT } from "./repo-root.js"; export type { CliResult } from "./cli.js"; export { @@ -41,4 +41,4 @@ export { waitForFree, warmUp, } from "./agent-device-targets.js"; -export type { DeviceRow, LeaseRow, RequestRow, Target } from "./agent-device-targets.js"; +export type { RequestRow, Target } from "./agent-device-targets.js"; diff --git a/e2e/helpers/physical-presence.ts b/e2e/helpers/physical-presence.ts index e9281508d..e7129861f 100644 --- a/e2e/helpers/physical-presence.ts +++ b/e2e/helpers/physical-presence.ts @@ -16,9 +16,9 @@ import { waitFor } from "./wait.js"; export const ENROLLED = ["com.example.mail", "com.example.maps"] as const; export const AGENT_APPS = ["com.agent.one", "com.agent.two"] as const; -export const RECLAIM_FAILURE: FakeDriverErrorSpec = { message: "device busy", type: "generic" }; +const RECLAIM_FAILURE: FakeDriverErrorSpec = { message: "device busy", type: "generic" }; -export const GRACE_HEALTH = { probeIntervalMs: 200, stableObservations: 2 }; +const GRACE_HEALTH = { probeIntervalMs: 200, stableObservations: 2 }; export type AbsentReason = "locked" | "untrusted" | "not-present"; @@ -69,7 +69,7 @@ function scriptedExtras(script: DeviceScript | undefined) { }; } -export function iosScript( +function iosScript( devices: readonly ScriptedSeed[], options: { readonly failReclaim?: boolean } = {}, ): FakeDriverPlatformScript { diff --git a/e2e/real-proxy/lane.ts b/e2e/real-proxy/lane.ts index ec284ffab..45a49f892 100644 --- a/e2e/real-proxy/lane.ts +++ b/e2e/real-proxy/lane.ts @@ -8,9 +8,9 @@ import { REPO_ROOT } from "../helpers/repo-root.js"; import { waitFor } from "../helpers/wait.js"; export const PROXY_URL = "http://127.0.0.1:4180"; -export const GATEWAY_PORT = 4700; +const GATEWAY_PORT = 4700; export const ISSUER = "http://127.0.0.1:5556/dex"; -export const CLIENT_ID = "simlock-real-proxy"; +const CLIENT_ID = "simlock-real-proxy"; export const ALICE = { email: "alice@example.test", password: "password" } as const; export interface LaneDaemon { diff --git a/e2e/real-proxy/ports.ts b/e2e/real-proxy/ports.ts index 09c7e7d84..f8dd62eec 100644 --- a/e2e/real-proxy/ports.ts +++ b/e2e/real-proxy/ports.ts @@ -1,6 +1,6 @@ import { createServer } from "node:net"; -export const LANE_PORTS = [4180, 5556, 4700] as const; +const LANE_PORTS = [4180, 5556, 4700] as const; function isFree(port: number): Promise { return new Promise((resolve) => { diff --git a/src/agent-device/gateway.ts b/src/agent-device/gateway.ts index 56adfa6ed..caf78d63b 100644 --- a/src/agent-device/gateway.ts +++ b/src/agent-device/gateway.ts @@ -123,7 +123,6 @@ function leaseFacts(ctx: Context, entry: FleetLease): FleetLeaseFacts { }; } -// fallow-ignore-next-line unused-export -- reached through the namespace of definition.ts's dynamic import. export function createGateway(ctx: Context): GatewayModule { const parts = createGatewayAgentDeviceFrontParts( fleetPort(ctx), diff --git a/src/agent-device/host/host.ts b/src/agent-device/host/host.ts index 0a2426b3f..909cf33ae 100644 --- a/src/agent-device/host/host.ts +++ b/src/agent-device/host/host.ts @@ -126,6 +126,7 @@ export class AgentDeviceHost { return identity === undefined ? null : { tenantId: identity.principal }; } + // fallow-ignore-next-line unused-class-member -- RoutedHostLink calls it as this.hosts[role].forward, a computed key Fallow does not follow. async forward(request: Request, identity: Identity): Promise { const status = await this.status(); if (status.state === "unavailable") throw new Error(status.reason); diff --git a/src/agent-device/host/upstream.ts b/src/agent-device/host/upstream.ts index f63cacd6d..b74689c5d 100644 --- a/src/agent-device/host/upstream.ts +++ b/src/agent-device/host/upstream.ts @@ -18,7 +18,6 @@ export interface AgentDeviceUpstream { export class SupervisorAgentDeviceUpstream implements AgentDeviceUpstream { constructor(readonly supervisor: { availability(): AgentDeviceAvailability }) {} - // fallow-ignore-next-line unused-class-member -- reached through the AgentDeviceUpstream port by the host. current(): AgentDeviceUpstreamState { const availability = this.supervisor.availability(); switch (availability.state) { @@ -37,7 +36,6 @@ export class SupervisorAgentDeviceUpstream implements AgentDeviceUpstream { } } - // fallow-ignore-next-line unused-class-member -- reached through the AgentDeviceUpstream port by the host. async fetch(request: Request): Promise { return globalThis.fetch(request); } diff --git a/src/agent-device/testing.ts b/src/agent-device/testing.ts index 6390eb35e..b65121a84 100644 --- a/src/agent-device/testing.ts +++ b/src/agent-device/testing.ts @@ -20,7 +20,6 @@ export class FakeAgentDeviceHealth implements AgentDeviceHealthProbe { this.#answer = { error }; } - // fallow-ignore-next-line unused-class-member -- reached through the AgentDeviceHealthProbe port by the supervisor. async read(baseUrl: string): Promise { this.reads.push(baseUrl); if (this.#answer === "never") return new Promise(() => undefined); diff --git a/src/agent-device/worker.ts b/src/agent-device/worker.ts index 73c871e30..d01b26adf 100644 --- a/src/agent-device/worker.ts +++ b/src/agent-device/worker.ts @@ -191,5 +191,4 @@ export function createWorkerWith( }; } -// fallow-ignore-next-line unused-export -- reached through the namespace of definition.ts's dynamic import. export const createWorker = createWorkerWith({}); diff --git a/src/core/component-installer.ts b/src/core/component-installer.ts index b252432ba..b7dda2662 100644 --- a/src/core/component-installer.ts +++ b/src/core/component-installer.ts @@ -301,7 +301,6 @@ export class ComponentInstaller { * reads the registry, and writes nothing. A driver whose listing rejects is left out and * logged, and the other platforms are still listed. */ - // fallow-ignore-next-line unused-class-member -- reached through the Dispatcher's `components` option, typed as a Pick of this class. async list(platform?: Platform): Promise { const listed = await Promise.all( this.options.drivers.select(platform).map(async (driver) => { @@ -466,7 +465,6 @@ export class ComponentInstaller { * Simlock device in `list` and in a removal's checks, so a removal cannot start under a * device the registry does not hold yet (ADR 0010 §8). */ - // fallow-ignore-next-line unused-class-member -- reached through DeviceProvisioner's `components` option, typed as a Pick of this class. claimProvision(spec: Pick): () => void { const key = componentKey(spec.platform, spec.osVersion); const removal = this.#removals.get(spec.platform); diff --git a/src/core/quarantine-coordinator.ts b/src/core/quarantine-coordinator.ts index c2223c1d0..86c44d276 100644 --- a/src/core/quarantine-coordinator.ts +++ b/src/core/quarantine-coordinator.ts @@ -77,7 +77,6 @@ export class QuarantineCoordinator { } // No `notifyAvailability`: a quarantined virtual device still counts as running, and a physical device counts toward no capacity, so no waiter can be satisfied by this. - // fallow-ignore-next-line unused-class-member -- called through ReclaimCoordinator's quarantine port. async enter(failure: QuarantinePurgeFailure): Promise { const nextRetryAt = this.options.clock.now() + this.options.config.retryBackoffMs; await this.options.decisions.run(async () => { diff --git a/src/core/reclaim-coordinator.ts b/src/core/reclaim-coordinator.ts index b1945e292..c81de8999 100644 --- a/src/core/reclaim-coordinator.ts +++ b/src/core/reclaim-coordinator.ts @@ -178,7 +178,6 @@ export class ReclaimCoordinator { return recovered; } - // fallow-ignore-next-line unused-class-member -- called through LeaseReleaseCoordinator's reclaim port. async reclaimReturned( deviceId: string, osVersion?: string, diff --git a/src/core/registry.ts b/src/core/registry.ts index 863ba306e..2eee5ea0d 100644 --- a/src/core/registry.ts +++ b/src/core/registry.ts @@ -910,7 +910,6 @@ export class Registry implements LeaseRequestStore { * opens: nothing in a new process drives a wait the old one started, so an open record from * before the restart would otherwise stay open with nothing to settle it. */ - // fallow-ignore-next-line unused-class-member -- called through LeaseStartup's registry port (LeaseStartupRegistry). async failOpenLeaseRequests( failure: LeaseRequestFailure, ): Promise { diff --git a/src/daemon/main.ts b/src/daemon/main.ts index a940cb99d..550a8a8bb 100644 --- a/src/daemon/main.ts +++ b/src/daemon/main.ts @@ -1198,7 +1198,6 @@ const PACKAGED_DEVICE_CAPTURE = fileURLToPath( new URL("../bin/simlock-device-capture", import.meta.url), ); const PACKAGED_SCRCPY_SERVER = fileURLToPath( - // fallow-ignore-next-line unresolved-import -- a file pnpm build copies into dist, not a module. new URL("../vendor/scrcpy/scrcpy-server-v5.0.1", import.meta.url), ); diff --git a/src/gateway/index.ts b/src/gateway/index.ts index 18de9f002..b9203811e 100644 --- a/src/gateway/index.ts +++ b/src/gateway/index.ts @@ -17,14 +17,11 @@ export { drainedWorkersOwner, StateDrainStore } from "./drain-store.js"; export type { DrainStore } from "./drain-store.js"; export { GatewayDispatcher } from "./dispatcher.js"; export { GatewayService, type GatewayServiceOptions } from "./service.js"; -export type { FleetViews } from "./fleet-ports.js"; -export type { WorkerRegistry } from "./worker-registry.js"; export type { WorkerDirectory } from "./fleet-ports.js"; -// fallow-ignore-next-line unused-type -- see FleetViews above. +// fallow-ignore-next-line unused-type -- public port shape: what a WorkerDirectory resolves a worker id to, for anyone supplying their own directory. export type { WorkerDispatchTarget } from "./fleet-ports.js"; export { FleetLeaseCoordinator } from "./fleet-coordinator.js"; export { FleetLeaseIndex } from "./lease-index.js"; -// fallow-ignore-next-line unused-type -- public record shape for FleetLeaseIndex's own consumers; no current caller outside src/gateway needs the type by name. export type { FleetLeaseEntry } from "./lease-index.js"; export { createRoutingPolicy } from "./routing.js"; // fallow-ignore-next-line unused-type -- public shape for a caller implementing its own routing policy; no current one exists outside routing.ts. diff --git a/src/gateway/test-support.ts b/src/gateway/test-support.ts index eeab6f2a5..abcede425 100644 --- a/src/gateway/test-support.ts +++ b/src/gateway/test-support.ts @@ -16,8 +16,6 @@ import type { ExecInput, ExecOptions, ExecOutput, - LeaseCancelInput, - LeaseCancelOutput, LeaseGrant, LeaseRecord, LeaseReleaseInput, @@ -465,6 +463,7 @@ export class ScriptedWorkerClient { return outcome.grant; } + // fallow-ignore-next-line unused-class-member -- reached through the GatewayWorkerClient that asClient() casts this double to. async renewLease(input: LeaseRenewInput): Promise { this.calls.push(`lease.renew:${input.leaseId}`); this.#throwIfFailing(); @@ -473,6 +472,7 @@ export class ScriptedWorkerClient { return outcome?.record ?? (leaseFixture(input.leaseId, "dev_1") as LeaseRecord); } + // fallow-ignore-next-line unused-class-member -- reached through the GatewayWorkerClient that asClient() casts this double to. async releaseLease(input: LeaseReleaseInput): Promise { this.calls.push(`lease.release:${input.leaseId}`); this.#throwIfFailing(); @@ -482,15 +482,7 @@ export class ScriptedWorkerClient { return { leaseId: input.leaseId }; } - async cancelLease(_input: LeaseCancelInput = {}): Promise { - this.calls.push("lease.cancel"); - this.#throwIfFailing(); - return { result: "cancelled" }; - } - - /** #118: `FleetLeaseCoordinator#exec` forwards here with the namespaced requester -- assert - * on `calls` (test: "device.exec forwarding sends the namespaced requesterId"). */ - // fallow-ignore-next-line complexity -- one scripted outcome per branch of a real exec. + // fallow-ignore-next-line -- one scripted outcome per branch of a real exec (complexity), reached through the GatewayWorkerClient that asClient() casts this double to (unused-class-member). async exec(input: ExecInput, options: ExecOptions = {}): Promise { this.calls.push(`device.exec:${input.requesterId ?? ""}`); this.#throwIfFailing(); @@ -504,7 +496,7 @@ export class ScriptedWorkerClient { return { exitCode: outcome?.exitCode ?? 0 }; } - /** ADR 0010 §7: the gateway's relay asks through this -- scripted by `installComponentHandler`. */ + // fallow-ignore-next-line unused-class-member -- reached through the GatewayWorkerClient that asClient() casts this double to. async installComponent( input: ComponentInstallInput, options: InstallComponentOptions = {}, @@ -514,6 +506,7 @@ export class ScriptedWorkerClient { return this.installComponentHandler(input, options); } + // fallow-ignore-next-line unused-class-member -- reached through the GatewayWorkerClient that asClient() casts this double to. async getCatalog(): Promise { this.calls.push("catalog.get"); if (this.hangingCalls.has("catalog.get")) return new Promise(() => {}); @@ -521,6 +514,7 @@ export class ScriptedWorkerClient { return this.catalog; } + // fallow-ignore-next-line unused-class-member -- reached through the GatewayWorkerClient that asClient() casts this double to. async getConfig(): Promise<{ readonly downloads: { readonly policy: DownloadPolicy; readonly timeoutMs: number }; readonly lease: { readonly maxTtlMs: number }; @@ -546,6 +540,7 @@ export class ScriptedWorkerClient { }; } + // fallow-ignore-next-line unused-class-member -- reached through the GatewayWorkerClient that asClient() casts this double to. async close(): Promise { this.closed = true; } diff --git a/src/leasing/acquisition-planner.ts b/src/leasing/acquisition-planner.ts index 1be60b5b4..bb93e2b69 100644 --- a/src/leasing/acquisition-planner.ts +++ b/src/leasing/acquisition-planner.ts @@ -148,7 +148,6 @@ export class AcquisitionPlanner { return this.#planProvision(input); } - // fallow-ignore-next-line unused-class-member -- called through LeaseAcquisitionCoordinator's `AcquisitionPlannerPort`. planPhysical(input: PhysicalPlanInput): AcquisitionPlan { let heldForOlder = false; for (const device of input.snapshot.devices) { diff --git a/src/leasing/lease-lifecycle.ts b/src/leasing/lease-lifecycle.ts index d428dec71..d95f30317 100644 --- a/src/leasing/lease-lifecycle.ts +++ b/src/leasing/lease-lifecycle.ts @@ -57,7 +57,6 @@ export interface LeaseLifecycleOptions { export class LeaseLifecycle { constructor(private readonly options: LeaseLifecycleOptions) {} - // fallow-ignore-next-line unused-class-member -- reached through LeaseAcquisitionCoordinator's leases port. async grant(input: { readonly deviceId: string; readonly ownerId: string; @@ -125,7 +124,6 @@ export class LeaseLifecycle { * The fallback to `defaultMs` is unreachable for a live lease -- it only covers a lease the * snapshot no longer has, whose `renewLease` below is about to throw `UnknownLeaseError`. */ - // fallow-ignore-next-line unused-class-member -- reached through LeaseReleaseCoordinator's lifecycle port. async renew(leaseId: string, ttlMs?: number): Promise { const current = this.options.registry.snapshot.leases.find((lease) => lease.id === leaseId); const effectiveTtlMs = ttlMs ?? current?.ttlMs ?? this.options.ttl.defaultMs; diff --git a/src/leasing/lease-release-coordinator.ts b/src/leasing/lease-release-coordinator.ts index 606492a0f..1fb233eb5 100644 --- a/src/leasing/lease-release-coordinator.ts +++ b/src/leasing/lease-release-coordinator.ts @@ -182,7 +182,6 @@ export class LeaseReleaseCoordinator } /** why: the reclaim joins the background reclaims, so nuke's `beginMaintenance` and a graceful stop wait for it. */ - // fallow-ignore-next-line unused-class-member -- called through PhysicalDeviceWatcher's releaser port. async reclaimReturned( deviceId: string, osVersion?: string, @@ -219,7 +218,6 @@ export class LeaseReleaseCoordinator ); } - // fallow-ignore-next-line unused-class-member -- called through LeaseMaintenance by NukeService. async beginMaintenance(): Promise { await this.options.decisions.run(() => { this.#maintenanceDepth += 1; @@ -234,7 +232,6 @@ export class LeaseReleaseCoordinator } } - // fallow-ignore-next-line unused-class-member -- called through LeaseMaintenance by NukeService. async releaseAllDuringMaintenance(reason: "killed"): Promise { await this.options.decisions.run(() => { if (this.#maintenanceDepth === 0) { @@ -247,7 +244,6 @@ export class LeaseReleaseCoordinator return this.#releaseAll(reason, "await"); } - // fallow-ignore-next-line unused-class-member -- called through LeaseMaintenance by NukeService. async endMaintenance(): Promise { const waiters = await this.options.decisions.run(() => { this.#maintenanceDepth = Math.max(0, this.#maintenanceDepth - 1); diff --git a/src/mcp/main.ts b/src/mcp/main.ts index d1f08199b..5beb0a719 100644 --- a/src/mcp/main.ts +++ b/src/mcp/main.ts @@ -71,7 +71,6 @@ export interface McpStdioRunner { * Connects the MCP server to stdio and remains pending until the connection closes. * The caller owns process exit status and any fatal diagnostic rendering. */ -// fallow-ignore-next-line unused-export -- CLI routing invokes this public frontend entrypoint. export async function runMcpStdio(environment: McpStdioEnvironment = {}): Promise { const runner = await startMcpStdio(environment); await runner.finished; diff --git a/src/mcp/test-support.ts b/src/mcp/test-support.ts index 98e2a6206..1f5846153 100644 --- a/src/mcp/test-support.ts +++ b/src/mcp/test-support.ts @@ -95,7 +95,6 @@ export class FakeSimlockClient implements SimlockClient { return this.#dead ? this.#deadConnection() : this.getCatalogImpl(input); } - // fallow-ignore-next-line unused-class-member -- part of the SimlockClient interface this fake implements; MCP exposes no identity tool of its own. identity(): Promise { this.calls.push({ input: undefined, method: "identity" }); return this.#dead ? this.#deadConnection() : Promise.reject(new Error("not implemented")); @@ -106,7 +105,6 @@ export class FakeSimlockClient implements SimlockClient { return this.#dead ? this.#deadConnection() : this.getStatusImpl(); } - // fallow-ignore-next-line unused-class-member -- part of the SimlockClient interface this fake implements; MCP exposes no component tool of its own. listComponents(input: ComponentListInput = {}): Promise { this.calls.push({ input, method: "listComponents" }); return this.#dead ? this.#deadConnection() : this.listComponentsImpl(input); diff --git a/src/ports/filesystem.ts b/src/ports/filesystem.ts index a529c5c27..28ee11282 100644 --- a/src/ports/filesystem.ts +++ b/src/ports/filesystem.ts @@ -111,7 +111,6 @@ export class NodeFilesystem implements Filesystem { return readFile(path, "utf8"); } - // fallow-ignore-next-line unused-class-member -- the updater reaches it through the Filesystem port. async copyFile(from: string, to: string): Promise { await copyFile(from, to); } diff --git a/src/ports/host-info.ts b/src/ports/host-info.ts index 40570ed32..f40bbe0ec 100644 --- a/src/ports/host-info.ts +++ b/src/ports/host-info.ts @@ -63,7 +63,6 @@ function swVersField(output: string, key: string): string | undefined { export class FakeHostInfo implements HostInfo { constructor(private readonly system: HostSystem) {} - // fallow-ignore-next-line unused-class-member -- reached through the HostInfo port by startDaemon. read(): Promise { return Promise.resolve(this.system); } diff --git a/src/ports/service-manager.ts b/src/ports/service-manager.ts index 5f62b39dc..8027e87a7 100644 --- a/src/ports/service-manager.ts +++ b/src/ports/service-manager.ts @@ -110,20 +110,17 @@ export class FakeServiceManager implements ServiceManager { return this.current; } - // fallow-ignore-next-line unused-class-member -- called through the `ServiceManager` port. async install(definition: ServiceDefinition): Promise { this.installs.push(definition); this.current = { state: "installed", path: definition.path }; await this.options.onInstall?.(definition); } - // fallow-ignore-next-line unused-class-member -- called through the `ServiceManager` port. async uninstall(definition: ServiceDefinition): Promise { this.uninstalls.push(definition); this.current = { state: "not-installed" }; } - // fallow-ignore-next-line unused-class-member -- called through the `ServiceManager` port. async start(definition: ServiceDefinition): Promise { this.starts.push(definition); await this.options.onStart?.(definition); diff --git a/src/stream/gateway.ts b/src/stream/gateway.ts index bd771b360..29e5d0be9 100644 --- a/src/stream/gateway.ts +++ b/src/stream/gateway.ts @@ -2,7 +2,6 @@ import type { GatewayContext, GatewayModule } from "../modules/index.js"; import { createGatewayStream } from "./create.js"; import type { StreamModuleConfig } from "./definition.js"; -// fallow-ignore-next-line unused-export -- reached through the namespace of definition.ts's dynamic import. export function createGateway(ctx: GatewayContext): GatewayModule { const stream = createGatewayStream({ fleet: { diff --git a/src/stream/worker.ts b/src/stream/worker.ts index 5aa026441..209b063ec 100644 --- a/src/stream/worker.ts +++ b/src/stream/worker.ts @@ -2,7 +2,6 @@ import type { WorkerContext, WorkerModule } from "../modules/index.js"; import { createWorkerStream } from "./create.js"; import type { StreamModuleConfig } from "./definition.js"; -// fallow-ignore-next-line unused-export -- reached through the namespace of definition.ts's dynamic import. export function createWorker(ctx: WorkerContext): WorkerModule { const stream = createWorkerStream({ clock: ctx.clock, From 1cb1ae822215f6f5fc65f37535fbd441ab392e08 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Sun, 25 Oct 2026 10:38:15 +0100 Subject: [PATCH 4/6] ci: the Fallow step prints its findings as annotations on the PR, not SARIF in the log --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8af586ecb..039718d6d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -258,7 +258,7 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile --ignore-scripts - name: Fallow analysis - run: pnpm fallow --ci + run: pnpm fallow --ci --format github-annotations # why: checked on the PR's merge commit, so a PR whose bump main has already taken (two PRs # moving 34 to 35) fails here although its own branch passes. From 74037d7ccb20e3f54592529fe3833cf39cce943a Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Sun, 25 Oct 2026 10:38:38 +0100 Subject: [PATCH 5/6] docs(internal): the toolchain says what the push hook's and CI's Fallow checks fail on --- docs/internal/agent-rules/toolchain.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/internal/agent-rules/toolchain.md b/docs/internal/agent-rules/toolchain.md index 1ec3a77e7..f40ba2a7d 100644 --- a/docs/internal/agent-rules/toolchain.md +++ b/docs/internal/agent-rules/toolchain.md @@ -45,8 +45,14 @@ list and this one the same. (`.agents/scripts/commit-msg-check.mjs`) refuses a subject on `/` that is not `(): (#)`, and on `bug/-repro` one that is not `test: reproduce # — `. -- **Push hook:** Fallow, then the e2e typecheck. -- **CI:** what `pnpm check` runs, and Fallow, on every push. The `pnpm +- **Push hook:** Fallow's audit (the findings the branch adds against + `origin/main`), then the e2e typecheck. +- **CI:** what `pnpm check` runs, and Fallow, on every push. CI's Fallow + (`fallow --ci`) checks the whole tree and fails on any dead-code finding + or clone group, `warn` rules included; complexity fails above the + `.fallowrc.json` limits, and refactoring targets never fail. Its findings + show as annotations on the PR. A `fallow-ignore` comment names its reason + after ` -- `, and one that no longer hides a finding fails. The `pnpm check` steps run as parallel jobs (static checks, unit tests, the timing tests, the e2e lane in three shards); the Quality check passes only when all of them do. The console lane runs in four shards From 7bc4e14aa48e389bc239eb9c631f222255e07a87 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Sun, 25 Oct 2026 10:43:37 +0100 Subject: [PATCH 6/6] test: the unit project gives the process-spawning script tests twenty seconds, since a loaded run times three of them out --- vitest.config.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/vitest.config.ts b/vitest.config.ts index 6f8e0fa98..8e6d2cdec 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -21,6 +21,10 @@ export default defineConfig({ // why: the timing files measure throughput and latency of GiB transfers: they run one // at a time (`pnpm test:timing`) so another file's load cannot move a number. exclude: ["**/node_modules/**", "src/**/*.timing.test.ts"], + // why: the script tests under src/ spawn node, git and a fake gh per case; under a full + // parallel run the default five seconds times three of them out (lint-rules, claim, + // mutate) while each passes alone in under a second. A hang still fails within 20 s. + testTimeout: 20_000, }, }, {