Skip to content

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

Description

@os-warren

Observation filed while landing PR #11068 (the #10692 logger-shape de-duplication). Not a claim; unassigned. Not repaired there — out of that card's scope.

⚠️ Duplicate search was not performed: the GitHub search/list API was returning API rate limit already exceeded for the whole window in which this was filed. Please close as a duplicate if one already exists.

Two independent narrowings in scripts/check-optional-error-sink-contract.mjs (#9754) make its population narrower than the census line advertises. Neither weakens what the gate enforces on the shapes it does see; both mean the tree contains sinks the rule would call red and never reports.

1. A sink spelled with bare Function members escapes the population entirely

isFunctionTyped() accepts a method signature or a property whose type is a
FunctionTypeNode. The type reference Function is neither, so a member spelled
error?: Function sets fn = false. In analyzeSourceFile's consider() that has two
compounding effects:

consterror=read.find((m)=>m.name==='error'&&m.fn);// ← not foundif(!error){if(pure&&read.length>0)census.noErrorMember.push(...);// ← pure is false tooreturn;}

The shape is counted in neither bucket — not the population, not noErrorMember, not
impure (that list is only appended after an error member is found). It is invisible.

Two real red sinks are hidden behind this today, both in plugin-sharing:

fileshape
packages/plugins/plugin-sharing/src/sharing-service.ts:280logger?: { info?: Function; warn?: Function; error?: Function; debug?: Function }
packages/plugins/plugin-sharing/src/share-link-service.ts:345logger?: { info?: Function; warn?: Function; error?: Function; debug?: Function }

Both declare error? beside an optionalwarn — precisely the "contract that permits
silence" #9754 forbids. Measured by rewriting the four members to real signatures and
re-running the gate (change reverted byte-identically afterwards, git hash-object
confirmed):

population declaring errorpermit silence (RED)gate exit
today372 (2 baselined)0 ✓
members given real signatures3941 ✗

The gate then names both, with its standard drop the ? from warn prescription. Note the
tightening itself costs 0 compile errorspnpm --filter @objectstack/plugin-sharing typecheck stays green.

⚠️ The repair is not mechanical, which is why this is filed rather than fixed:
SharingServiceOptions and ShareLinkServiceOptions are both publicly exported from
packages/plugins/plugin-sharing/src/index.ts, so making warn required is a breaking
change for any host passing { info, error }. That is a contract decision for the #10556
family.

2. The noErrorMember tally is systematically undercounted by the text prefilter

run() skips a file before parsing unless its raw text matches:

if(!/\berror\s*\??\s*[:(]/.test(text))continue;

That is sound for the enforced population — a sink declaring error necessarily
contains error:, error?: or error?(. It is unsound for the census line that
counts sinks declaring noerror, since such a file has no reason to contain the token
at all. A file whose only sink is a pure { info?, warn? } is skipped before the parser
sees it and is never counted.

Reproduced while landing #11068: the new packages/plugins/plugin-sharing/src/logger-shapes.ts
declares exactly one pure { info?, warn? } sink and does not move the tally. Deleting the
file leaves it at 54, unchanged.

The two declarations that file replaced were counted — but only incidentally, because
their call sites pass { error: err?.message }, i.e. a literal error: elsewhere in the
file. So membership in this tally currently depends on unrelated text.

This matters because the header argues the number is a re-measurable cost:

That number is printed as impure on every run — a narrowing whose cost is only argued
is a narrowing nobody re-measures.

The 56 / 54 figure is not that measurement; it is a lower bound of unknown slack.

Suggested direction (not a decision)

  • For (1): treat Function, (...args: any[]) => any and similar catch-all callables as
    function-typed for population purposes, or report them in a third bucket so they are
    visible rather than silently dropped. Either way the two plugin-sharing shapes above
    need a ruling first, since they go red the moment they become visible.
  • For (2): apply the prefilter only when computing the population, and parse
    unconditionally when the noErrorMember tally is wanted — or drop the tally's claim to
    be a count.

Refs

#9754 (the gate and its population rule) · #10556 (the sink ledger) · #10692 / PR #11068 (where this was measured)

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions