Skip to content

fix(discovery): stop advertising a realtime service with no mounted surface, and define a subscribable channel once - #15978

Draft
claude[bot] wants to merge 7 commits into
mainfrom
claude/issue-14646-discovery-stops-advertising-unmounted-realtime
Draft

fix(discovery): stop advertising a realtime service with no mounted surface, and define a subscribable channel once#15978
claude[bot] wants to merge 7 commits into
mainfrom
claude/issue-14646-discovery-stops-advertising-unmounted-realtime

Conversation

@claude

@claude claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Closes #14646

Maintainer ruling A (director summon #14, 2026-09-04): realtime stays out of open core, /discovery stops advertising a realtime service that has no mounted surface, and "what counts as a subscribable channel" becomes one explicit definition. No realtime surface is implemented here.

The defect was a field with two meanings, not a wrong value

On a stock boot /api/v1/discovery reported the realtime slot as enabled: true and, in the same entry, "In-process event bus only — no HTTP/WS realtime surface is mounted", with no routes.realtime. Both statements were true, because enabled meant "the slot is filled" — which for an in-process pub/sub bus says nothing about whether anything is listening on the wire. A client reading it as "a channel exists" subscribed to nothing and lost the feature silently: no error, no failed request, no signal at all.

So flipping the boolean would have satisfied clause 2 and missed clause 3, leaving the next half-mounted service to reproduce the bug.

The definition

A subscribable channel exists only where discovery reports handlerReady: true together with a connectable route; enabled never means "there is a channel".

It is written and computed in exactly one place — isSubscribableChannel() in @objectstack/spec/api — and it is enforced rather than documented: both discovery producers set services.realtime.enabled and capabilities.websockets to the value of that call, so the field a consumer reads and the predicate a consumer is told to use are the same computation and cannot disagree.

capabilities.websockets was previously a literal false in each producer. Two constants that happen to agree are not agreement; they are two places to forget.

Where it applies, and where it deliberately does not

The predicate is applied per slot, to the slots whose advertised capability is a channel — CHANNEL_SURFACE_SLOTS, realtime alone. cache, queue and job deliver their whole contract in-process, so they stay honestly enabled: true with no route; making enabled mean "a route exists" globally would have retracted three working services. realtime keeps status: 'degraded' and its message, so a consumer can still tell "registered but no wire" from "not installed".

The route comes from the occupant (IRealtimeService.getChannelRoute(), new and optional), because no producer in the open framework mounts a realtime transport and neither builder can honestly invent one. Deliberately about the channel and not about a handshake: SSE mounts a plain GET and never upgrades, so gating on handleUpgrade would have refused a legitimate transport. @objectstack/service-realtime does not implement it, and a pin says so.

What changed

file change
packages/spec/src/api/discovery.zod.ts isSubscribableChannel(), readChannelRoute(), CHANNEL_SURFACE_SLOTS; enabled / websockets / routes.realtime descriptions say what they do and do not mean
packages/spec/src/contracts/realtime-service.ts optional IRealtimeService.getChannelRoute() — the producer half
packages/metadata-protocol/src/protocol.ts getDiscovery() derives the realtime entry, routes.realtime and capabilities.websockets from the predicate; the routes loop reads the built entry instead of re-deriving from the table
packages/runtime/src/http-dispatcher.ts same for getDiscoveryInfo(); routes.realtime is no longer a hardcoded undefined
packages/services/service-realtime/src/realtime-service-plugin.ts comment only: why the shipped occupant names no channel route
docs protocol/kernel/realtime-protocol.mdx carries the definition and a do-not-key-on-enabled callout; kernel/services-checklist.mdx rows updated; content/docs/references/** regenerated

Red first, then green — both directions

Ablation: both producer files restored to the merge base d4c2cb196 (fix committed first; restore trap with absolute paths; marker counts proved the mutation landed — isSubscribableChannel 5 → 0 in protocol.ts, 4 → 0 in http-dispatcher.ts; restore verified by blob hash and an empty git diff HEAD).

RED, metadata-protocol/src/discovery-realtime-channel.pin.test.ts — exit 1, 3 failed / 2 passed:

  • does NOT advertise an in-process realtime bus as a channelexpected true to be false
  • DOES advertise a realtime occupant that mounts a channelexpected 'degraded' to be 'available'
  • answers enabled, routes.realtime and capabilities.websockets with the SAME predicateservices.realtime.enabled: expected true to be false

RED, runtime/src/discovery-realtime-channel.pin.test.ts — exit 1, 3 failed / 1 passed, the same three assertions.

GREEN on this branch: both files pass. The positive pin is kept deliberately: without it the fix would be indistinguishable from "never advertise realtime", which is another hardcode rather than a definition. ⚠️ The producer pins drive a declared stand-in, not the shipped InMemoryRealtimeAdapter — see "Two CI failures" below for why, what it cost, and where that claim is pinned instead.

Verification

Suites and typechecks run at ff617529c; the ratchet-class gates, the two CI repairs below and their pins were re-run at the final head 3c22dddf2. Exit codes captured before any pipe.

command exit
pnpm --filter @objectstack/metadata-protocol exec vitest run src/discovery-realtime-channel.pin.test.ts src/discovery-schema-conformance.test.ts 0 — 27 passed
pnpm --filter @objectstack/runtime exec vitest run src/discovery-realtime-channel.pin.test.ts src/http-dispatcher.test.ts src/discovery-schema-conformance.test.ts 0 — 274 passed
pnpm --filter @objectstack/metadata-protocol test (full package) 0 — 2391 passed, 10 skipped (166 files)
pnpm --filter @objectstack/service-realtime test (full package) 0 — 31 passed
pnpm --filter @objectstack/objectql test (full package) 0 — 4714 passed (273 files)
pnpm --filter @objectstack/runtime test (full package) 0 — 3295 passed (232 files)
pnpm --filter @objectstack/dogfood exec vitest run test/authz-probe-blind-spot.test.ts 0 — 33 passed
node scripts/check-type-source-resolution.mjs (+ --self-test) 0 — 125 programs / 78 packages, registry unwidened
pnpm --filter @objectstack/rest exec vitest run over the five discovery-* suites, against a rebuilt metadata-protocol dist 0 — 40 passed
pnpm --filter PKG typecheck for @objectstack/spec, @objectstack/metadata-protocol, @objectstack/runtime, @objectstack/objectql, @objectstack/service-realtime 0 (each; runtime red once on two TS2339 in the new pin file, fixed by annotating the dispatcher's realtime entry ServiceInfo, then 0)
pnpm --filter @objectstack/spec run check:generated 0 after regenerating api-surface/, export-origins/, content/docs/references/**
check:api-surface · check:authorable-surface · check:export-origins · check:exported-any · check:dual-source-exports · check:entry-nameability · spec check:docs 0 (each)
check:nul-bytes · check:cross-package-test-inputs · check:test-source-alias · check:service-providers · check:merge-driver 0 (each)
check:doc-anchors · check:doc-authoring · check:docs-single-h1 · check:docs-redirects · check:docs-audit-scope · check-doc-frontmatter · check-docs-section-name 0 (each)
check-empty-changeset --base origin/main · check:objectui-changeset 0

Lint is CI's repo-wide run; the narrowing here is measured rather than assumed. pnpm lint is eslint . --no-inline-config over every file the one root eslint.config.mjs declares — it registers no .mdx/.md/.json handling, so the lintable intersection of this diff is exactly its .ts/.mjs paths. npx eslint --no-inline-config --format json on those reported 11 files linted, 0 errors, 0 warnings, exit 0 (captured before any pipe). Untouched files cannot have moved: that config's own header records, with a positive control, that it "never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for ANY file".

api-surface delta: 3 added, 0 removed / narrowed (CHANNEL_SURFACE_SLOTS, isSubscribableChannel, readChannelRoute).

Gate family derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack from the real change set (16 paths vs merge base), not from a hand-written list. The repo-wide scans (pnpm lint, the full 55+ gate farm) are CI's run.

Published-docs drift — per-page verdicts

Six hand-written pages were flagged. Each was read per file at a known clean sha (ff617529c, git status --porcelain empty), not by a corpus-wide grep; every zero-hit claim below was taken from a command whose positive control fired on the same file and scope.

page verdict
content/docs/protocol/kernel/realtime-protocol.mdx Was falsified — fixed in this PR. Its Connection Endpoint section printed "enabled": true for realtime and promised "discovery will advertise routes.realtime again". Rewritten: the payload now shows enabled: false plus capabilities.websockets, and a new "What counts as a subscribable channel" section carries the definition and a do-not-key-on-enabled callout. Re-scanned after the edit: every remaining advertis/discovery/enabled: true hit on the page (7) is inside that rewritten block; control subscribe = 51 hits. The rest of the page documents a planned transport and stays true because nothing was mounted.
content/docs/kernel/services-checklist.mdx Was falsified in three places — all fixed. (1) the realtime callout said the slot reports degraded while routes.realtime is "never advertised"; (2) the closing capability bullet said "websockets is hardcoded false" — it is now derived, so that sentence is simply wrong; (3) the general rule "the services map still reports a registered stub as { enabled: true, … }" now has exactly one exception, since a registered realtime stub reads enabled: false. All three rewritten, the exception declared rather than left implicit. hardcoded no longer appears on the page.
content/docs/ai/knowledge-rag.mdx Null. One hit (line 76): IRealtimeService events drive KnowledgeService.handleRecordUpsert/Delete. That is in-process publish/subscribe fan-out; this change adds one optional member and alters neither delivery nor how discovery describes in-process delivery. No edit.
content/docs/automation/webhooks.mdx Null. Every hit is about IRealtimeService.publish(event) fan-out and InMemoryRealtimeAdapter being single-node and unpersisted (lines 225, 255, 332–335, 358, 440, 459, 610, 630) plus one line contrasting WebSocket fan-out as a different transport (722). All still true: no transport was wired and the adapter is unchanged. No edit.
content/docs/kernel/contracts/index.mdx Null. Single hit (line 56), a one-row table describing IRealtimeService as a publish/subscribe event system. The page does not enumerate the interface's members, and an additive optional member does not change that description. No edit.
content/docs/plugins/packages.mdx Null. Hits at 276–283 say service-realtime is in-process pub/sub, that there is no WebSocket/SSE endpoint and no client transport, and that handleUpgrade is deliberately unimplemented. All still exactly true — this change mounts nothing, and the shipped adapter implements neither handleUpgrade nor the new getChannelRoute. No edit.

content/docs/releases/v16.mdx was flagged via WellKnownCapabilitiesSchema and is release-owned and read-only — not opened for writing. Read-only verdict: not falsified, nothing for anyone to fix. Its two hits are historical: line 239 records that schema-only surfaces were labelled "not yet enforced" in #3197 (those labels are untouched), and line 779 records that WellKnownCapabilitiesSchema gained transactionalBatch in v16. This change adds no capability key; it changes how websockets is computed, not the vocabulary.

Filed rather than folded in: #15982 — the same capability bullet in services-checklist.mdx also names the retired features map spelling (#4828 renamed it to capabilities) and the retired workflow slot (#4451). Both were wrong before this change and are not in its scope, so only the clause this change falsified was corrected here.

Scope against the ruling, file by file

Ruling A is (a) realtime stays out of open core, (b) /discovery stops advertising an unmounted realtime service, (c) "what counts as a subscribable channel" becomes one explicit definition.

file doing serves
packages/spec/src/api/discovery.zod.ts isSubscribableChannel(), CHANNEL_SURFACE_SLOTS, readChannelRoute(); enabled / websockets / routes.realtime descriptions (c)
packages/spec/src/contracts/realtime-service.ts optional getChannelRoute() — the producer half of the definition (c), and (a): it is the seam by which a non-open-core host advertises, so open core needs to mount nothing
packages/metadata-protocol/src/protocol.ts getDiscovery() derives the realtime entry, routes.realtime and capabilities.websockets from the predicate (b) + (c)
packages/runtime/src/http-dispatcher.ts same for getDiscoveryInfo() (b) + (c)
packages/services/service-realtime/src/realtime-service-plugin.ts comment only, no behaviour: why the shipped occupant names no channel route (a)
packages/metadata-protocol/src/discovery-realtime-channel.pin.test.ts the two-direction pin for the getDiscovery producer (b) + (c)
packages/runtime/src/discovery-realtime-channel.pin.test.ts the same for the dispatcher (b) + (c)
packages/services/service-realtime/src/no-channel-route.pin.test.ts pins that the shipped occupant names no channel — makes (a) a decision, not an omission (a)
packages/objectql/src/protocol-discovery.test.ts existing realtime pin updated from enabled: true to false (b)
packages/runtime/src/http-dispatcher.test.ts the same, in the dispatcher's D12 suite (b)
packages/qa/dogfood/test/authz-probe-blind-spot.census.ts re-measured control count, moved by the comment above consequence of (a)
content/docs/protocol/kernel/realtime-protocol.mdx, content/docs/kernel/services-checklist.mdx the published pages this change falsified (b) + (c)
content/docs/references/api/discovery.mdx, content/docs/references/api/protocol.mdx, packages/spec/api-surface/api.json, packages/spec/export-origins/api.json generated — pnpm --filter @objectstack/spec run check:generated --fix, never hand-edited consequence of (c)
.changeset/discovery-subscribable-channel-definition.md minor changeset with the client note the ruling asked for required by the repo

No realtime transport, route, handler or client primitive is added anywhere; the only new members are the predicate, its slot set, its reader, and the optional contract method that lets a host outside open core answer it.

Two CI failures on 04b60ff7d, and what the second one cost

1. Dogfood Regression Gateauthz-probe-blind-spot.test.ts. The comment added to realtime-service-plugin.ts moved a pinned exact control count: the census pattern is a bare /RealtimeService/g, so it matches inside IRealtimeService and prose about the symbol moves the symbol's count exactly as code does. Reproduced locally (exit 1, 1 failed / 32 passed), then re-measured in the ledger 10 → 11, which is what that pin is for — the comment was not reworded to push a counter back. Green after: exit 0, 33 passed. The row now carries a note for the next person who documents that file.

2. Lint & Repo Gatescheck-type-source-resolution. The gate offers two remedies and refuses to choose between them from inside the failure. The discriminating question is whether the change ONBOARDED the program the dep is reached through. Measured against origin/main, not recalled: packages/runtime's typecheck script names tsconfig.test.json byte-identically before and after this diff (the diff added one devDependency line and no script), so the (via tsconfig.test.json) program was already counted. That is the registry doc-block's "dep newly reached through a program that was ALREADY counted", i.e. the exposure the ratchet exists to catch — the re-baseline limb is closed and widening the entry was not available.

Which remedy was taken: neither. paths was tried first, as the gate instructs, and measured: it cleared check-type-source-resolution (exit 0, registry unwidened) and then billed 13 TS6059 from @objectstack/service-realtime's file graph into packages/runtime's test-typecheck ledger — a ledger that package cannot see, the shape PR #12570 measured — because the mandated rule pulls another package's source into a program whose rootDir is ./src, which tsconfig.test.json states in its own header it will not widen. Its remedies are "fix them" (they are not code errors) or a maintainer-only ledger expansion. So the dist-resolved import was removed entirely: the devDependency, the vitest alias and the lockfile change are all reverted to the merge base, and the runtime pin declares its occupant locally.

What that cost, measured rather than argued. The runtime pin no longer drives the shipped adapter — it drives a stand-in, in those words. One mutation, two pins: giving InMemoryRealtimeAdapter a getChannelRoute() returning /api/v1/realtime (mutation proved on disk, marker 0 → 1 and blob changed; restore proved by blob equality and an empty git diff HEAD) reddens service-realtime's no-channel-route.pin.test.ts (exit 1, 2 of 3 cases) and leaves the runtime pin green on all 4. So the assertion that lost evidential value is exactly the first one — "does NOT advertise an in-process realtime bus as a channel" no longer says anything about the shipped occupant.

It is covered, not merely moved. That same mutation is what proves it: the shipped-occupant claim is pinned against the real class, in the package that owns it, and that pin has teeth. The two pins divide the reading — one pins what the shipped occupant names, the other that the producer derives from whatever an occupant names — and neither can go green by accident of the other. The stand-in is equivalent to the real adapter only on the two reads either producer performs on an occupant (readChannelRoute, readServiceSelfInfo — both undefined on both objects, measured), which is a statement about today and precisely why the shipped-occupant claim lives next door. All of this is written at both code sites and in the changeset, not only here.

Contract review

Clause-② : yes — re-declared from the diff, and it agrees with the ruling's own declaration. The published /discovery payload changes what it says for realtime, and @objectstack/spec gains three exported members plus one optional interface member on a published contract face. Additive in shape, changed in value. needs:contract-review is on this PR and on the card, and per the dispatch this PR parks: not flipped ready, auto-merge not armed.

Client note

A client that keyed on services.realtime.enabled to decide whether to subscribe now sees false and should poll or degrade. To ask whether the slot is occupied at all, read status ('unavailable' = nothing registered, 'degraded' = registered but reduced) — that is the question enabled was answering for this slot before. Cross-repo: this is the "the answer is poll" half of objectstack-ai/objectui#7249, which is not addressed here.


Generated by Claude Code


Generated by Claude Code

…h discovery producers

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
…ion; docs + changeset

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
@github-actions github-actions Bot added size/l dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation tests tooling labels Sep 5, 2026
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/metadata-protocol, @objectstack/runtime, @objectstack/service-realtime, @objectstack/spec, touching 13 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (packages/spec/api-surface/api.json, packages/spec/export-origins/api.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/ai/knowledge-rag.mdx (via IRealtimeService (symbol, a top-level interface))
  • content/docs/automation/webhooks.mdx (via IRealtimeService (symbol, a top-level interface))
  • content/docs/kernel/contracts/index.mdx (via IRealtimeService (symbol, a top-level interface))
  • content/docs/kernel/services-checklist.mdx (via CHANNEL_SURFACE_SLOTS (symbol, a top-level const object), IRealtimeService (symbol, a top-level interface), getDiscovery (symbol, a method of class ObjectStackProtocolImplementation), isSubscribableChannel (symbol, a top-level function))
  • content/docs/plugins/packages.mdx (via IRealtimeService (symbol, a top-level interface))
  • content/docs/protocol/kernel/realtime-protocol.mdx (via IRealtimeService (symbol, a top-level interface), RealtimeServicePlugin (symbol, a top-level class), getChannelRoute (symbol, a method of interface IRealtimeService), isSubscribableChannel (symbol, a top-level function), /api/v1/realtime (route, a path literal in ApiRoutesSchema; a path literal in IRealtimeService))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx (via WellKnownCapabilitiesSchema (symbol, a top-level const))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/spec/api-surface/api.json, packages/spec/export-origins/api.json) — pages documenting those are invisible to this run
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • 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 — 137 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 f7db8f4fd268a86a08c62ae4894cf7417720f8c9packageMentionDocs.

Which tree this was computed on

This run read content/docs from a6c3e5a9d76f9c9323c8bba2aac91a7ac347c067 — the merge of head 3c22dddf2ec9e7233cfd5de8c7d68562cab28bf1 into base f7db8f4fd268a86a08c62ae4894cf7417720f8c9, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin a6c3e5a9d76f9c9323c8bba2aac91a7ac347c067 && git checkout a6c3e5a9d76f9c9323c8bba2aac91a7ac347c067
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f7db8f4fd268a86a08c62ae4894cf7417720f8c9 3c22dddf2ec9e7233cfd5de8c7d68562cab28bf1 && git checkout -B drift-repro f7db8f4fd268a86a08c62ae4894cf7417720f8c9 && git merge --no-ff 3c22dddf2ec9e7233cfd5de8c7d68562cab28bf1

node scripts/docs-audit/affected-docs.mjs --json f7db8f4fd268a86a08c62ae4894cf7417720f8c9

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs f7db8f4fd268a86a08c62ae4894cf7417720f8c9 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…e test's cross-package source dep

The dogfood census pins per-file control counts exactly; the comment added to
realtime-service-plugin.ts moved `RealtimeService` 10 -> 11 because the pattern
matches inside `IRealtimeService`. Re-measured in the ledger rather than
reworded in the file.

The runtime pin's import of the real InMemoryRealtimeAdapter made
@objectstack/runtime type-resolve @objectstack/service-realtime through dist.
Measured against origin/main: this package's typecheck script already named
tsconfig.test.json, so the program was already counted and the registry's
re-baseline limb does not apply; the mandated `paths` remedy then billed 13
TS6059 from another package's file graph into this package's ledger, against a
rootDir tsconfig.test.json states it will not widen. Edge removed instead. The
claim about the shipped occupant stays pinned against the real class in
service-realtime's own suite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
Measured, one mutation and two pins: giving the shipped InMemoryRealtimeAdapter
a getChannelRoute() reddens service-realtime's no-channel-route pin (2 of 3
cases) and leaves the runtime producer pin green on all 4. So the shipped-
occupant claim is covered next door, not lost — and the runtime pin's stand-in
is equivalent to the real adapter only on the two reads a producer performs,
which was measured rather than assumed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
@claude

claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

Standing down on the red Lint & Repo Gates: not this PR's — it is the third PR hit by the gate defect filed as #15990.

Lint & Repo Gatescheck:merge-driverscripts/check-regen-pending.mjs --self-test, job 101351691342, on the current head 3c22dddf2. Same case set and the same ✗ self-test failed -- 1 failure(s) (cases and floor) summary seen on #15980 and #15996.

Mechanism (measured on #15980, filed as #15990): the self-test writes a stub package.json into a temp dir under tmpdir() with only name and scripts, then runs it through ownerRunCommand = pnpm <script>. That dir declares no packageManager and has no parent manifest, so the verdict depends on an ambient pnpm resolving there. When it cannot, every stub collapses to "gate exited non-zero", read as stale — so exactly the stubs expecting a non-stale verdict go red. A 2×2 established that origin/main fails identically under the same condition: the verdict is a function of the runner, not the tree.

New datum from today, worth recording here: the failure is flaky per job, not per PR. #15980's re-run reproduced it byte-identically; #15996's re-run cleared it. So this is not a property of any diff — it is whether the launcher resolves in that particular job.

Severity bound: false-RED only, never false-green — a failing launcher makes every artifact look stale, and stale is the refusing verdict.

No fix to port: #15990 is dispatched and in flight; no patch exists yet to carry into this PR.

⛔ No test skipped, disabled or quarantined; ⛔ no empty commit. Spending this PR's one permitted re-run, which is unspent.

Unrelated to the park: this PR remains Clause-②: yes and parked for an at-tier contract review — the CONTRACT_REVIEW_TIER has returned HTTP 429 on five probes today (15:55, 16:44, 17:31, 17:52, 18:10). Getting CI green does not release it. ⛔ No ready flip, ⛔ no arm.


Generated by Claude Code

@claude

claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

Correction to this seat's own comment above: the "re-run cleared it" rows were WRONG, and the mechanism I gave was wrong too.

1. No re-run cleared anything. I reported #15996's and #15978's re-runs as having cleared the failure. They had not. Measured just now, their Lint & Repo Gates check-runs read status=in_progress, conclusion=null — the re-run had replaced the failed check-run with an in-progress one. My filter counted only runs that were completed-and-failed, so an in-progress run fell out of the "failing" bucket and I read that absence as a pass. My own output was showing running=1 beside it and I read past it. A count of failures is not a reading about success while anything is still running.

2. The mechanism I described — "an ambient pnpm fails to resolve" — is falsified. Measured by the round on #15990: it resolves and runs. The real chain:

  • .github/actions/setup-pnpm materialises only the pinned pnpm 10.31.0 and writes no lastKnownGood.json.
  • The fixture's tmpdir pins nothing, so Corepack ignores the repo pin and resolves pnpm's latest from the registry (measured: it downloads 12.3.4).
  • pnpm 12's Rust CLI rejects -s with exit 2 — that, not a missing launcher, is what makes every stub look like "gate exited non-zero" ⇒ stale.
  • Corepack then writes lastKnownGood.json, and it is sticky: the warm re-run costs ~116 ms with no network, matching the failing job's cadence, and the job log shows a Corepack cache hit with no download line.
  • actions/cache@v6 saves COREPACK_HOME back on a miss, so a job restores a store holding either a -s-accepting pnpm (green) or a pnpm 12 (red).

That last point is what my "flaky per job" story could not explain and this one does: flaky ACROSS jobs, byte-identical WITHIN one.

3. origin/main is red on its own push build, which settles the question properly and without re-run archaeology: run 33981169123 (Lint & Type Check, event=push, head_branch=main, head_sha=f7db8f4fd) concluded failure at 17:51Z with the byte-identical signature. main and the four PRs I called green were sampled before that crossover, not exonerated by their trees.

The standing-down conclusion is unchanged and is now better supported: this failure is not this PR's, it can only false-RED, and #16009 takes the launcher dependency out of the fixture. What changes is that my evidence for it was partly wrong, and the corrected evidence is stronger.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation needs:contract-review size/l tests tooling

Projects

None yet

1 participant