diff --git a/AGENTS.md b/AGENTS.md index 315b287ec6..fc5bdde8f5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -970,7 +970,7 @@ rate, so the cost stays bounded to the source files you actually changed. Run branch that changes `src/` files; the standard `deno task precommit` no longer runs it (it was too slow for every commit). Known-equivalent survivors recorded in -`scripts/mutation/equivalent-mutants.txt` are suppressed, as with a manual run. +`scripts/mutation/equivalent-mutants/` are suppressed, as with a manual run. Never record `=== → ==`/`!== → !=` mutants: Biome's `noDoubleEquals` rule is configured to reject loose comparisons even against `null`, and the runner counts that lint failure as killed before tests run. Use @@ -980,8 +980,8 @@ now kill. The audit never runs tests and refuses to rewrite stale or malformed entries. Like `deno task mutation`, it works in a copy of the checkout under `.mutation-runs/`, so the live source files are never left mutated and a commit made while it runs cannot pick up a mutant. With `--write`, the pruned -`equivalent-mutants.txt` is copied back when the run ends — unless that file was -edited meanwhile, which fails the run instead of overwriting the edit. +`equivalent-mutants/` registry files are copied back when the run ends — unless +one was edited meanwhile, which fails the run instead of overwriting the edit. Before it runs the mapped tests, the runner puts every mutant through two cheap **static gates**, ordered cheapest-first: a per-file Biome **lint** and then a @@ -998,7 +998,7 @@ trusted after the runner confirms the *unmutated* target passes it (the baseline probe): a standalone `deno task mutation` doesn't run `lint:ci`/`typecheck` first, so if the target isn't already clean the run aborts loudly rather than scoring a bogus 100%. This means a mutant recorded in -`equivalent-mutants.txt` must be one that survives *both* gates *and* the tests; +`equivalent-mutants/` must be one that survives *both* gates *and* the tests; a mutation that produces a type error never reaches the ignore-list because the type-check gate kills it first. @@ -1008,7 +1008,7 @@ to fix. Never determine whether a survivor "predates main" (no `git stash`, no diffing against the base to excuse it): the bar is 100%, and a survivor on a line in your changed file is a real gap in that file's tests that you are now the person best placed to close. Either write the assertion that kills it, or -record the mutant in `scripts/mutation/equivalent-mutants.txt` with a proof that +record the mutant in `scripts/mutation/equivalent-mutants/` with a proof that no input can distinguish it. "It was already there" is not a resolution; leaving it just guarantees the next person trips over the same survivor. This is the [Good citizen](#preferences) rule applied to mutation testing. diff --git a/TODO.md b/TODO.md index 3d2026d6c4..94698e88ef 100644 --- a/TODO.md +++ b/TODO.md @@ -1225,43 +1225,6 @@ proves they create the attendee and ledger rows exactly once. --- -## Stale equivalent-mutant line numbers across recent refactors - -*Origin: running `deno task mutation:audit-equivalents` while hardening -`src/shared/square.ts` for the confirmed-Square-refunds job.* - -`scripts/mutation/equivalent-mutants.txt` carries several entries whose -`file:line:col` no longer points at a generated mutant, so -`deno task mutation:audit-equivalents` aborts with "No generated mutant matches". -The mutants are still real and equivalent; only the code moved. Confirmed stale -entries span at least: -- `src/shared/uptime-kuma/socket.ts` (lines 107, 130, 167, 187 — `===`/`!==`↔loose) -- `src/shared/uptime-kuma/monitors.ts` (lines 87, 101, 116, 119, 138, 193, 227, - 250, 306, 325 — `===`/`!==`↔loose and `1000→1001`) -- `src/shared/scheduled-access.ts:16:20 ??→||` -- `src/shared/storage.ts:376:18 application/octet-stream→""` -- `src/ui/templates/admin/images.tsx:84:28` and `:85:20 ??→||` -- `src/features/app/routes.ts:235:14 →"mutated"` - -These most likely drifted when the Uptime Kuma modules were split into -one-concept-per-file (#1906) and other recent move/refactor PRs. The audit is a -standalone task (`mutation:audit-equivalents`, not part of `deno task -precommit`), so CI doesn't gate on it; it surfaced here only because the -Square-refunds job ran exhaustive mutation and used the audit to validate its -own equivalent entries. The `square.ts` entries in the equivalent-mutants -registry have been refreshed in place (they drift as the source shifts lines — -the audit command's own output lists every stale `file:line:col`); - -Fix: for each stale entry, re-run `deno task mutation '' ---exhaustive`, locate the surviving equivalent mutant's current `file:line:col` -from the report, and update the line/col in `equivalent-mutants.txt` (or remove -the entry if the static gates now kill it — `mutation:audit-equivalents --write` -does this automatically for entries the lint/type-check gates catch). Then -re-run the audit until it reports no stale entries. Starting point: the audit's -own output lists every stale `file:line:col`. - ---- - ## Square PENDING refunds — propagate a pending result, not a plain false *Origin: Codex review of PR #1911 (confirmed Square refund outcomes), thread @@ -1334,7 +1297,7 @@ a malformed response throws rather than being silently cast. Starting point: Direct tests at `test/features/api/folded-booking.test.ts` and `test/features/api/folded-booking/parent-booking.test.ts` kill every non-equivalent mutant on the unchanged `folded-booking.ts`. Five equivalents (lines 87, 118, 176, 301, 381) are recorded in -`scripts/mutation/equivalent-mutants.txt` with proofs — no unsuppressed survivors remain. +`scripts/mutation/equivalent-mutants/` with proofs — no unsuppressed survivors remain. ## Split `render-selector.test.ts` by what each case actually checks diff --git a/deno.json b/deno.json index e83f79c36b..6f3bac9c08 100644 --- a/deno.json +++ b/deno.json @@ -128,6 +128,7 @@ "@std/assert": "jsr:@std/assert@^1.0.19", "@std/cli": "jsr:@std/cli@^1.0.30", "@std/collections": "jsr:@std/collections@^1.2.0", + "@std/csv": "jsr:@std/csv@^1.0.6", "@std/expect": "jsr:@std/expect@^1.0.18", "@std/testing": "jsr:@std/testing@^1.0.17", "@std/path": "jsr:@std/path@^1.1.4" diff --git a/deno.lock b/deno.lock index e5eb732f30..c7be029894 100644 --- a/deno.lock +++ b/deno.lock @@ -8,9 +8,11 @@ "jsr:@std/async@0.224": "0.224.2", "jsr:@std/async@^1.1.0": "1.2.0", "jsr:@std/bytes@^1.0.2": "1.0.6", + "jsr:@std/bytes@^1.0.6": "1.0.6", "jsr:@std/cli@^1.0.30": "1.0.30", "jsr:@std/collections@^1.1.3": "1.2.0", "jsr:@std/collections@^1.2.0": "1.2.0", + "jsr:@std/csv@^1.0.6": "1.0.6", "jsr:@std/data-structures@^1.0.10": "1.0.10", "jsr:@std/dotenv@~0.225.7": "0.225.7", "jsr:@std/encoding@1": "1.0.10", @@ -31,6 +33,7 @@ "jsr:@std/path@^1.0.6": "1.1.5", "jsr:@std/path@^1.1.4": "1.1.5", "jsr:@std/path@^1.1.5": "1.1.5", + "jsr:@std/streams@^1.0.9": "1.1.1", "jsr:@std/streams@^1.1.1": "1.1.1", "jsr:@std/testing@^1.0.17": "1.0.17", "jsr:@std/yaml@1": "1.1.1", @@ -109,7 +112,7 @@ "@luca/esbuild-deno-loader@0.11.1": { "integrity": "dc020d16d75b591f679f6b9288b10f38bdb4f24345edb2f5732affa1d9885267", "dependencies": [ - "jsr:@std/bytes", + "jsr:@std/bytes@^1.0.2", "jsr:@std/encoding@^1.0.5", "jsr:@std/path@^1.0.6" ] @@ -142,6 +145,12 @@ "@std/collections@1.2.0": { "integrity": "47627a21d3a13138b77fd0e4d790ba9d2e603c3510b686cde6b132fe9aa98a88" }, + "@std/csv@1.0.6": { + "integrity": "52ef0e62799a0028d278fa04762f17f9bd263fad9a8e7f98c14fbd371d62d9fd", + "dependencies": [ + "jsr:@std/streams@^1.0.9" + ] + }, "@std/data-structures@1.0.10": { "integrity": "f574f86b0e07c69b9edc555fcc814b57d29258bad39fd5a34ba8a80ecf033cfe" }, @@ -189,7 +198,7 @@ "jsr:@std/media-types", "jsr:@std/net", "jsr:@std/path@^1.1.5", - "jsr:@std/streams" + "jsr:@std/streams@^1.1.1" ] }, "@std/internal@1.0.12": { @@ -223,7 +232,10 @@ ] }, "@std/streams@1.1.1": { - "integrity": "92556d350e537e9dce527a6d08f6f15be3ff65e656079dea69d15252187c7613" + "integrity": "92556d350e537e9dce527a6d08f6f15be3ff65e656079dea69d15252187c7613", + "dependencies": [ + "jsr:@std/bytes@^1.0.6" + ] }, "@std/testing@1.0.17": { "integrity": "87bdc2700fa98249d48a17cd72413352d3d3680dcfbdb64947fd0982d6bbf681", @@ -2399,6 +2411,7 @@ "jsr:@std/assert@^1.0.19", "jsr:@std/cli@^1.0.30", "jsr:@std/collections@^1.2.0", + "jsr:@std/csv@^1.0.6", "jsr:@std/dotenv@~0.225.7", "jsr:@std/expect@^1.0.18", "jsr:@std/http@^1.1.1", diff --git a/e2e-payments/src/flow.ts b/e2e-payments/src/flow.ts index f5996e9bc2..06c53eb6d7 100644 --- a/e2e-payments/src/flow.ts +++ b/e2e-payments/src/flow.ts @@ -32,7 +32,7 @@ export const runSetup = async ( await session.fill("admin_password_confirm", config.adminPassword); await session.select("country", country); await session.check("accept_agreement"); - await session.clickButton("Complete Setup"); + await session.clickButton("Complete setup"); log(` setup complete (admin=${config.adminUsername}, country=${country})`); }; diff --git a/scripts/audit-equivalent-mutants.ts b/scripts/audit-equivalent-mutants.ts index abe3508be9..df55b1509c 100644 --- a/scripts/audit-equivalent-mutants.ts +++ b/scripts/audit-equivalent-mutants.ts @@ -1,9 +1,11 @@ #!/usr/bin/env -S deno run --allow-all +import { relative, resolve } from "@std/path"; +import { splitFlagValues } from "#scripts/flag-values.ts"; import { auditEquivalentMutants } from "#scripts/mutation/equivalent-audit.ts"; import { - EQUIVALENT_MUTANTS_FILE, - EQUIVALENT_MUTANTS_PATH, + listRegistryFiles, + registryFilePath, } from "#scripts/mutation/ignore.ts"; import { runInSnapshot } from "#scripts/mutation/isolation.ts"; import { @@ -21,28 +23,42 @@ const usage = `Usage: deno task mutation:audit-equivalents [--write] Runs lint and type-check against every recorded equivalent mutant without running tests. Pass --write to remove entries that static checks now kill.`; -const parseOptions = (args: string[]): { write: boolean } => { - if (args.length === 0) return { write: false }; - if (args.length === 1 && args[0] === "--write") { - return { write: true }; - } - if (args.length === 1 && ["-h", "--help"].includes(args[0]!)) { +/** The audit's options. The registry list is internal plumbing: the parent + * enumerates the shards once and hands the same list to its snapshot child, + * so the audited files and the copied-back files can never differ. */ +interface AuditOptions { + registry: string[]; + write: boolean; +} + +const parseOptions = (args: string[]): AuditOptions => { + const { rest, values } = splitFlagValues(args, "--registry"); + const registry = values.map((path) => { + if (!path || path.startsWith("--")) { + throw new Error(`--registry needs a path\n\n${usage}`); + } + return path; + }); + if (rest.length === 1 && ["-h", "--help"].includes(rest[0]!)) { console.log(usage); Deno.exit(0); } - throw new Error(`Unknown arguments: ${args.join(" ")}\n\n${usage}`); + if (rest.length > 1 || (rest.length === 1 && rest[0] !== "--write")) { + throw new Error(`Unknown arguments: ${rest.join(" ")}\n\n${usage}`); + } + return { registry, write: rest[0] === "--write" }; }; /** Audit inside this checkout, which the snapshot child owns outright. */ -const runAudit = async (options: { write: boolean }): Promise => { +const runAudit = async (options: AuditOptions): Promise => { const controller = new AbortController(); const onSignal = (): void => controller.abort(); onTerminationSignals(onSignal); const result = await auditEquivalentMutants({ - ...options, - ignoreFile: EQUIVALENT_MUTANTS_FILE, + ignoreFiles: options.registry.map((path) => resolve(projectRoot, path)), root: projectRoot, signal: controller.signal, + write: options.write, }) .catch((error) => { if (controller.signal.aborted) Deno.exit(130); @@ -60,17 +76,33 @@ const runAudit = async (options: { write: boolean }): Promise => { return !options.write && result.retained !== result.checked ? 1 : 0; }; +/** Project-relative registry paths, listed before snapshotting so a --write + * audit can carry each pruned file back to the live checkout. */ +const registryCopyBackPaths = async (): Promise => + (await listRegistryFiles()).map((file) => + relative(projectRoot, registryFilePath(file)), + ); + if (import.meta.main) { // Parsed here too, so a bad flag or --help answers without copying anything. const options = parseOptions(Deno.args); + if (isSnapshotChild()) { + Deno.exit(await runSnapshotChild(() => runAudit(options))); + } + // The manifest is the parent's to build: a hand-passed file would be + // audited in the snapshot yet never copied back, so refuse it outright. + if (options.registry.length > 0) { + throw new Error(`--registry is internal to the audit\n\n${usage}`); + } + // One shard list serves the audit and the copy-back alike, so a shard added + // while the snapshot is being made can never be pruned and then lost. + const registry = await registryCopyBackPaths(); Deno.exit( - isSnapshotChild() - ? await runSnapshotChild(() => runAudit(options)) - : await runInSnapshot({ - args: Deno.args, - // Only a --write audit rewrites the list, so only it keeps a file. - copyBack: options.write ? [EQUIVALENT_MUTANTS_PATH] : [], - entryScript: "scripts/audit-equivalent-mutants.ts", - }), + await runInSnapshot({ + args: [...Deno.args, ...registry.flatMap((path) => ["--registry", path])], + // Only a --write audit rewrites the registry, so only it keeps files. + copyBack: options.write ? registry : [], + entryScript: "scripts/audit-equivalent-mutants.ts", + }), ); } diff --git a/scripts/flag-values.ts b/scripts/flag-values.ts new file mode 100644 index 0000000000..0069de4224 --- /dev/null +++ b/scripts/flag-values.ts @@ -0,0 +1,21 @@ +/** + * Splitting one repeated flag and its values out of a command line. + */ + +/** Split argv into the given flag's values and everything else. A flag at the + * end with nothing after it yields undefined, for the caller to refuse with + * its own words. */ +export const splitFlagValues = ( + args: string[], + flag: string, +): { rest: string[]; values: (string | undefined)[] } => { + const rest: string[] = []; + const values: (string | undefined)[] = []; + const remaining = [...args]; + while (remaining.length > 0) { + const value = remaining.shift()!; + if (value === flag) values.push(remaining.shift()); + else rest.push(value); + } + return { rest, values }; +}; diff --git a/scripts/mutation/equivalent-audit.ts b/scripts/mutation/equivalent-audit.ts index 43b0a9c493..f15cd75412 100644 --- a/scripts/mutation/equivalent-audit.ts +++ b/scripts/mutation/equivalent-audit.ts @@ -1,7 +1,12 @@ import { isAbsolute, relative, resolve, SEPARATOR } from "@std/path"; import { createStaticGates, type StaticGate } from "./execution.ts"; import { applyMutant, generateMutants, type Mutant } from "./generate.ts"; -import { mutantKeyForPath, parseIgnoreLine } from "./ignore.ts"; +import { + mutantKeyForPath, + parseIgnoreLine, + registryFilePath, +} from "./ignore.ts"; +import { writeWholeOrNotAtAll } from "./write-whole.ts"; interface PhysicalEntry { chunk: string; @@ -12,6 +17,8 @@ interface PhysicalEntry { lineNumber: number; newOperator: string; operator: string; + /** Which registry file the entry lives in, as an index into the file list. */ + registry: number; sourcePath: string; } @@ -21,8 +28,15 @@ interface ResolvedEntry extends PhysicalEntry { original: string; } +/** One registry file's text, split into prune-able line chunks. */ +interface RegistryFile { + chunks: string[]; + file: string | URL; + original: string; +} + export interface EquivalentAuditOptions { - ignoreFile: string | URL; + ignoreFiles: (string | URL)[]; root: string; signal?: AbortSignal; write: boolean; @@ -44,9 +58,12 @@ const realDeps: EquivalentAuditDeps = { createGates: createStaticGates }; const chunksOf = (text: string): string[] => text.match(/[^\n]*\n|[^\n]+$/g) ?? []; -const physicalEntries = (chunks: string[]): PhysicalEntry[] => { - const seen = new Set(); - return chunks.flatMap((chunk, index) => { +const physicalEntries = ( + registry: number, + chunks: string[], + seen: Set, +): PhysicalEntry[] => + chunks.flatMap((chunk, index) => { const line = chunk.replace(/\r?\n$/, ""); const parsed = parseIgnoreLine(line); if (!parsed) { @@ -58,9 +75,8 @@ const physicalEntries = (chunks: string[]): PhysicalEntry[] => { } seen.add(parsed.key); const { line: lineNumber, ...rest } = parsed; - return [{ chunk, index, line, lineNumber, ...rest }]; + return [{ chunk, index, line, lineNumber, registry, ...rest }]; }); -}; const sourceFile = (root: string, sourcePath: string): string => { if (isAbsolute(sourcePath)) { @@ -146,7 +162,8 @@ const failedGateFor = async ( }; interface AuditClassification extends EquivalentAuditResult { - killedIndexes: Set; + /** Killed entries as `${registry}:${index}` chunk coordinates. */ + killedChunks: Set; } const auditEntries = async ( @@ -164,39 +181,40 @@ const auditEntries = async ( const killedByLint: string[] = []; const killedByTypeCheck: string[] = []; - const killedIndexes = new Set(); + const killedChunks = new Set(); for (const entry of entries) { signal.throwIfAborted(); const failed = await failedGateFor(entry, gates, signal); if (!failed) continue; const target = failed === "lint" ? killedByLint : killedByTypeCheck; target.push(entry.line); - killedIndexes.add(entry.index); + killedChunks.add(`${entry.registry}:${entry.index}`); } return { checked: entries.length, killedByLint, killedByTypeCheck, - killedIndexes, - retained: entries.length - killedIndexes.size, + killedChunks, + retained: entries.length - killedChunks.size, }; }; const pruneKilledEntries = async ( - ignoreFile: string | URL, - originalIgnore: string, - chunks: string[], - killedIndexes: Set, + registries: RegistryFile[], + killedChunks: Set, ): Promise => { - if ((await Deno.readTextFile(ignoreFile)) !== originalIgnore) { - throw new Error("Equivalent-mutant file changed during the audit."); + for (const [registry, { chunks, file, original }] of registries.entries()) { + const kept = chunks + .map((chunk, index) => + killedChunks.has(`${registry}:${index}`) ? "" : chunk, + ) + .join(""); + if (kept === original) continue; + if ((await Deno.readTextFile(file)) !== original) { + throw new Error("Equivalent-mutant file changed during the audit."); + } + await writeWholeOrNotAtAll(registryFilePath(file), kept); } - await Deno.writeTextFile( - ignoreFile, - chunks - .map((chunk, index) => (killedIndexes.has(index) ? "" : chunk)) - .join(""), - ); }; /** Apply every listed equivalent and run only lint plus type-check. */ @@ -204,20 +222,25 @@ export const auditEquivalentMutants = async ( options: EquivalentAuditOptions, deps: EquivalentAuditDeps = realDeps, ): Promise => { - const { ignoreFile, root } = options; - const originalIgnore = await Deno.readTextFile(ignoreFile); - const chunks = chunksOf(originalIgnore); - const entries = await resolveEntries(root, physicalEntries(chunks)); + const { ignoreFiles, root } = options; + const registries: RegistryFile[] = await Promise.all( + ignoreFiles.map(async (file) => { + const original = await Deno.readTextFile(file); + return { chunks: chunksOf(original), file, original }; + }), + ); + const seen = new Set(); + const entries = await resolveEntries( + root, + registries.flatMap(({ chunks }, registry) => + physicalEntries(registry, chunks, seen), + ), + ); const gates = await deps.createGates(); const signal = options.signal ?? new AbortController().signal; const result = await auditEntries(entries, gates, signal); - if (options.write && result.killedIndexes.size > 0) { - await pruneKilledEntries( - ignoreFile, - originalIgnore, - chunks, - result.killedIndexes, - ); + if (options.write && result.killedChunks.size > 0) { + await pruneKilledEntries(registries, result.killedChunks); } return { checked: result.checked, diff --git a/scripts/mutation/equivalent-mutants.txt b/scripts/mutation/equivalent-mutants.txt deleted file mode 100644 index b6c3b09c16..0000000000 --- a/scripts/mutation/equivalent-mutants.txt +++ /dev/null @@ -1,996 +0,0 @@ -# Known-equivalent mutants — suppressed from the survivor count. -# -# A mutant is "equivalent" when NO possible input distinguishes it from the -# original, so no test could ever kill it. Recording it here lets the mutation -# tester gate CI on genuinely NEW survivors instead of re-reporting these. -# -# Equivalent means unkillable by *any* test — not merely unkilled by the ones -# that exist today. A survivor a stronger assertion could catch is a test gap, -# not an equivalent: fix the test instead of listing it here. Prefer entries -# whose equivalence is provable from types (e.g. `?? → ||` where the operand is -# never falsy-but-non-null) over ones that lean on a domain invariant. -# -# Format — copy a survivor line from the report and add a reason: -# :: → # why it is equivalent -# -# Do NOT list a mutant whose output the linter or type checker rejects. Biome's -# noDoubleEquals rule rejects every `=== → ==` and `!== → !=` mutant. The runner -# counts a static-check failure as killed, so those never survive to be recorded. - -src/shared/form-data.ts:16:33 ?? → || # trim returns a string, and its only falsy value is the empty string supplied by the fallback -src/shared/forms/validation.ts:101:46 = → += # this assignment runs only when trimmed is the empty string, so replacing it and appending to it produce the same defaultValue -src/features/admin/dashboard.ts:232:65 1 → 2 # The query needs any number above the 200-row display limit only to detect overflow; both limits produce the same truncated flag and the same first 200 displayed rows -src/shared/bearer.ts:7:55 ?? → || # the optional regex capture is non-empty because the capture uses +, so only undefined reaches the null fallback -src/shared/uptime-kuma/socket.ts:189:28 0 → -1 # the private map's missing set and retained empty set behave identically: once adds to either, while off and emit are no-ops for both -src/shared/uptime-kuma/matching.ts:58:25 1000 → 1001 # timeout is a positive whole number of seconds and the 25,000ms deadline lies between the same integer seconds under either multiplier - -src/features/admin/entity-pages.ts:306:28 ?? → || # opts.baseUrl is a string and the only falsy string is the empty fallback itself -src/features/admin/entity-pages.ts:307:24 ?? → || # opts.query is a URLSearchParams object when present, so it is always truthy -src/features/admin/site-content-page.ts:78:38 ?? → || # extraTabs is an array when present, and arrays are always truthy -src/ui/templates/admin/holidays.tsx:87:45 ?? → || # values is a render-values object when present, so it is always truthy -src/ui/client/admin/manual-checkin.ts:59:36 ?? → || # textContent is string|null; its only falsy string is the empty fallback itself -src/ui/client/admin/order-gallery.ts:105:34 ?? → || # states is a record object when present, so it is always truthy - -# `x ?? F` vs `x || F` differ only when x is falsy-but-not-null; these are all -# cases where x can't be such a value, or F equals the only one it can be. -src/shared/checkout-pricing.ts:324:41 ?? → || # intent.modifiers: an array is always truthy -src/shared/booking-lines.ts:71:42 ?? → || # packageGroupId is absent or a positive database group id; an explicit 0 also has the same 0 fallback -src/features/api/payment-processing/create.ts:63:36 ?? → || # lineGroupId returns undefined or a positive package group id; an explicit 0 would also keep the same 0 fallback -src/features/api/payment-processing/create.ts:200:28 ?? → || # listingAnswerIds is an object when present, and objects are always truthy -src/features/api/payment-processing/create.ts:203:67 ?? → || # a listing's text refs are an array when present, and arrays are always truthy -src/features/api/payment-processing/create.ts:206:46 ?? → || # a grouped answer set is an object when present, and objects are always truthy -src/features/api/payment-processing/create.ts:210:35 ?? → || # existing text answer ids are an array when present, and arrays are always truthy -src/shared/logistics-filter.ts:22:35 ?? → || # raw: string|null, only falsy string "" === fallback "" -src/shared/config.ts:170:39 ?? → || # getEnv(): string|undefined, only falsy string "" === fallback "" -src/shared/config.ts:36:5 ?? → || # providerValue returns boolean or null, and false stays false with either fallback operator -src/shared/config.ts:183:61 ?? → || # getEnv(): string|undefined, only falsy string "" === fallback "" -src/shared/config.ts:190:33 ?? → || # getEnv(): string|undefined, only falsy string "" === fallback "" -src/shared/config.ts:197:33 ?? → || # getEnv(): string|undefined, only falsy string "" === fallback "" -src/shared/qr-token.ts:60:18 ?? → || # input.date?: string, only falsy string "" === fallback "" -src/shared/qr-token.ts:62:18 ?? → || # input.name?: string, only falsy string "" === fallback "" -src/shared/app-forms.ts:73:18 ?? → || # config.auth is an AuthPolicy object when present, so it is always truthy -src/shared/site-assignment.ts:155:57 ?? → || # parseReadOnlyFromMs(): number|null, 0 ?? 0 === 0 || 0 -src/shared/site-assignment.ts:317:34 ?? → || # available[idx]: BuiltSite|undefined, and a BuiltSite object is always truthy -src/shared/site-assignment.ts:339:34 ?? → || # getEmailConfig(): EmailConfig|null -src/shared/site-assignment.ts:363:53 ?? → || # parseEmail(): ValidEmail|null, always truthy or null -src/shared/ledger/project.ts:20:41 ?? → || # allBalances: Map.get; the only falsy-non-null number is 0, and 0 ?? 0 === 0 || 0 -src/shared/listing-parents-rules.ts:126:4 ?? → || # find()?.error(): an i18n message is never "", so only undefined reaches the fallback either way -src/shared/ledger/project.ts:38:49 ?? → || # balanceOf: same Map.get fallback; 0 ?? 0 === 0 || 0 -src/shared/ledger/reconcile.ts:70:29 ?? → || # IDENTITY_FIELDS kind: string|undefined, only falsy-non-null is "" and "" ?? "" === "" || "" -src/shared/ledger/reconcile.ts:112:56 ?? → || # multisetDiff: Map.get count; 0 ?? 0 === 0 || 0 -src/shared/ledger/reconcile.ts:115:35 ?? → || # multisetDiff: Map.get count; 0 ?? 0 === 0 || 0 -src/shared/ledger/reconcile.ts:142:44 ?? → || # expected.get(): LegFingerprint[]|undefined — an array is always truthy -src/shared/ledger/reconcile.ts:143:43 ?? → || # observed.get(): LegFingerprint[]|undefined — an array is always truthy -src/shared/ledger/reverse.ts:51:18 ?? → || # reverseOf memo: string|undefined, only falsy-non-null is "" and "" ?? "" === "" || "" -src/shared/accounting/mappers.ts:55:20 ?? → || # REFUND_KIND[kind]: every value is non-empty and a miss is undefined — never "", so ?? and || coincide -src/shared/accounting/mappers.ts:270:32 ?? → || # refundKind(leg.kind ?? ""): string|undefined, only falsy-non-null is "" and "" ?? "" === "" || "" -src/shared/accounting/store.ts:202:60 ?? → || # planGroup existingByGroup.get(): Transfer[]|undefined — an array is always truthy -src/shared/accounting/store.ts:225:42 ?? → || # planGroup originalsById.get(): Transfer|undefined — a Transfer object is always truthy -src/shared/accounting/rows.ts:116:21 ?? → || # insertStatement kind: string|undefined, only falsy-non-null is "" and "" ?? "" === "" || "" -src/shared/accounting/rows.ts:117:21 ?? → || # insertStatement memo: string|undefined, only falsy-non-null is "" and "" ?? "" === "" || "" -src/shared/accounting/rows.ts:250:58 ?? → || # selectById (await …)[0]: Transfer|undefined — a Transfer object is always truthy -src/shared/accounting/rows.ts:136:72 ?? → || # renderInsert guard?.args: InValue[]|undefined — an array is always truthy -src/shared/accounting/manual-entries.ts:236:29 ?? → || # guard error message `transfer.kind ?? ""`: string|undefined, "" ?? "" === "" || "" -src/shared/checkout-ledger.ts:42:72 ?? → || # find(...)?.amount: number|undefined; the only falsy-non-null amount is 0 and 0 ?? 0 === 0 || 0 -src/features/settings-bundles.ts:247:40 ?? → || # bundle is a readonly string[] or undefined; every present array, including [], is truthy -src/shared/maintenance/runner.ts:183:5 0 → 1 # every non-empty run needs at least two startup calls, so both allowances skip it; an empty run makes no calls -src/shared/maintenance/runner.ts:190:5 0 → 1 # every non-empty run needs at least two database startup calls, so both allowances skip it; an empty run makes no calls -src/shared/maintenance/runner.ts:202:23 ?? → || # wakePolicy is a non-empty MaintenanceWakePolicy value or undefined, so only undefined reaches the fallback -src/shared/builder.ts:201:44 ?? → || # dbToken is string|undefined and the fallback is "", so its only falsy string already equals the fallback -src/shared/builder.ts:202:35 ?? → || # dbProvider is a non-empty provider value or undefined, so only undefined reaches the fallback -src/shared/builder.ts:207:36 ?? → || # dbProvider is a non-empty provider value or undefined, so only undefined reaches the fallback -src/shared/builder.ts:290:26 ?? → || # hostingProvider is a non-empty provider value or undefined, so only undefined reaches the fallback -src/features/url.ts:58:35 ?? → || # URLSearchParams.get returns null or a string; its only falsy string is already the empty fallback -src/shared/storage.ts:368:18 application/octet-stream → "" # the pinned Bunny SDK normalizes an empty contentType to application/octet-stream, as the request-level upload test proves -src/shared/site-build.ts:39:21 = → += # retainedId starts at 0 and buildSite invokes retain exactly once, so both assignments store row.id -src/shared/db/built-site-scheduler.ts:11:36 ?? → || # scheduledTaskKey is null or a non-empty canonical key, so both operators select the candidate only when no key exists -src/shared/deno-deploy-api.ts:139:30 ?? → || # failure_reason is null or a non-empty picklist value, so both use the status only for null -src/shared/bunny-cdn.ts:478:42 ?? → || # DefaultHostname is string|undefined; its only falsy string is already the same empty fallback -src/shared/bunny-cdn.ts:530:50 ?? → || # Secrets is an array|null, and every present array is truthy while null takes the fallback either way -src/shared/subrequest-budget.ts:38:40 ?? → || # scoped counts is an object when present, and every object is truthy -src/shared/accounting/queries.ts:166:26 || → && # transferActivityBounds: MIN(occurred_at) and MAX(occurred_at) over one table are NULL together (both iff the table is empty), so either-null and both-null coincide -src/shared/admin-features.ts:121:58 ?? → || # find(): AdminFeatureDefinition|undefined; a feature object is always truthy -src/shared/db/admin-features.ts:51:10 __required_setting_condition__ → "" # the placeholder key is never stored: a failed condition always aborts on its NULL value -src/shared/db/admin-features.ts:51:10 __required_setting_condition__ → "__required_setting_condition__ mutated" # same as above; no successful statement exposes this key -src/shared/db/admin-features.ts:77:49 ?? → || # inUseSql: a non-empty SQL string|undefined, so only undefined reaches the fallback -src/shared/db/migrations/schema/admin-feature-triggers.ts:160:35 ?? → || # source.columns is undefined or an array; every array, including [], is truthy -src/shared/square-provider.ts:55:48 ?? → || # amount is bigint|undefined, and its only falsy bigint is 0n, which equals the fallback -src/shared/square-provider.ts:56:51 ?? → || # refunded amount is bigint|undefined, and its only falsy bigint is 0n, which equals the fallback -src/shared/square-provider.ts:101:19 ?? → || # retrieveSession returns a session object or null, so every present value is truthy -src/shared/square-provider.ts:126:59 ?? → || # paymentId is string|undefined, and its only falsy string is the empty fallback itself - -# Collection cache generation values are only compared for equality. Their -# absolute value and direction do not matter: every invalidation changes the -# value, so a fetch started before it cannot populate the cache. The unloaded -# timestamp is never read because `items` is null, and size is an array length. -src/fp.ts:435:33 0 → 1 # initial time is replaced when items first load; null items short-circuit the TTL check -src/fp.ts:435:33 0 → -1 # same: the unloaded timestamp is never observed -src/fp.ts:437:20 0 → 1 # generation starts at an arbitrary value and is only compared with a captured copy -src/fp.ts:437:20 0 → -1 # same: only changes relative to the captured generation matter -src/fp.ts:452:17 ++ → -- # either direction changes generation and invalidates every older captured copy -src/fp.ts:455:49 ?? → || # items?.length is undefined or a non-negative integer; 0 and undefined both produce 0 - -# Table inputs make these operators equivalent: nullable query results exclude -# undefined, paired null checks already reject both nullish values, and optional -# conditions/dependencies are objects or arrays. -src/shared/db/table.ts:445:28 ?? → || # condition?.args is an array or undefined, and every array is truthy -src/shared/db/table.ts:492:71 ?? → || # a found row is a truthy object, and a miss is undefined, so both operators give the row or null -src/shared/db/table.ts:496:31 ?? → || # findByIds's element is a truthy row, null, or undefined, and all three agree -src/shared/db/table.ts:584:21 ?? → || # dependsOn is a readonly array or undefined, and every array is truthy -src/shared/db/table.ts:617:32 ?? → || # the cached table's dependsOn is the same array-or-undefined as above - -# Login attempt rows store an integer count. For undefined, null, zero, or any -# non-zero integer, `(attempts ?? 0) + 1` and `(attempts || 0) + 1` agree. -src/shared/db/login-attempts.ts:46:39 ?? → || -src/shared/db/users.ts:264:62 ?? → || # queryAll()[0] is a truthy UserAuthFields object or undefined, so both operators return the row or null -src/shared/db/query-log.ts:71:62 ?? → || # queryLogScope.current() is a truthy QueryLogState object or undefined -src/shared/db/query-log.ts:252:43 ?? → || # stored read counts start at 1 and remain positive, so only undefined reaches the zero fallback -src/shared/db/query-log.ts:292:22 ?? → || # store is a truthy QueryLogState object or undefined -src/shared/update.ts:153:45 ?? → || # the stored commit is a string or undefined and the fallback is empty, so both operators return the same string -src/shared/update.ts:186:11 ?? → || # the primary row value is a string or absent and the fallback is empty, so both operators return the same string - -# ticket-payment values whose present form is always truthy, whose falsy value -# equals the fallback, or whose mutated constant is normalized before use. -src/features/public/ticket-payment.ts:193:11 ?? → || # date is nullish or a non-empty ISO date; only the nullish values use the null fallback -src/features/public/ticket-payment.ts:221:42 ?? → || # childrenByParentId.get(): TicketListing[]|undefined, and every present array is truthy -src/features/public/ticket-payment.ts:188:14 1 → 0 # bookingDateFields clamps either default day count to one, so the produced availability rows are identical -src/features/public/ticket-payment.ts:265:30 ?? → || # a package stand-in is a non-empty group name or undefined, so only undefined reaches the listing-name fallback -src/features/public/ticket-payment.ts:337:28 ?? → || # prebuiltTree is a BookingTree object when present, and objects are always truthy -src/features/public/ticket-payment.ts:346:43 ?? → || # base.quantities.get(): number|undefined; the only falsy number is 0, 0??0===0||0 -src/features/public/ticket-payment.ts:391:17 0 → 1 # EMPTY_PRICED_ORDER.fullSubtotal is never read: bookingFactsFromOrder derives its facts from total, extras, lines, and modifierApplications -src/features/public/ticket-payment.ts:402:14 1 → 0 # bookingsForOrder normalizes either default day count to one, so the stored booking duration is identical -src/features/public/ticket-payment.ts:439:37 ?? → || # ledgerOrder: PricedOrder|null — an object is always truthy, null→fallback either way -src/features/public/ticket-payment.ts:593:61 ?? → || # childrenByParentId.get(): TicketListing[]|undefined — an array is always truthy, undefined→null either way -src/features/public/ticket-payment.ts:791:17 ?? → || # pagePackages is an array when present, and arrays are always truthy - -# Parent/child curried atoms (shared.tsx). -src/ui/templates/layout.tsx:48:30 ?? → || # Theme is "light"|"dark" or undefined, so every present value is truthy - -# Parent/child per-unit render (reservations contact-fields.ts). -src/ui/templates/public/reservations/contact-fields.ts:106:50 ?? → || # addOns?.some(...) is boolean|undefined; ?? and || agree on every boolean and on undefined -src/ui/templates/public/reservations/form.tsx:216:56 → "mutated" # TicketPageForm's only caller supplies YYYY-MM-DD options; the fallback affects HTML only if it equals an option, which "mutated" never can -src/ui/templates/public/reservations/ticket-page.tsx:200:14 ?? → || # questions is QuestionWithAnswers[]|undefined, and every present array, including [], is truthy -src/ui/templates/public/reservations/ticket-page.tsx:204:19 ?? → || # childDatesById is a ReadonlyMap|undefined, and every present Map, including an empty one, is truthy - -# Attendee phone blind-index (attendee-phone-index.ts). -src/shared/db/attendee-phone-index.ts:36:17 ?? → || # row?.id is undefined or an AUTOINCREMENT id (≥1, never 0), so ?? and || agree - -# Parent/child relationship DB (listing-parents.ts). -src/shared/db/listing-parents.ts:221:52 ?? → || # childrenByParent.get(id): ListingWithCount[]|undefined — an array is always truthy -src/shared/db/listing-parents.ts:222:49 ?? → || # parentsByChild.get(id): ListingWithCount[]|undefined — an array is always truthy - -# Group membership and package storage (groups.ts). -src/shared/db/groups.ts:84:58 1 → 0 # the stored group ids come from group_listings.group_id, a foreign key onto groups' INTEGER PRIMARY KEY, so SQLite never issues 0 and no stored value can tell minValue(1) from minValue(0) -src/shared/db/groups.ts:238:7 Missing group listing membership → "" # getListingsByGroupIds builds its result map from the ids it was given, so the single-group lookup always finds its own id and requiredMapValue never raises this message -src/shared/db/groups.ts:504:55 groupRecord → "" # columnsSql only qualifies a column when the alias is truthy, so "" selects the same bare column names, which are unambiguous in this single-table query -src/shared/db/groups.ts:224:35 ?? → || # entriesByGroup.get(id): readonly entry[]|undefined — an array is always truthy -src/shared/db/groups.ts:458:20 ?? → || # blocked[0]: { listing, block }|undefined — the present object is always truthy -src/shared/db/groups.ts:703:66 ?? → || # package rows: GroupListing[]|undefined — an array is always truthy -src/shared/db/groups.ts:836:34 ?? → || # quantity is undefined or at least 1 by PackageMemberInput's contract, so every present value is truthy -src/features/admin/api-groups.ts:128:31 ?? → || # parseMemberDayPrices returns a DayPrices object or undefined; every object, including {}, is truthy -src/features/admin/api-groups.ts:252:48 ?? → || # package rows are an array or undefined; every array, including [], is truthy - -# CRUD configuration values are functions or objects when present, and hydrated -# map values are records, so none can be falsy-but-non-null. -src/shared/rest/crud-api.ts:342:31 ?? → || # policy is an AuthPolicy object or undefined -src/shared/rest/crud-api.ts:346:18 ?? → || # lookup is a function or undefined -src/shared/rest/crud-api.ts:353:28 ?? → || # lookupAfterWrite is a function or undefined -src/shared/rest/crud-api.ts:363:31 ?? → || # hydrated map values are records or undefined; records are truthy -src/shared/rest/crud-api.ts:573:27 ?? → || # extraRoutes is a route object or undefined -src/shared/rest/resource.ts:157:25 ?? → || # validationError is an ErrorResult object or null; an object is truthy - -# Invariant-based, confirmed by reading toUnits: `lineIdxs` comes from toUnits, -# which assigns lineIdx as the flatMap array index, so the sort's input is -# always already ascending — and `sort((a,b) => a/b)` is identity on an -# already-ascending array, so swapping the comparator changes nothing. -src/shared/checkout-pricing.ts:167:16 - → / # toUnits gives ascending lineIdx; a/b is identity on sorted input - -# attendee-form-model: `?? → ||` where the left operand can never be a -# falsy-but-non-null value, so the two operators always agree. -src/features/admin/attendee-form-model.ts:304:55 ?? → || # existingByKey.get(key): ListingAttendeeRow|undefined — an object is always truthy -src/features/admin/attendee-form-model.ts:305:38 ?? → || # listingsById.get(id): ListingWithCount|undefined — an object is always truthy -src/features/admin/attendee-form-model.ts:188:36 ?? → || # (existingBooking?.price_paid ?? 0): a number; the only falsy number is 0, and 0 ?? 0 === 0 || 0 -src/features/admin/attendee-page-data.ts:140:69 ?? → || # priceRowsByGroupId.get(): PriceRow[]|undefined — an array is always truthy -src/features/admin/attendee-page-data.ts:156:33 ?? → || # byListing.get(): Map|undefined — a Map is always truthy -src/features/admin/attendee-page-data.ts:237:65 ?? → || # preselectedQty.get(): number|undefined; the only falsy number is 0, and the fallback is also 0 -src/features/admin/attendee-page-data.ts:353:47 1 → 0 # standard bookings pass date:null, so buildCapacityCondition ignores durationDays entirely -src/features/admin/attendee-page-data.ts:380:42 ?? → || # totalByListing.get(): number|undefined; the only falsy number is 0, and the fallback is also 0 -src/features/admin/attendee-page-data.ts:469:44 ?? → || # incompleteParents stores only a non-empty translated warning string, otherwise get returns undefined -src/features/admin/attendee-page-data.ts:558:63 ?? → || # find returns a status object or undefined, and a status object is always truthy -src/features/admin/attendee-page-data.ts:560:34 ?? → || # remaining_balance is an integer; its only falsy value is 0, which equals the fallback -src/features/admin/attendee-page-data.ts:561:30 ?? → || # fullPrice is an integer or the summary is absent; 0 equals the fallback and every other amount is truthy -src/features/admin/attendee-page-data.ts:562:32 ?? → || # depositPaid is an integer or the summary is absent; 0 equals the fallback and every other amount is truthy -src/features/admin/attendee-page-data.ts:562:36 0 → 1 # the fallback is used only in create mode, where fullPrice is 0; amountPaid cannot affect any balance-notice branch when no order exists -src/features/admin/attendee-page-data.ts:572:42 ?? → || # hasMixedTimings is boolean|undefined, and false equals the fallback -src/features/admin/attendee-page-data.ts:579:30 ?? → || # questions is an array when present, and arrays are always truthy -src/features/admin/attendee-page-data.ts:582:46 ?? → || # selectedAnswerIds is an array when present, and arrays are always truthy -src/features/admin/attendee-page-data.ts:583:50 ?? → || # selectedTextAnswers is a Map when present, and Maps are always truthy -src/features/admin/attendee-page-data.ts:626:62 ?? → || # attendeeAnswerMap values are arrays, and arrays are always truthy -src/features/tickets/token-utils.ts:227:44 ?? → || # (attendees[i]?.bookings ?? []): ListingAttendeeRow[]|undefined — an array is always truthy, undefined→[] either way - -# Ticket form parsing (ticket-form.ts): `?? → ||` where the operand is an array, -# an object, a Map.get number (only falsy-non-null is 0, 0??0===0||0), or a -# string that is never the empty string — so the two operators always agree. -src/features/public/ticket-form.ts:49:60 ?? → || # questionListingMap.get(): number[]|undefined — arrays always truthy -src/features/public/ticket-form.ts:137:28 ?? → || # ctx.actionUrl: string|undefined, only ever a real URL or undefined (never ""), so ?? and || agree -src/features/public/ticket-form.ts:168:36 ?? → || # quantities.get(): number|undefined; the only falsy number is 0, 0??0===0||0 - -# largest-remainder: option hooks are functions when present, so they are -# truthy; undefined falls through to the same default under `??` and `||`. -src/shared/largest-remainder.ts:19:40 ?? → || # canReceive?: function|undefined — functions are truthy, undefined→default -src/shared/largest-remainder.ts:20:40 ?? → || # tieBreaker?: function|undefined — functions are truthy, undefined→default - -# crypto/utils: Uint8Array indexing yields number|undefined; fallback is 0, and -# the only falsy non-null byte is 0, so `byte ?? 0` and `byte || 0` agree. -src/shared/crypto/utils.ts:17:19 ?? → || # bufA[i]: number|undefined, 0 ?? 0 === 0 || 0 -src/shared/crypto/utils.ts:18:19 ?? → || # bufB[i]: number|undefined, 0 ?? 0 === 0 || 0 - -# request-scoped ambient fallback: scope.current() is T|undefined where T is -# always a truthy container object (never a falsy-non-null value), so -# `?? fallback` and `|| fallback` always agree. (createScopedValue's ?? is NOT -# equivalent — a falsy scoped value must win over the fallback — and the -# "falsy value still wins" test kills it.) -src/shared/request-scoped.ts:130:35 ?? → || # scope.current(): T|undefined — a container object is always truthy - -# savedFormValue: form?.getString(name) is string|undefined; the only falsy -# non-null string is "" which equals the "" fallback, so ?? and || agree. -src/shared/forms/saved-data.ts:30:49 ?? → || # getString(): string|undefined, "" ?? "" === "" || "" - -# test-utils/csrf: regex `match` results are either null/undefined or truthy -# values; the quantity capture is `\d+`, so it can never be an empty string. -test/test-utils/csrf.ts:15:46 ?? → || # input tag match: RegExpMatchArray|null; a non-null array is truthy -test/test-utils/csrf.ts:32:46 ?? → || # input tag match: RegExpMatchArray|null; a non-null array is truthy -test/test-utils/csrf.ts:43:47 ?? → || # option tag match: RegExpMatchArray|null; a non-null array is truthy -test/test-utils/csrf.ts:236:20 ?? → || # quantity id capture: string|undefined, but `\d+` makes every present value truthy - -# test-utils/test-browser: remaining nullish fallbacks either receive truthy -# objects/non-empty regex matches, or fall back to the same empty string for the -# only falsy non-null string value. -test/test-utils/test-browser.ts:80:73 ?? → || # find(): LinkMatch|undefined; a found object is truthy -test/test-utils/test-browser.ts:100:26 ?? → || # missing input type defaults to text; empty/invalid type follows the same successful-input path as text -test/test-utils/test-browser.ts:150:19 ?? → || # submitted option tags are non-empty strings when present, otherwise undefined -test/test-utils/test-browser.ts:249:45 ?? → || # button value fallback is "", so empty value and absent value agree -test/test-utils/test-browser.ts:382:58 ?? → || # RequestInit.method is omitted or a non-empty method string in every caller -test/test-utils/test-browser.ts:460:28 ?? → || # forms[0] is a FormInfo object when present, otherwise undefined, so both operators reach throwNoForm() on the same input -src/ui/templates/admin/listing-defaults.tsx:105:17 ?? → || # UrlControl value: string|undefined; the only falsy-non-null string is "" which equals the "" fallback, so ?? and || coincide -src/ui/templates/admin/listing-defaults.tsx:128:38 ?? → || # value?.includes(day): boolean|undefined; for boolean b, b ?? false === b || false, and undefined ?? false === undefined || false -src/shared/booking/build-tree.ts:130:48 ?? → || # childrenByParentId?.get(): readonly TicketListing[]|undefined — an array is always truthy, so ?? and || coincide -src/shared/booking/build-tree.ts:201:52 ?? → || # pkg.quantities.get(): a member's fixed per-package quantity is ≥1 — every save path enforces it (the form's parsePackageQuantity clamps 0→1 in admin/groups.ts, the JSON API's parsePackageMember rejects quantity<1 in admin/api-groups.ts), so the map never holds 0 and (n ?? 1)===(n || 1) -src/shared/booking/build-tree.ts:228:33 ?? → || # deriveRootRef input.packages: readonly TreePackage[]|null|undefined — a non-empty array is truthy and [] destructures to the same `first`, so ?? and || coincide -src/shared/booking/build-tree.ts:232:38 ?? → || # standaloneListingIds?.size: number|undefined — a real size is ≥0 and (0 ?? 0)===(0 || 0), undefined too, so ?? and || coincide -src/shared/booking/build-tree.ts:244:34 ?? → || # buildBookingTree input.packages: readonly TreePackage[]|null|undefined — same as the deriveRootRef case, an array/null/undefined makes ?? and || coincide -src/shared/booking/build-tree.ts:251:56 ?? → || # standaloneListingIds?.has(): boolean|undefined — (b ?? false)===(b || false) for any boolean b, and undefined too, so ?? and || coincide -src/shared/booking/fold-tree.ts:136:55 ?? → || # selectFoldableParents parentQty: base quantities are undefined or ≥0, and a 0 takes the same `<= 0` skip, so ?? and || coincide -src/shared/booking/fold-tree.ts:168:4 ?? → || # childQtyField: parseNonNegativeInt returns a non-negative int or null; its only falsy result (0) already maps to 0, so ?? and || coincide -src/shared/booking/fold-tree.ts:180:63 10 → 0 # parseInt differs only for a non-decimal key; childQtyField then reads its canonical decimal key, whose own scan rejects it when positive, so both paths agree -src/shared/booking/fold-tree.ts:301:48 ?? → || # foldChild summed: state.quantities.get(childId) is undefined (first fold) or a positive running total, never 0, so ?? and || coincide -src/shared/booking/fold-tree.ts:465:41 ?? → || # dayCount: customisableDuration is a positive day count or null, never 0, so ?? base.dayCount and || base.dayCount coincide -src/shared/booking/signed-metadata.ts:120:59 ?? → || # allocations come from foldChild with qty > 0, so a running child total is undefined or positive and both fallbacks agree -src/shared/booking/signed-metadata.ts:142:37 ?? → || # foldedQty values are positive allocation totals; absent and present values behave identically with either zero fallback -src/shared/catalog-fields/definition.ts:166:38 1 → 0 # a missing column becomes selected by its non-empty database name, but the columns projection still returns undefined and drops it; present columns are selected either way -src/shared/booking/capacity-tree.ts:38:45 ?? → || # groupIdsByListingId.get(): number[]|undefined — an array is always truthy, undefined→[] either way - -# Attendee group-capacity lookups: Map values are arrays (always truthy), and -# duration normalization clamps both a default of 0 and 1 to one day. -src/shared/db/attendees/capacity/groups.ts:135:50 ?? → || # rowsByGroup.get(): GroupRow[]|undefined — an array is always truthy -src/shared/db/attendees/capacity/groups.ts:146:14 1 → 0 # expandDailyRange normalizes both 0 and 1 to a one-day span - -# v2 signed metadata (Phase 2d). -src/shared/payment-helpers.ts:198:24 ?? → || # toBookingItems intent.allocations: ChildAllocation[]|undefined — an array is always truthy, undefined→[] either way -src/shared/payment-helpers.ts:569:16 || → && # parsed JSON null/falsy primitives return {} in both forms; truthy primitives expose none of PACKED_KEYS and also produce {} -src/shared/payment-helpers.ts:628:40 ?? → || # metadata field length is undefined or a non-negative integer; 0 and undefined both use 0 - -# Admin listing API fallbacks receive only objects, arrays, positive ids, or -# nullish values, so no valid request can distinguish these operators. -src/features/admin/api.ts:219:66 ?? → || # stored listing lookup returns a ListingWithCount object or null; listing objects are truthy -src/features/admin/api.ts:250:25 ?? → || # parsed group ids are an array or undefined; every array, including [], is truthy -src/features/admin/api.ts:369:30 1 → 0 # the mutation changes sentinel -1 to -0; positive listing ids match neither value -src/features/admin/api.ts:411:19 ?? → || # groupIds is an array or undefined; every array, including [], is truthy -src/features/admin/api.ts:424:43 ?? → || # an existing listing id is positive and truthy, otherwise optional access yields undefined -src/features/admin/api.ts:426:38 ?? → || # groupIds is an array or undefined; every array, including [], is truthy - -# --- External order widget (src/ui/client/order.ts) ---------------------------- -# URL matches and catalog entries are either non-empty strings/objects or absent, -# so these nullish and truthy fallbacks have the same result. -src/ui/client/order.ts:101:57 ?? → || # RegExp match group is a non-empty slug or undefined -src/ui/client/order.ts:108:36 ?? → || # catalogEntry returns a CatalogEntry object or undefined -src/ui/client/order.ts:213:51 ?? → || # merged quantities are positive integers, so an existing sum is truthy -src/ui/client/order.ts:64:37 0 → 1 # quantity 1 takes the fallback 1 instead of returning 1 - -# reconcile runs once on a new controller whose notice starts empty. The only -# production caller of add always passes parseAddQuantity's positive result. -src/ui/client/order.ts:216:18 = → += # empty notice plus the message equals the message -src/ui/client/order.ts:222:39 1 → 0 # add's default is unused; its only caller always supplies quantity - -# Every `= → +=` below writes to a newly-created DOM element property whose DOM -# default is "". Concatenating the assigned text onto "" is exactly assignment. -src/ui/client/order.ts:294:24 = → += # fresh heading textContent -src/ui/client/order.ts:299:18 = → += # fresh notice paragraph className -src/ui/client/order.ts:300:20 = → += # fresh notice paragraph textContent -src/ui/client/order.ts:316:22 = → += # fresh subtotal paragraph className -src/ui/client/order.ts:317:24 = → += # fresh subtotal paragraph textContent -src/ui/client/order.ts:322:23 = → += # fresh caveat paragraph className -src/ui/client/order.ts:323:25 = → += # fresh caveat paragraph textContent -src/ui/client/order.ts:329:24 = → += # fresh empty paragraph textContent -src/ui/client/order.ts:338:18 = → += # fresh row className -src/ui/client/order.ts:340:19 = → += # fresh name span className -src/ui/client/order.ts:341:21 = → += # fresh name span textContent -src/ui/client/order.ts:345:20 = → += # fresh price span className -src/ui/client/order.ts:346:22 = → += # fresh price span textContent -src/ui/client/order.ts:360:23 = → += # fresh Continue button textContent -src/ui/client/order.ts:378:18 = → += # fresh count span className -src/ui/client/order.ts:379:20 = → += # fresh count span textContent -src/ui/client/order.ts:381:20 = → += # fresh cart label span textContent -src/ui/client/order.ts:389:21 = → += # fresh Close button textContent -src/ui/client/order.ts:399:17 = → += # fresh stepper span className -src/ui/client/order.ts:402:18 = → += # fresh decrease button textContent -src/ui/client/order.ts:406:20 = → += # fresh quantity span textContent -src/ui/client/order.ts:409:18 = → += # fresh increase button textContent -src/ui/client/order.ts:414:21 = → += # fresh Remove button textContent -src/ui/client/order.ts:422:20 = → += # fresh style element textContent - -# buildButton's initial values are overwritten by the constructor's render -# before the button is exposed. -src/ui/client/order.ts:376:19 true → false # constructor render immediately sets hidden from the cart count -src/ui/client/order.ts:379:23 0 → "" # constructor render immediately writes the current numeric count - -# The widget owns this global registry: before init it is undefined, afterwards -# it is an object. Both are mapped to the same object by these operators. -src/ui/client/order.ts:450:40 ?? → || # registry is Record|undefined - -# Dataset values are strings when present. At initial scan the selector requires -# the attribute; after enhancement, an absent value and either fallback string -# all fail both resolvers and preserve normal navigation. -src/ui/client/order.ts:466:40 ?? → || # present dataset string; empty maps to the same empty fallback -src/ui/client/order.ts:466:44 → "mutated" # fallback is unreachable for the matching selector -src/ui/client/order.ts:481:46 ?? → || # present strings are truthy or empty; both operators keep the same value -src/ui/client/order.ts:481:50 → "mutated" # absent fallback values resolve to neither listing nor package -src/shared/site-pages/core.ts:142:50 ?? → || # descendantsOf: forest.itemsByPage.get() → SitePageItem[]|undefined — an array is always truthy -src/shared/site-pages/core.ts:309:43 ?? → || # levelOf: forest.itemsByPage.get() → SitePageItem[]|undefined — an array is always truthy - -# listing_prices sync (listing-prices.ts): `?? → ||` where the operand's only -# falsy-non-null value equals the fallback. -src/shared/db/listing-prices.ts:345:45 ?? → || # sourceRowStatements row.unit_price: number|null; the only falsy-non-null value is 0 and 0 ?? 0 === 0 || 0 -src/shared/db/listing-prices.ts:110:38 ?? → || # groupDayPriceStatements member.dayPrices: DayPrices|undefined — an object is always truthy, undefined→{} either way -src/shared/db/listing-prices.ts:201:45 ?? → || # foldGroupDayRows result.get(): Map|undefined — a Map is always truthy - -# attendee entity page: `?? → ||` where the operand's only falsy-non-null -# value equals the "" fallback. -src/features/admin/attendee-page.ts:266:60 ?? → || # ctx.query.get("token"): string|null; the only falsy-non-null value "" equals the "" fallback -src/shared/db/name-registry.ts:89:51 ?? → || # matchName index.get(): number[]|undefined — an array is always truthy -src/shared/db/name-registry.ts:109:20 ?? → || # isNameTakenAnywhere index.get(): number[]|undefined — an array is always truthy -src/features/admin/catalog-transfer/import.ts:182:33 ?? → || # membershipSpec quantity comes from the PositiveInt schema (at least 1) or is undefined, so it is never 0 -src/features/admin/catalog-transfer/import.ts:267:15 true → false # childOnlyAddOnCheckerForListings resolves only ids and groupIds; it never reads ListingGroupMembership.active -src/features/admin/catalog-transfer/import.ts:295:42 0 → 1 # edgeFieldError never reads EdgeListing.id, and child reachability uses the separate placeholder id declared above -src/features/admin/catalog-transfer/import.ts:537:36 ?? → || # isPackage is boolean|undefined, and false equals the false fallback -src/features/admin/catalog-transfer/schema.ts:285:18 ?? → || # formatTransferIssues flat.root: string[]|undefined — an array is always truthy -src/features/admin/catalog-transfer/schema.ts:286:34 ?? → || # formatTransferIssues flat.nested: object|undefined — an object is always truthy - -# Listing business logic (listings-actions.ts) — optional-field defaults where the -# fallback equals the only falsy-non-null value, or the operand is always truthy. -src/shared/listings-actions.ts:79:36 ?? → || # validateMaxPrice unitPrice: number|undefined, 0 ?? 0 === 0 || 0 -src/shared/listings-actions.ts:133:34 ?? → || # groupIds: number[]|undefined — an array is always truthy -src/shared/listings-actions.ts:138:46 ?? → || # canPayMore: boolean|undefined, only falsy-non-null is false === fallback -src/shared/listings-actions.ts:157:24 ?? → || # listingType: "standard"|"daily"|undefined — never "", so ?? and || coincide -src/shared/listings-actions.ts:158:29 ?? → || # customisableDays: boolean|undefined, only falsy-non-null is false === fallback -src/shared/listings-actions.ts:159:17 ?? → || # existingId is a positive listing id or undefined, never 0 -src/shared/listings-actions.ts:192:27 ?? → || # monthsPerUnit: number|undefined, 0 ?? 0 === 0 || 0 -src/shared/listings-actions.ts:195:56 ?? → || # initialSiteMonths: number|undefined, 0 ?? 0 === 0 || 0 -src/shared/listings-actions.ts:207:44 ?? → || # customisableDays: only falsy-non-null false === fallback -src/shared/listings-actions.ts:208:30 ?? → || # dayPrices: DayPrices|undefined — an object is always truthy -src/shared/listings-actions.ts:209:54 ?? → || # durationDays fallback is clamped; 0 and 1 both become 1 -src/shared/listings-actions.ts:209:58 1 → 0 # clampDurationDays maps either fallback to the minimum duration of 1 -src/shared/listings-actions.ts:221:34 ?? → || # listingType: never "", so ?? and || coincide -src/shared/listings-actions.ts:222:39 ?? → || # monthsPerUnit: 0 ?? 0 === 0 || 0 -src/shared/listings-actions.ts:411:19 ?? → || # groupIds: number[]|undefined — an array is always truthy - -# Admin group routes (features/admin/groups.ts) — object/Map/0-fallback nullish -# coalescing where ?? and || always agree. -src/features/admin/groups.ts:182:47 ?? → || # parseMemberDayPrices byListing.get(): DayPrices|undefined — an object is always truthy -src/features/admin/groups.ts:201:51 ?? → || # parsePackageMembers dayPricesByListing.get(): DayPrices|undefined — an object is always truthy -src/shared/listings-actions.ts:376:38 && → || # clearing false means mayStripPage never loads a flagged child; clearing true without a flagged child only reruns unchanged reachability, which cannot create an orphan - -# attendee merge/refund route: mutants with no observable effect. -src/features/admin/attendee-refunds.ts:179:24 ?? → || # flash.error: string|undefined from applyFlash; a refund error flash is always a non-empty message, so the only falsy-non-null value "" never occurs and ?? / || agree - -# attendee table values and columns -src/ui/templates/attendee-table/values.ts:54:67 ?? → || # getAnswerDisplay attendeeAnswerMap.get(): number[]|undefined — an array is always truthy, so ?? and || coincide -src/ui/templates/attendee-table/columns.tsx:59:39 || → ?? # date column row.attendee.date: string|null; null gives "" under both operators, and "" is the only falsy-non-null string, which also gives "" under both -src/ui/templates/attendee-table/columns.tsx:64:36 || → ?? # email column row.attendee.email: string (never null/undefined); "" is the only falsy value and is also the fallback, so || and ?? agree for every possible value -src/ui/templates/attendee-table/values.ts:106:47 ?? → || # listing name is string|undefined; its only falsy string is the empty fallback, so ?? and || agree - -# --- Rich markdown editor (src/ui/client/admin/markdown-editor*.ts) ------------ -# Browser DOM-construction code, same class of equivalents as order.ts above: -# `= → +=` on a freshly created element's property whose value before the -# assignment is "" (className, src), or a boolean IDL attribute whose setter -# coerces the truthy `false + true === 1` to the same true. -src/ui/client/admin/markdown-editor.ts:49:18 = → += # mount.className: fresh element, "" before -src/ui/client/admin/markdown-editor.ts:91:19 = → += # toggle.className: fresh element, "" before -src/ui/client/admin/markdown-editor-toolbar.ts:164:16 = → += # dom.className: fresh element, "" before -src/ui/client/admin/markdown-editor-toolbar.ts:167:23 = → += # button.textContent: fresh element, "" before -src/ui/client/admin/markdown-editor-toolbar.ts:168:17 = → += # button.title: fresh element, "" before -# The ordered-list input rule's `match[1]` vs `match[0]`: the regex is -# /^(\d+)\.\s$/ so match[0] is always "N. " and Number("N. ") === Number("N") -# (Number trims whitespace and "N." is the same decimal) — no typed input can -# tell them apart. -src/ui/client/admin/markdown-editor-setup.ts:107:25 1 → 0 # Number("N. ") === Number("N") for every ordered-list match - -# resize.ts writes each output pixel's channels exactly once into a freshly -# zeroed Uint8ClampedArray, so `out[o] = x` and `out[o] += x` are identical -# (0 + x === x). No input can revisit an index, so no test can distinguish them. -src/shared/images/resize.ts:59:13 = → += # pixel[0] starts at 0 and is written once -src/shared/images/resize.ts:60:13 = → += # pixel[1] starts at 0 and is written once -src/shared/images/resize.ts:61:13 = → += # pixel[2] starts at 0 and is written once - -# middleware.ts's Content-Type fallback already equals the only falsy string. -# Once a tracking key is found, continuing instead of breaking can only -# rediscover more tracking keys without changing the final boolean. -src/features/middleware.ts:155:39 ?? → || # Content-Type is string|null; the only falsy non-null string is "", which is also the fallback -src/features/middleware.ts:217:7 break; → (removed) # later keys cannot reverse hasTracking - -# bundle-loader.ts assigns query/style/script values onto freshly-created URLs -# and elements. Each left-hand value is empty (or false for defer), so `=` and -# `+=` produce the same final URL/string/boolean after DOM coercion. -src/ui/client/admin/bundle-loader.ts:25:16 = → += # a sibling URL starts with an empty search -src/ui/client/admin/bundle-loader.ts:41:13 = → += # fresh link.rel is empty -src/ui/client/admin/bundle-loader.ts:42:14 = → += # fresh link.href is empty -src/ui/client/admin/bundle-loader.ts:46:13 = → += # fresh script.src is empty - -# group-page-data fallbacks have operands whose only falsy non-null value -# coincides with the fallback. -src/features/admin/group-page-data.ts:64:44 ?? → || # (row.package_price ?? 0): number|null, only falsy-non-null is 0 and 0 ?? 0 === 0 || 0 -src/features/admin/group-page-data.ts:194:49 ?? → || # dayPrices.get(): DayPrices (a Map) | undefined — a Map is always truthy - -# questions.ts form `id`s feed only CSRF/flash-form coordination, never the -# rendered CsrfForm id, which is "new-question". -src/features/admin/questions.ts:133:7 questionText → "" # defineForm config.id: only feeds CSRF/flash-form coordination; the page's CsrfForm uses id="new-question", so this value is never rendered observably -src/features/admin/questions.ts:146:7 answerText → "" # answerTextForm config.id: same as questionText — unobservable form-coordination id - -# buildAssetPathsModule embed-script gate `pathConst === "EMBED_JS_PATH"`: -# pathConst is the string 4th element of an AssetDef tuple compared against a -# string literal, so === and == never disagree — no null/undefined/number -# operand exists to coerce. -# The generated WASM modules terminate every statement with punctuation, so -# joining their source lines with "" or ";" parses and behaves identically. -scripts/inline-jsquash-wasm.ts:53:21 ; → "" # inline module remains semicolon/brace delimited -scripts/inline-jsquash-wasm.ts:75:38 ; → "" # remote module remains semicolon/brace delimited - -# sort-listings.ts's tier constants (0/1/2 for no-date-standard/dated- -# standard/daily) and the "date === ''" empty-date sentinel: Array.sort only -# ever branches on tierA-tierB's SIGN (never its magnitude), so shifting the -# daily tier's value, or shifting a misclassified tier by a constant offset, -# preserves the same relative ordering. Separately, misclassifying a no-date -# listing as tier 1 (or -1) is unobservable because whichever comparator path -# it then takes still produces the same result: compareDatedStandard's -# `"".localeCompare(realDate)` is always negative (empty sorts first, same as -# the intended tier-0-before-tier-1 rule), and two misclassified no-date -# items tie on date and fall through to the same name comparison the real -# tier-0 branch would have used; a same-tier compare that lands in -# compareDaily instead (via a tier value matching neither 0 nor 1) hits its -# own non-daily fallback-to-name-comparison, again the same result. Several -# entries below only exist as mutants under --exhaustive (extra number-literal -# offsets, and the ternary's "consequent/alternate only" variants); the -# ignore-list checker cross-checks staleness against the full --exhaustive -# mutant set regardless of which mode a given run used, so they stay valid -# (not "stale") even under the default-mode precommit gate. -src/shared/sort-listings.ts:22:48 2 → 3 # daily tier constant; only used via tierA-tierB's sign, never compared for equality -src/shared/sort-listings.ts:23:10 ?: → alternate only # listingTier always-1: masked by compareDatedStandard's ""-sorts-first + same-tier name fallback, see block comment above -src/shared/sort-listings.ts:23:27 → "mutated" # same effect as "always 1" above, for any date value that isn't literally the string "mutated" -src/shared/sort-listings.ts:23:32 0 → 1 # same effect as "always 1" above -src/shared/sort-listings.ts:23:32 0 → -1 # tier -1 preserves sign vs 1/2, and same-tier dispatch falls through to compareDaily's non-daily name fallback — same result -src/shared/sort-listings.ts:63:19 0 → -1 # tierA===-1 never matches a real tier; tier-0 same-tier compares fall through to compareDaily's non-daily fallback, which returns the same name comparison - -# sort-listings.ts's compareDaily: nextDates.get() is typed string|null, never -# undefined in practice because every daily listing gets an explicit entry, and -# getNextBookableDate never returns "". Therefore `?? null` and `|| null` agree. -src/shared/sort-listings.ts:46:36 ?? → || # nextDates.get(a.id): string|null, never a falsy-non-null string -src/shared/sort-listings.ts:47:36 ?? → || # nextDates.get(b.id): string|null, never a falsy-non-null string - -# sort-listings.ts's compareDaily return values on the dateA===null branch: -# confirmed by direct experiment that Array.sort's swap decision only checks -# whether a comparator result is strictly negative — 0, a positive number, or -# NaN (from `return undefined`) are all treated identically ("don't swap"). -# Since the mirrored `dateB === null` branch independently and correctly -# returns -1 for the reverse pairing, no input can ever make this branch's -# non-negative return value observably differ from the intended `1` (the -# magnitude-only `1 → 2` entries below are --exhaustive-only; see note above). -src/shared/sort-listings.ts:49:30 1 → 0 # non-negative result; see block comment above -src/shared/sort-listings.ts:49:30 1 → 2 # sign-preserving magnitude change; see block comment above -src/shared/sort-listings.ts:50:31 1 → 2 # sign-preserving magnitude change on `return -1`; only the sign is ever observed - -# orphan-retention.ts's Number.parseInt(safeValue, 10) radix: safeValue is -# gated by isOrphanRetentionValue to always be one of ORPHAN_RETENTION_OPTIONS' -# values or the DEFAULT_ORPHAN_RETENTION fallback — a closed set of plain -# decimal digit strings ("0","182","365",...) that never start with "0x"/"0o"/ -# "0b". Radix 0 (auto-detect) and radix 10 agree on every value in that set -# (confirmed directly), so no input safeValue can ever take distinguishes them. -src/shared/orphan-retention.ts:61:43 10 → 0 # safeValue is a closed set of plain decimal strings; radix 0 and 10 agree on all of them - -# listing-templates.ts's matchesSignature is a private helper consumed only as -# an Array.prototype.find() predicate, which coerces its result with -# ToBoolean rather than comparing strictly to `true`/`false` — so an early -# `return false` becoming `return undefined` is unobservable through -# inferTemplate (the only caller) for any input, even though the exhaustive -# 16-combo dimension table already covers every reachable production case. -src/shared/listing-templates.ts:132:4 ?? → || # LISTING_TEMPLATES.find(): ListingTemplate|undefined — an object is always truthy, so ?? and || agree - -# bulk-replace.ts's formatIsoForPreview 24:00→00:00 fix-up: this guards against -# an ICU/V8 midnight-formatting quirk (`toLocaleString` with hour12:false -# rendering exact local midnight as "24:00" instead of rolling to the next -# day's "00:00") that may occur on the Bunny edge runtime's ICU build. Swept -# many timezone/date combinations (including a DST transition) against this -# Deno runtime's ICU and never observed "24:" in the output — the regex this -# replacement string feeds never matches here, so the replacement's own -# content is unobservable in this test environment. Same class as -# src/shared/images/codecs.ts's edge-only codec init below. -src/shared/bulk-replace.ts:103:56 $1 00: → "" # this runtime's ICU never renders local midnight as "24:00", so the regex this feeds never matches -src/shared/bulk-replace.ts:103:56 $1 00: → "$1 00: mutated" # same as above - -# childHasDateOrStockForDays' `days ?? 1` fallback: normalizeDurationDays -# (called downstream in isBookingRangeValid) clamps any non-finite or -# sub-1 value up to 1, so `1`, `0`, and `-1` all normalize identically — -# and `days || 1` reaches the same clamp for every falsy `days` (null, 0, -# NaN), agreeing with `days ?? 1` for every value the type permits. -src/shared/booking/model.ts:71:41 ?? → || # every falsy days (null/0/NaN) clamps to 1 downstream either way -src/shared/booking/model.ts:71:45 1 → 0 # normalizeDurationDays clamps 0 up to 1 anyway -src/shared/booking/model.ts:71:45 1 → -1 # normalizeDurationDays clamps negatives up to 1 anyway - -# bookableChildIds: `childrenByParentId?.values()` is either undefined or a -# live MapIterator, and iterator objects are always truthy — never a -# falsy-but-defined value — so ?? and || agree. -src/shared/booking/model.ts:131:38 ?? → || # Map#values() is undefined or a truthy iterator object, never falsy-but-defined - -# keepDayCountsChildrenSupport: `dayCountsChildSupports(child) ?? current` -# — dayCountsChildSupports returns number[]|null, and arrays are always -# truthy in JS even when empty, so ?? and || agree for every possible -# return value. -src/shared/booking/model.ts:274:49 ?? → || # dayCountsChildSupports(): number[]|null, and an array is truthy even when empty - -# package-cap.ts's groupIdsFor: `ctx.groupIdsByListingId.get(...) ?? []` — a -# Map#get() on a ReadonlyMap is either undefined or a -# truthy array (even an empty array is truthy), so ?? and || agree. -src/shared/booking/package-cap.ts:82:63 ?? → || # groupIdsByListingId.get(): number[]|undefined, and an array is truthy even when empty - -# smallestLimitedGroupFor: `limitedGroupsFor(...).toSorted(...)[0] ?? null` -# — array-index access yields the element (an object, always truthy) or -# undefined for an empty array, so ?? and || agree. -src/shared/booking/package-cap.ts:170:7 ?? → || # array[0]: object|undefined — an object is always truthy, never falsy-but-defined - -# parentGroupChildLimit: `shared.length === 0`, mutated to `=== -1` — array -# length is never negative, so this condition is always false either way; -# the fallback computation (Math.min() over an empty spread is +Infinity, -# ticketsThatFitInPool(Infinity,2) is Infinity, sumOf of [] is 0, and -# Math.min(Infinity,0) is 0) coincidentally reproduces the same `0` the -# early return gives for a genuinely empty `shared`, so no input can ever -# distinguish the two paths' output. -src/shared/booking/package-cap.ts:190:25 0 → -1 # shared.length is never negative; the empty-array fallback path already returns 0 too - -# registerTableInvalidation: `invalidatorsByTable.get(table) ?? new Set()` -# — Map#get() is undefined or a Set object, and a Set is always truthy -# (even when empty), so ?? and || agree. -src/shared/cache-registry.ts:129:47 ?? → || # invalidatorsByTable.get(): Set|undefined — a Set is always truthy, even when empty - -# band-name-generator.ts's fixArticles: the "An → A " -# correction has no template or pool word that ever produces a literal "An" -# token to correct — confirmed empirically over 20,000 generated descriptions -# with zero matches for /\bAn [^aeiouAEIOU]/ — so this replacement's argument -# is unobservable via any input the public API can produce. (The mirrored -# "A → An " correction on the next line IS exercised — several -# BUILDING_STATES/BAND_ADJECTIVES entries start with a vowel — and is covered -# by dedicated tests instead of being listed here.) -src/shared/band-name-generator.ts:119:39 A $1 → "" # no template/pool ever produces a literal "An" token for this correction to act on -src/shared/band-name-generator.ts:119:39 A $1 → "A $1 mutated" # same as above - -# logistics-filter.ts's parseAgentFilter fallback is unobservable through -# parsePositiveIntId's strict decimal-digit schema. -src/shared/logistics-filter.ts:22:39 → "mutated" # parsePositiveIntId's schema rejects any non-digit string identically; the "" fallback and any mutated string both parse to null - -# address-lookup client: panelText's "" fallback matches the only falsy dataset -# value, and freshly created options start with empty value/text. -src/ui/client/admin/address-lookup.ts:50:21 ?? → || # dataset[key]: string|undefined, only falsy string "" === fallback "" -src/ui/client/admin/address-lookup.ts:55:15 = → += # a just-created