Skip to content

docs(service-cluster-redis): record the measured ioredis 5-vs-6 gap in the contract test header, and drop a live-Redis path that never existed - #15985

Merged
os-warren merged 4 commits into
mainfrom
claude/issue-15467-ioredis-mock-major-seam
Sep 5, 2026
Merged

docs(service-cluster-redis): record the measured ioredis 5-vs-6 gap in the contract test header, and drop a live-Redis path that never existed#15985
os-warren merged 4 commits into
mainfrom
claude/issue-15467-ioredis-mock-major-seam

Conversation

@os-warren

@os-warren os-warren commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

Fixes #15467

Comment-only change to one test file. No code, no dependency, no behaviour touched. Head a2bbec441.

Round 2 — the clause-② review returned NOT PASS on three header sentences whose stated population was wider than what was measured. All three are corrected, plus the two advisories. The three corrections and their new measurements are in the last section; the body below is corrected in place so nothing here still states the over-broad version.

What was measured, before anything was proposed

The card asked whether the contract test still tests the shipped behaviour. On the surface these suites drive, the intersection with "surface where ioredis 5 and 6 differ" is empty — that is the finding, and the scoping words are load-bearing.

Population 1 — what this package issues that lives in RedisCommander.d.ts. 14 commands: get, set, del, incr, incrby, pttl, watch, unwatch, exec, publish, subscribe, unsubscribe, quit, eval. multi() and duplicate() are not in this population — they are declared elsewhere and are measured separately below.

Population 2 — the version pair. ioredis 5.11.1 (newest release satisfying the mock's ^5 peer, fetched via npm pack) against ioredis 6.0.0 (the version resolved in this workspace). Compared declaration-by-declaration, multi-line aware, normalising only quote style, (T)[] versus T[], and whitespace.

surface reading
the 14 RedisCommander commands every v5 overload survives verbatim into v6
set strict superset — v6 adds 48 overloads for the IFEQ/IFNE/IFDEQ/IFDNE tokens; nothing removed or re-shaped
Pipeline.d.ts byte-identical between the two
multi() declared on Transaction, in RedisCommander.d.ts in neither version (measured 0 and 0; control member exec reads 1 and 1). All four overloads did change return type, to ChainableCommander parameterised by a reply mapping
the class gains a ReplyMapping parameter defaulting to legacy, and extends Transaction at resp2 when it is legacy; duplicate() with no override inherits the caller's mapping
RedisOptions keys client.ts sets lazyConnect, maxRetriesPerRequest, enableAutoPipelining declared identically

multi()'s change is inert here: the mapping parameter defaults to resp2, the class defaults to legacy and extends Transaction at resp2, so a client built without replyMapping resolves multi() to the non-RESP3 instantiation — and exec()'s own declaration is byte-identical across the pair.

Two controls on the normaliser, both fired: a genuinely different pair (NX versus XX) stayed different after normalisation; a purely cosmetic pair collapsed. An earlier run reported eval as changed — bracket-adjacent whitespace from my own multi-line join; the normaliser was fixed rather than the result eyeballed.

Separately, the command-metadata tables (@ioredis/commands 1.11.0 for the mock, 2.0.0 for ioredis 6) differ on 3 of the 15 used entries — del, subscribe, eval — entirely in server-side and cluster-routing fields. None changes a single-node client's call or reply shape. Control: 12 commands exist only in 2.0.0, 0 only in 1.11.0, an absent name reads undefined in both.

On the surface these suites drive, v6's one substantive change is RESP3 reply mapping, and it is opt-in. This package never passes replyMapping. ⚠️ Scoped deliberately: v6 also changes connection defaults that are not opt-in — protocol: 3 (no such option in v5) and keepAlive 0 to 30000. Those live on the connection surface, reached in production via createRedisClient() and by no suite here, so they are changed-but-unexercised. The card's promotion trigger to p2 asks about a surface this package uses in these tests, and it does not fire.

Where the suite stops standing in for shipped behaviour

Measured, not assumed: the shipped test's makeClient was temporarily wrapped in a recording Proxy and the suite run unmodified. All 28 tests passed under instrumentation; the file was then restored to a state proved byte-identical to HEAD.

The suite reaches the mock only through the injected client and the pub/sub duplicate(). It never calls createRedisClient() — so new Redis(url, options), this package's only contact with ioredis's constructor and connection surface, is exercised by nothing in this file. That is why the version gap is inert here, and it is not a reason to trust the double. It is also what makes the scoping above honest rather than convenient: the non-opt-in connection changes are real, and unexercised.

The @ts-expect-error: proven still needed, not assumed

Vacuity first: this package's tsconfig uses include: ["src"] and the test lives in src/. Proved with tsc --listFiles — the contract test is in the file set (present-control 1, absent-control 0).

Then the removal experiment, under a trap with the anchor asserted unique, the mutation proved on disk by a blob-hash delta plus a marker count going 1 to 0, and the restore proved byte-identical:

TYPECHECK_EXIT_WITHOUT_DIRECTIVE=2
src/redis.contract.test.ts: error TS7016: Could not find a declaration file for
module 'ioredis-mock' ... implicitly has an 'any' type.

The suppression has not rotted. ioredis-mock@8.13.1 ships no types field and no .d.ts. Its comment now also records the cost: with the module untyped the client is any and satisfies ioredis's Redis type without being checked against it.

Routes, including the ones not taken

  • Accept and declare — taken; the triage comment named this the honest terminal state for exactly this measurement outcome.
  • Adopt @types/ioredis-mock — not taken. It declares new(): ioredis.Redis: it asserts the mock is a real client rather than describing it, trading an honest any for an unearned certainty. ⭐ The review added a fact that strengthens this: the package is in the store but on no @types directory tsc walks from here, so it is not the near-free change it looks like.
  • Pin ioredis back to ^5 / real Redis in CI — both above the manual floor per the card's triage, and neither warranted by a measurement that found the gap inert.

Bounded in-place fix

The header promised RUN_REAL_REDIS=1 plus REDIS_URL and "conditional describe.skipIf blocks at the bottom". Scanning the whole package (not just src/): RUN_REAL_REDIS and skipIf occur nowhere outside that sentence, so the escape hatch never existed. REDIS_URL does occur, at README.md:42, as the env var a caller feeds to createRedisClient() — unrelated to any test path. The header now says exactly that. Same defect class as the card (the header misdescribing its own instrument), same file, same gate family.

Verification at a2bbec441

Gate family derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack from the actual changed file, never hand-built: 45 families, no STALE TREE warning, list identical to round 1. origin/main merged (no merge=os-regen path in the merge).

  • node scripts/check-adr-0087-registration.mjs --self-testexit 0 (325 assertions) — control.
  • node scripts/check-adr-0087-registration.mjs --base origin/main --head a2bbec441exit 0, no declared-breaking changeset.
  • 45 of 45 derived gates green, each exit captured right after a single redirected command. Two answered 3 (PREREQUISITE NOT MET) on the fresh worktree — check:dual-build-cjs-loads and check:type-check-debt; their prerequisite was built and both re-run green. Neither 3 is reported as a pass.
  • pnpm --filter @objectstack/service-cluster-redis test28 passed, exit 0. typecheck → exit 0 (which re-proves the directive is still "used").
  • Diff proved comments-only: every added and removed line is a comment line, with a control showing the filter admits code.
  • check:nul-bytes green plus a control-byte scan of the edited file, with a planted-byte control that fired.

Round-2 corrections

  1. "nowhere in this package" was false — the scan had been over src/, and REDIS_URL occurs at README.md:42. Re-scanned package-wide and the sentence now names each of the three strings by what it actually found.
  2. multi was listed among the verbatim RedisCommander commands. It is declared there in neither version (0 and 0, control exec 1 and 1) and its return type did change. Removed from the list and given its own measured bullet, including why it is inert here.
  3. "one substantive change reachable from this package" scoped to "the surface these suites drive", with the non-opt-in connection defaults named as changed-but-unexercised.
  4. Advisory: the expiry clause now also fires on a resolved-version move under an unchanged caret range (a lockfile-only bump).
  5. Advisory: the unmet-peer warning is scoped to a resolving install.

Out of scope, filed separately

#15983multi().del is never called, so RedisKV.delete's versioned branch is unexercised, and nothing produces the exec() === null WATCH-abort retry. #15986 tracks the pin that would defend this declaration mechanically.


🤖 Generated with Claude Code

https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y

…n the contract test header, and drop a promised live-Redis path that never existed

The contract test runs `ioredis-mock@8.13.1` (peer `ioredis: ^5`) against a
package depending on `ioredis@^6`, with the untyped import suppressed by
`@ts-expect-error`. This commit changes no code and no dependency: it writes
down the measurement that was missing, so the next reader starts where this
one finished.

Measured against ioredis 5.11.1 (newest release satisfying the mock's `^5`
peer) and 6.0.0 (the version resolved in this workspace):

  - all 14 Redis commands this package issues carry every v5 overload verbatim
    into v6's RedisCommander.d.ts; `set` is a strict superset there (v6 adds
    IFEQ/IFNE/IFDEQ/IFDNE), and nothing used here was removed or re-shaped
  - v6's RESP3 reply mapping is opt-in: the class defaults its ReplyMapping
    parameter to "legacy", ChainableCommander defaults to "resp2", and
    duplicate() with no override inherits the caller's mapping. This package
    never passes replyMapping, so every reply shape it sees is the v5 one
  - the three RedisOptions keys client.ts sets (lazyConnect,
    maxRetriesPerRequest, enableAutoPipelining) are declared identically

The peer mismatch is therefore real but inert on the surface these suites
drive. The header now says so, names the version pair the claim rests on, and
states that the claim expires if either range in package.json moves.

Also recorded: these suites reach the mock only through the injected client
and the pub/sub duplicate, never through createRedisClient(), so
`new Redis(url, options)` — this package's only contact with ioredis's
constructor and connection surface, and the area v6 changed most — is
exercised by nothing in this file. That is why the gap is inert here, and it
is not a reason to trust the double.

The header additionally promised `RUN_REAL_REDIS=1` + `REDIS_URL` and
"conditional describe.skipIf blocks at the bottom". No such blocks exist, and
both names occurred nowhere in the package outside that one sentence. The
false promise is removed rather than left standing as an escape hatch nobody
can take.

The `@ts-expect-error` stays. Removing it fails typecheck with TS7016 —
ioredis-mock ships no `types` field and no .d.ts of its own — so the
suppression has not rotted. Its comment now also records what it costs: with
the module untyped the client is `any` and satisfies ioredis's Redis type
without being checked against it, which is the second reason a v5-vs-v6
divergence could not surface here. `@types/ioredis-mock` would type the seam
but only asserts `new(): ioredis.Redis` rather than describing the mock, so
adopting it would trade an honest `any` for an unearned certainty; that trade
is left to a maintainer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y
@github-actions github-actions Bot added the size/s label Sep 5, 2026
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json f50c394da55846da8d38f1e1efadbc24faa26ce2packageMentionDocs.

Copy link
Copy Markdown
Collaborator Author

Clause-② contract review — PR #15985 (card #15467)

Tier. CONTRACT_REVIEW_TIER = 'claude-fable-5-1' (scripts/pm/dispatch-gates.mjs:9852). This call ran under an explicit model: fable override attested by the PM seat; my tier is override + self-report (self-reported as claude-fable-5-1).
Independence. Dev was a separate os-dev subagent on claude/issue-15467-ioredis-mock-major-seam; not the self-review case.
Subject. Head cacfac4d4, merge-base ceb48776 (re-derived against origin/main), one commit; one file, packages/services/service-cluster-redis/src/redis.contract.test.ts, +58/−3. Reviewed in a detached worktree /home/user/objectstack-review-15985 after a fresh pnpm install --frozen-lockfile (exit 0) and turbo run build --filter=@objectstack/service-cluster-redis... (exit 0).
Not redone (verified upstream by the PM seat): the comments-only nature of the diff and the five fences.

Verdict: NOT PASS as written. Every measurement the PR rests on re-derives and holds; three sentences in the shipped header state a wider population than was measured (§7). With those three line edits this is a PASS.

1. Intersection empty on the suite-driven surface — re-derived, holds

Population: ioredis 5.11.1 (npm view: newest of the 40 5.x releases; fetched via npm pack) vs 6.0.0 (lockfile-resolved, pnpm-lock.yaml:6962). My own declaration-level parser over built/utils/RedisCommander.d.ts (comment-stripped, brace/paren-aware ; split; normalising whitespace, quote style, (T)[]), not the dev's script.

member v5 v6 v5-only v6-only
get 1 1 0 0
set 36 84 0 48 (48/48 carry IFEQ/IFNE/IFDEQ/IFDNE)
del 4 4 0 0
incr / incrby / pttl / unwatch / publish / quit 1 each 1 each 0 0
watch 4 4 0 0
exec 1 1 0 0
subscribe 2 2 0 0
unsubscribe 3 3 0 0
eval 9 9 0 0
  • Zero v5-only overloads on any used command; RedisCommander member names 373 → 384, none removed.
  • Pipeline.d.ts byte-identical (cmp); so is utils/Commander.d.ts.
  • RESP3 mapping opt-in: class Redis<ReplyMapping extends ReplyMappingMode = "legacy"> (v6 Redis.d.ts:34); ChainableCommander<Mapping extends "resp2" | "resp3" = "resp2"> (RedisCommander.d.ts:17); duplicate<Override … = undefined>(override?) returns Redis<ReplyMappingFromOptions<ReplyMapping, Override>>, and ReplyMappingFromOptions falls through to Current when no replyMapping key is present (types.d.ts:11-13); runtime default replyMapping: "legacy" (RedisOptions.js:56). grep replyMapping src/ → only the new header.
  • The three keys: lazyConnect?: boolean | undefined, maxRetriesPerRequest?: number | null | undefined, enableAutoPipelining?: boolean | undefined — same text at v5 RedisOptions.d.ts:187/145/153 and v6 :212/170/178.
  • Normaliser controls, all fired: an NX-vs-XX set pair stays different; a cosmetic variant (quotes, newlines, (RedisKey)[]) collapses; a bracket-whitespace multi-line join of eval (the dev's self-caught bug class) collapses — eval reads 9/9 with no false REMOVED.

2. Census population — is this file the only mock driver? Yes

git grep -l ioredis-mock HEAD over every tracked file (lockfile excluded): package.json and this test file only. vi.mock('ioredis'…): 0. Files importing ioredis at all: this package's six src/*.ts plus one docs page. The carve-out "says nothing about other suites" is honest, and there are none.

Census re-taken with my own recording Proxy (mutation B): injected client → del, duplicate, eval, get, incr, incrby, multi, pttl, publish, set, unwatch, watch; the multi() chain → exec, set; the pub/sub duplicate → on, quit, subscribe, unsubscribe. Identical to the dev's 14 + 4. multi().del is absent, so #15983's basis reproduces.

3. createRedisClient() never called — verified by instrumentation, not by reading

Same mutation: vi.mock('./client.js') wrapping createRedisClient with a vi.hoisted counter. 28/28 passed under instrumentation; counter after all suites = 0. Positive control: calling createRedisClient({ existing }) through index.js's re-export in afterAll moved it to 1, so the counted binding is the one index.ts uses. Static corroboration: index.ts:67-69 reaches createRedisClient only when driverOptions.client is absent, and both defineCluster calls in the file pass client. Mutation proved on disk: marker CENSUS15985 0 → 7, blob b015f34fe76bfa3b; restored under trap, git diff HEAD 0 lines, blob back to b015f34f, marker 0.

4. Ablation re-taken — the suppression has not rotted, and it suppresses an implicit-any

Vacuity first: tsc --noEmit --listFiles at HEAD, exit 0 — present control redis.contract.test.ts = 1, absent control = 0, 7 src/ files in the set, ioredis-mock typings 0, ioredis@6.0.0 typings 25.
Mutation A: the anchor // @ts-expect-error — ioredis-mock has no published types occurs exactly 1× in the deleted form; blob b015f34fa978cb8d, marker 1 → 0; pnpm --filter @objectstack/service-cluster-redis typecheck > log 2>&1TYPECHECK_EXIT_WITHOUT_DIRECTIVE=2, src/redis.contract.test.ts(67,23): error TS7016: Could not find a declaration file for module 'ioredis-mock' … implicitly has an 'any' type. Baseline at HEAD: exit 0. Restore under trap … EXIT INT TERM, proved: git diff HEAD 0 lines, blob back to b015f34f, marker 1.
Also confirmed: the mock manifest has no types field and the tarball has 0 .d.ts; peer ioredis: ^5; @types/ioredis-mock@8.2.7 is in the store (the mock's own peer) but on no node_modules/@types directory tsc walks from this package — which is why the ablation yields TS7016 rather than a false pass — and its index.d.ts is exactly the new(): ioredis.Redis assertion the header describes.

5. Bounded fix — the promise was false and its removal is the whole edit

At merge-base the header (lines 8-9) promised RUN_REAL_REDIS=1 + REDIS_URL and describe.skipIf blocks. git grep at merge-base over the package: skipIf 0; RUN_REAL_REDIS only in that sentence. Repo-wide RUN_REAL_REDIS at HEAD: only the new parenthetical. The diff's only deletion is those three lines. See §7(a) for the one thing the replacement sentence gets wrong.

6. Falsifiability of the new header

Names the measured pair (5.11.1 / 6.0.0, mock 8.13.1) and says the paragraph expires if either package.json range moves — meets the bar. Advisory, non-blocking: both ranges are carets, so a lockfile-only bump (ioredis 6.x, mock 8.14) moves the resolved pair without moving the range; "or the resolved version" would close that. Not ruling on the pin (#15984).

7. Prose guard — three over-claims in the shipped header (blocking)

(a) L9-10, "the two names appeared nowhere in this package outside that sentence" — false for REDIS_URL: packages/services/service-cluster-redis/README.md:42, url: process.env.REDIS_URL ?? 'redis://localhost:6379'. The PR body says the scan was over src/; the header says "this package". Fix: "nowhere in this package's src/ outside that sentence".
(b) L22-25, multi listed among commands that "carr[y] all of [their] v5 overloads verbatim into v6's RedisCommander.d.ts"multi is declared in RedisCommander.d.ts in neither version (0 matches); it lives on Transaction (transaction.d.ts), and all four overloads changed return type ChainableCommanderChainableCommander<Mapping> (default "resp2"). The substance survives — your next bullet states the resp2 default — but the sentence is false for that member. Fix: drop multi from the verbatim list and add: "multi() (declared on Transaction) now returns ChainableCommander<Mapping>, whose Mapping defaults to resp2 — the v5 shape."
(c) L28-29, "v6's one substantive change reachable from this package is RESP3 reply mapping, and it is opt-in" — the package does reach the connection surface via createRedisClient in production, and there v6 changes defaults that are not opt-in: protocol: 3 (RedisOptions.js:55, @default 3; no such option in v5), keepAlive 0 → 30000 (RedisOptions.js:14 vs :17), reconnect jitter. Your own "do NOT certify" section then calls that surface "the area v6 changed most", which contradicts "one substantive change". The reply-mapping half is true (replyMapping: "legacy" by default). Fix: "v6's one substantive change reachable from the surface these suites drive is RESP3 reply mapping…" — scope it to what was measured.

Non-blocking wording: "so pnpm install prints an unmet-peer warning" — it prints on a resolving install (the card's evidence); a frozen re-link printed nothing here. "on a resolving install" makes it exact.

8. Self-caught errors — controls

The dev's diff script is not in the PR, so I did not verify their controls; I rebuilt both and a third (the bracket-whitespace regression) and all fired (§1). eval 9/9 confirms the corrected reading.

9. Sanity only — NOT MEASURED by me

node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack at the PR head: reconciliation 45 families (34 by path + 6 by kind + 7 whole-tree, 2 dual), plus the same 6 $RUNNER_TEMP/matrix invocations marked NOT RUNNABLE — matches the dev. STALE TREE warned on my detached head (2 commits behind origin/main: scripts/engine-double-contract.pinned.json, scripts/pm/check-half-states.mjs), consistent with the dev's pre-merge warning. NOT MEASURED: the 45 gate runs (the dev's 45/45 with the two 3 → green re-runs is not re-attested here); the 6 CI-only invocations; the @ioredis/commands 1.11.0 vs 2.0.0 table diff (both versions are in the store; contents not diffed).

Baselines at HEAD

pnpm --filter @objectstack/service-cluster-redis test → exit 0, 28 passed. typecheck → exit 0. Every exit captured immediately after a single redirected command.

Tree

Both mutations restored under trap; final git status --porcelain in the review worktree: 0 lines; test-file blob b015f34f3f6abf65bea561d328a52afdcf2b9503 = HEAD. Nothing pushed, no stash, primary checkout untouched.


🤖 Generated with Claude Code

https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y


Generated by Claude Code

…ces to the populations actually measured

Review found three sentences in the header whose stated population was wider
than the measurement behind them. All three are corrected here; the diff stays
comments-only (proved: every added and removed line is a comment line, with a
control showing the filter admits code).

1. The "no live-Redis path" parenthetical claimed both names appeared nowhere
   in the package. Re-scanned the WHOLE package rather than `src/`:
   `RUN_REAL_REDIS` and `skipIf` do occur nowhere outside that sentence, so the
   escape hatch genuinely never existed — but `REDIS_URL` occurs at
   `README.md:42`, as the env var a caller feeds to `createRedisClient()`. The
   sentence now says exactly that instead of over-reaching.

2. `multi` was listed among the commands carrying their v5 overloads verbatim
   into `RedisCommander.d.ts`. That is false for `multi` specifically: it is
   declared there in NEITHER version (measured 0 and 0, against a control
   member `exec` reading 1 and 1); it lives on `Transaction`. Measured on its
   real home, it DID change — all four overloads went from returning
   `ChainableCommander` to returning it parameterised by a reply mapping. It is
   removed from the verbatim list and given its own bullet stating why the
   change is inert here: the parameter defaults to "resp2", the class defaults
   to "legacy" and extends `Transaction` at "resp2", so a client built without
   `replyMapping` resolves `multi()` to the non-RESP3 instantiation.

3. "v6's one substantive change reachable from this package" was wrong in the
   other direction: v6 also changes connection defaults that are NOT opt-in —
   `protocol: 3` (no such option in v5) and `keepAlive` 0 to 30000. The
   sentence is scoped to "the surface these suites drive", which is what was
   measured, and the connection-default changes are named as
   changed-but-unexercised, since no suite here calls `createRedisClient()`.

Two further wording fixes, each closing a way the note could read as true when
it is not:

  - the expiry clause now also fires on a RESOLVED-version move under an
    unchanged caret range, which a lockfile bump alone will do; as written it
    expired only when a range moved
  - the unmet-peer warning is scoped to a resolving install, since a frozen
    re-link prints nothing

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y
@os-warren
os-warren marked this pull request as ready for review September 5, 2026 18:01
@os-warren
os-warren enabled auto-merge September 5, 2026 18:02

Copy link
Copy Markdown
Collaborator Author

PM verification — undrafted and armed

Verified at head a2bbec441d06d5604ceb162eafe98b89b52087e9. Still comments only: the non-comment, non-blank code-line filter over the whole diff returns empty, with the same filter run against a PR that did change code returning 164 lines as the control — so the empty result is a measurement, not a broken filter. check-adr-0087-registration exit 0 (0 non-breaking changeset(s) seen, consistent with skip-changeset on a comments-only test change), --self-test exit 0.

⚠️ A process error of mine, stated because it changes nothing but should not be invisible: I undrafted before the gate result was in hand. My first run returned exit 1 — which was Cannot find module '/home/user/scripts/check-adr-0087-registration.mjs', a lost working directory, not a gate verdict. Re-run with an explicit cd: exit 0. ⇒ The undraft was not harmed by it, but the ordering was wrong and the exit 1 is exactly the class of number that certifies nothing. Fourth cwd loss in this lane today; every Bash command carries its own cd and I did not.

The three fixes were re-measured, not reworded

(a) went further than I asked. I said "either scope the sentence to src/ or re-scan the package." It re-scanned and kept the fact: RUN_REAL_REDIS and skipIf occur nowhere outside the sentence — so the escape hatch genuinely never existed — while REDIS_URL does occur at README.md:42, as the env var a caller feeds to createRedisClient(). The header now names each string by what the scan found, rather than narrowing the claim and losing the distinction.

(b) is the one I flagged as not-a-word-move, and it was treated that way. Measured on multi's real home: it is in RedisCommander.d.ts in neither version (0 and 0, against control member exec reading 1 and 1), it lives on Transaction, and all four overloads did change return typeChainableCommanderChainableCommander<Mapping>. Inertness was then measured rather than inferred: ChainableCommander<Mapping extends "resp2" | "resp3" = "resp2">, and interface Redis<ReplyMapping … = "legacy"> extends Transaction<ReplyMapping extends "resp3" ? "resp3" : "resp2">, so a client built without replyMapping resolves multi() to the resp2 instantiation.

And it declined to overstate where the conclusion holds: v6's context gains a mapping field v5's lacks, so the header says "resolves to the non-RESP3 instantiation" — ⛔ not "identical to v5". That is the discipline this whole card is about, applied to the sentence fixing the sentence.

(c) confirmed before narrowing: v6's RedisOptions.js defaults protocol: 3 (v5 has 0 matches for protocol) and keepAlive 0 → 30000. Scoped to "the surface these suites drive", with the connection defaults named as changed-but-unexercised — which is what the second leg (createRedisClient() never called) actually proves.

Both advisories taken: the expiry clause now also fires on a resolved-version move under an unchanged caret range, and the unmet-peer warning is scoped to a resolving install.

⭐ A self-report worth more than the fix it concerns

I wrote the "both re-run green" line into the PR body before those two gates had actually re-run on this head. I caught it on read-back, held the claim open, and only left it standing once both returned 0 — but it was briefly a number I had not measured, and the safer order is to measure first and write after.

That is the eighth instance this session of a claim outrunning its evidence, and the first caught by its own author before it reached a reviewer. ⇒ The rule it names — measure first, write after — is the one that would have prevented most of the other seven, and it costs nothing.

Gate family re-derived at this head: 45 families, no STALE TREE, list identical to round 1; 43 green directly, with check:dual-build-cjs-loads and check:type-check-debt first answering 3 = PREREQUISITE NOT MET on an unbuilt worktree and re-running green after a full build — ⛔ neither reported as a pass while it was a 3. Package tests 28 passed, typecheck 0 (which re-proves the @ts-expect-error is still used). Still NOT MEASURED and correctly declared: the 6 $RUNNER_TEMP/matrix CI invocations dispatch-gates marks not runnable locally.

#15986 (the pin that would defend this declaration mechanically) is deliberately not folded in, and the lockfile advisory has been recorded there as widening what such a pin must cover.

Undrafted and auto-merge armed.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

CI red — measured NOT this PR's

Lint & Repo Gates is the only failing job on head a2bbec441 (run 33982466735, job 101350013279). The other 36 check runs on that head are green or skipped, Type Check · workspace included.

The failing step, named: Merge-driver wiring gatepnpm check:merge-drivernode scripts/check-regen-pending.mjs --self-test. The first half (git-merge-regen --self-test) passes in full — 18 routed paths, 338 check-attr agreements, both end-to-end merges. The second half fails from regeneration DISCHARGES the deferral and clears the marker onward, closing ✗ self-test failed -- 1 failure(s) (cases and floor).

The control that settles ownership. The identical step fails on main itself at f7db8f4fd — run 33981169123, job 101346594800, step 141, same name, same closing line. This PR is comments-only inside packages/services/service-cluster-redis/src/redis.contract.test.ts and touches no script, no .gitattributes row and no generated artifact.

Where it is owned. #15992 (p0, domain:devx, pm:dispatched), with #15990 and #15994 on the same failure. #15992 records the cause as the self-test's gate stubs shelling out to an unpinned pnpm -s, which Corepack now resolves to pnpm latest = 12.3.4, a version that rejects -s. ⚠️ Quoted from #15992, not measured here.

Measured here. node scripts/check-regen-pending.mjs --self-test run on this PR's own head a2bbec441 exits 0, 0 , ✓ check-regen-pending self-test passed. — on git 2.43.0. The same command at origin/main f7db8f4fd also exits 0. So the difference is the CI environment, not either tree.

Why no fix is ported into this PR. scripts/check-regen-pending.mjs is a single-writer path and No other open PR may claim the same single-writer path is a required check that is currently green on this PR; carrying the fix here would red it and collide with #15992's fix PR. This PR waits for #15992 to land, then takes it by merging main.

Why no re-run. An unpinned launcher resolving to a version that rejects a flag is deterministic, not a flake. The one re-run this PR is entitled to is left unspent.

State of this PR: the review's three population narrowings are in (a2bbec441). Nothing is waiting on the author.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Base branch recovered — taken, as the earlier comment said it would be.

main is green again at f50c394da — run 33986517218, conclusion success, so the Merge-driver wiring gate that reddened this PR is fixed at its source. The fix is #16002 (for #15992), and the cause it names matches the diagnosis quoted earlier: the self-test's throwaway fixture carried no packageManager, so Corepack resolved the registry's latest dist-tag afresh on every run, and when latest moved to a pnpm major whose CLI rejects -s, the stub exited 2 without running. ⭐ That is also why it passed locally here on exit 0 while red in CI — a different pnpm answered the fixture. The local run was never the control, which is why the base-branch job was.

Merged origin/main into this branch and pushed: a2bbec4417d26f995b. No rebase, no force-push.

Verified the merge did not disturb what was reviewed:

packages/services/service-cluster-redis/src/redis.contract.test.ts   SAME (blob unchanged)
scripts/check-regen-pending.mjs                                      DIFFERS  ← the firing control

One file came in, git merge reported no conflict, and a whole-tree scan for conflict markers found none. node scripts/check-regen-pending.mjs --self-test on the merge product: exit 0.

Auto-merge was already armed (18:02:06Z) and stays armed. Nothing here is waiting on the author.


Generated by Claude Code

@os-warren
os-warren added this pull request to the merge queue Sep 5, 2026
Merged via the queue into main with commit 5b41477 Sep 5, 2026
34 checks passed
@os-warren
os-warren deleted the claude/issue-15467-ioredis-mock-major-seam branch September 5, 2026 20:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

2 participants