Skip to content

fix(scripts): close two measured blind spots in check:optional-error-sink - #11549

Merged
os-steve merged 4 commits into
mainfrom
claude/issue-11069-error-sink-blind-spots
Aug 24, 2026
Merged

fix(scripts): close two measured blind spots in check:optional-error-sink#11549
os-steve merged 4 commits into
mainfrom
claude/issue-11069-error-sink-blind-spots

Conversation

@claude

@claudeclaudeBot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Fixes#11069

Two narrowings in scripts/check-optional-error-sink-contract.mjs that the gate neither argued nor counted, so its own census line overstated what it had read. Both are repaired; the gate's exit code on a clean tree is unchanged (0), and nothing about what it enforces changes.

Route taken for blind spot (1): triage's — recognise the catch-all callable into the population and baseline the newly visible reds. The discriminator the claim thread asked me to check: this gate does have a ledger (scripts/optional-error-sink-contract.baseline.json) and it is shrink-only — a baselined entry that is no longer red fails the gate ("delete the entry"), and there is deliberately no --fix/--update flag. So a baseline entry is the durable record, and the third-bucket fallback was not needed.

What moved

Measured on cd932772 (the branch base), same tree throughout:

populationrednoErrorMember tallyunreadablegate exit
base382 (2 baselined)570
+ prefilter fix only3829500
+ Function limb only4155831
this PR (both)415 (5 baselined)9630

The two fixes are independent and additive: 57 + 38 + 1 = 96.

1. Bare-Function sinks escaped the population entirely

error?: Function set fn = false, which both hid the error member from the population lookup and made the whole shape impure — so such a sink landed in no bucket: not the population, not impure, not noErrorMember. isFunctionTyped now reads bare Function as a channel.

Function is the only catch-all spelling live in the tree (14 members, 4 files, all plugin-sharing) — (...args: any[]) => any is already a FunctionTypeNode and needed no case.

Three red sinks were hidden this way, not two. The card named two from a hand search; widening the matcher found a third:

filesinkexported from index.ts?
sharing-service.ts:280logger@SharingServiceOptionsyes
share-link-service.ts:345logger@ShareLinkServiceOptionsyes
sharing-rule-service.ts:76logger@SharingRuleServiceOptionsyes — not named on the card

All three are baselined, not flipped. All three options types are publicly exported, so making warn required breaks any host passing { info, error } — that is #10556's contract call, out of scope here. Each ledger row says so, names the widening, and stays shrink-only. The third one is itself the argument for the structural fix: a hand-listed set of shapes is exactly what a structural population exists to replace.

⛔ No file under packages/plugins/plugin-sharing is touched by this PR.

2. The noErrorMember tally was undercounting by 40%

The prefilter matched /\berror\s*\??\s*[:(]/ — sound for the enforced population (a sink declaring error must spell the token), unsound for the census line that counts sinks declaring noerror, which is exactly the set of files with no reason to contain it.

It is now derived from LOG_CHANNELS rather than written out, which makes it sound for every bucket: every shape this gate records in any bucket has at least one identifier-named member from the vocabulary, spelled name:, name?:, name( or name?( — all four covered. So the tally is a count again, not a lower bound of unknown slack. Deriving it also means adding a channel widens it automatically; a hand-written second copy of the vocabulary is how the previous narrowing outlived its reason.

Verified by measurement, not only by that argument: removing the prefilter entirely and parsing all 1914 files produces a byte-identical 146-line --list census. The prefilter still skips 1337/1914 files (69.9%), 2.8s vs 3.7s unfiltered.

The specimen the card identified is the control: packages/plugins/plugin-sharing/src/logger-shapes.ts declares one pure { info?, warn? } sink and contains no error token anywhere (verified: the old prefilter returns false on it). It was skipped before the parser saw it; it now appears in the tally. --list also lists the tally's members now — it was the one census line whose shapes were unnameable, and an unauditable count is what let a 40% undercount sit in this output unnoticed.

3. The reject side of (1), counted rather than argued

isFunctionTyped reads syntax and this gate has no type checker (deliberately — a detector with no program to build cannot fail to build one in CI). So warn?: Logger['warn'] is still opaque to it. Rather than let those vanish — the exact defect this card is about — a shape whose members are all channel names but which carries such a type is counted as unreadable and listed.

Measured cost today: zero missed reds. Two of the three are import-coerce.ts's { error: FieldCoerceError } result envelopes with error REQUIRED; one is suspended-run-store.ts's MinimalLogger = { warn?: Logger['warn']; debug?: Logger['debug'] }, which declares no error. Keyword types (string, unknown, boolean) are deliberately not counted — those are result fields, unambiguously not callable.

Non-vacuity — ablations, mutation proven on disk both ways

Each leg: anchor counted before (must be 1) and after (must be 0), injected marker counted (must be 1), git hash-object before/after, trap restore EXIT INT TERM, byte-identical restore verified.

ablationobservedrestore
A — remove the bare-Function limbself-test 3 cases red; population back to 38, red back to 2byte-identical ✓
B — revert the prefilter to the old error-only regexself-test 1 case red (call-site pin); tally back to 58byte-identical ✓
C — drop the unreadable bucketself-test 2 cases red; the 3 shapes silently vanish while the gate still exits 0byte-identical ✓

Two process notes, both worth more than the numbers:

  • Ablation B initially left the self-test GREEN. The prefilter pins all tested the constant; none tested the scan. A pin that cannot observe the revert it exists to prevent is this card's own defect one level up, so the scan now asserts it routes through CHANNEL_PREFILTER.
  • That new pin then also stayed green under the same ablationFunction.prototype.toString() returns comments, and the comment above the call site names CHANNEL_PREFILTER in prose. A pin satisfied by a sentence describing the code is satisfied by deleting the code and keeping the sentence. Comments are now stripped first. Only after that did leg B go red (third commit).
  • Leg C's first run was invalid, not passing: a // marker commented out the closing }; and the leg measured a syntax error. Re-run with a block comment; the table above is the valid run.

Verification

Gate union derived from the real change set via node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (2 paths, merge-base cd932772), all run at final HEAD ed579483:

gateexitverdict line
pnpm check:optional-error-sink0✓ … (5 baselined, shrink-only) + self-test ✓ 19 case(s)
pnpm check:cross-package-test-inputs0
pnpm check:entry-guard0✓ check:entry-guard: 139 scripts/ file(s) …
pnpm check:parse-guard0✓ check:parse-guard: 138 scripts/ file(s) …
pnpm check:pnpm-filter-targets0✓ check:pnpm-filter-targets: 120/148 …
node scripts/check-ci-filter-parity.mjs0
node scripts/check-cross-package-test-inputs.mjs0
pnpm check:nul-bytes0✓ … no raw ASCII control bytes

Also ran the two artifact-roster families the derivation flagged as sitting in a directory one of my paths is in (check:pm-label-desc-cap, check-aggregator-roster.mjs) — both exit 0. Exit codes captured before any pipe.

Lint — declared narrowing, with the measurement. Not the repo-wide run; the shared verify lock was held with a queue. Three evidences:

  1. Receiving population read from eslint's own config (ESLint#isPathIgnored over git ls-files): 4958 of 6465 tracked files. scripts/check-optional-error-sink-contract.mjs is in it; scripts/optional-error-sink-contract.baseline.json is ignored by eslint, so excluding it excludes nothing.
  2. File count from --format json: 1 file linted, 0 errors, 0 warnings (eslint --no-inline-config).
  3. Invariance for untouched files: this repo runs one eslint.config.mjs which never enables type-aware linting for any file (no parserOptions.project, no typed @typescript-eslint rules) — stated and measured with a positive control in that config's own header. So this diff cannot move the verdict on any file it does not contain.

Also in this branch: the header's Wiring section said UNWIRED

Found while checking whether the exit-code requirement was load-bearing. The script's ## Wiring section said the gate was "NOT wired into .github/workflows/lint.yml" and that wiring it "is a lane-PM decision, recorded on #9754 rather than taken here". It has since been wired: lint.yml runs pnpm check:optional-error-sink on every pull request, with no paths: filter. So a red here blocks, and "exit code on a clean tree stays 0" is a load-bearing requirement rather than a courtesy.

Corrected in place — same file, same defect class as the two blind spots (a claim nothing re-measures), no new verification surface.

One further drift found and deliberately not fixed here: the lint.yml step comment for this gate states 13 cases, 36 sink types, 15 baselined — all three now wrong, and 15 baselined has been wrong since #10556's paydown. Editing lint.yml would add a hot conflict-magnet file and new gate families to a scripts/**-only PR, so it is filed as #11551 (unassigned, finding) with three options for what to do about numbers living in a workflow comment at all.

Scope


Generated by Claude Code


Generated by Claude Code

…sink
Both were narrowings the gate neither argued nor counted, so its own census
line overstated what it had read.
1. A sink spelled with bare `Function` set `fn = false`, which hid its `error`
from the population lookup AND made the shape impure, so it landed in no
bucket at all — not the population, not `impure`, not `noErrorMember`.
`isFunctionTyped` now reads `Function` as a channel: population 38 -> 41,
red 2 -> 5. All three newly visible sinks are pre-existing reds in
plugin-sharing whose options types are publicly exported, so they are
recorded in the shrink-only ledger rather than flipped; tightening `warn`
there is #10556's contract call.
2. The file prefilter matched on `error` alone — sound for the enforced
population, unsound for the `noErrorMember` census line, which counts sinks
declaring NO `error`. It is now derived from the whole channel vocabulary,
so the tally is a count rather than a lower bound of unknown slack: 57 -> 95
on an unchanged tree.
The reject side of (1) gets its own counted bucket, `unreadable`: a shape whose
members are all channel names but whose member type is a named type this
syntactic matcher cannot resolve. Measured cost today is zero missed reds.
Gate exit code on a clean tree is unchanged (0).
Found by ablation: replacing the call site with a literal `/\berror.../` -- the
exact regression this PR repairs -- restored the undercount (tally 96 -> 58)
while every prefilter pin stayed green, because they all test the constant and
none of them tests the scan.
Part of #11069
The pin as first written tested `run.toString()` directly and stayed green
under the very ablation it exists for: `toString()` returns comments, and the
comment above the call site names CHANNEL_PREFILTER in prose. A pin satisfied
by a sentence describing the code is satisfied by deleting the code and
keeping the sentence.
Part of #11069
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 24, 2026
…ks on every PR
Not wired by the PR that introduced it, and wired since -- but the paragraph
saying wiring 'is a lane-PM decision, recorded on #9754 rather than taken here'
outlived the decision being taken. It runs from lint.yml on every pull request
with no paths filter, so a red here blocks and the clean-tree exit code is
load-bearing rather than informational.
Same species as the blind spots this branch repairs: a claim nothing
re-measures.
Part of #11069
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

check:optional-error-sink has two measured blind spots: bare-Function sinks escape the population, and the no-error census tally undercounts

1 participant

@os-steve