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:
| file | shape |
|---|
packages/plugins/plugin-sharing/src/sharing-service.ts:280 | logger?: { info?: Function; warn?: Function; error?: Function; debug?: Function } |
packages/plugins/plugin-sharing/src/share-link-service.ts:345 | logger?: { 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 error | permit silence (RED) | gate exit |
|---|
| today | 37 | 2 (2 baselined) | 0 ✓ |
| members given real signatures | 39 | 4 | 1 ✗ |
The gate then names both, with its standard drop the ? from warn prescription. Note the
tightening itself costs 0 compile errors — pnpm --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)
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.
API rate limit already exceededfor 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
Functionmembers escapes the population entirelyisFunctionTyped()accepts a method signature or a property whose type is aFunctionTypeNode. The type referenceFunctionis neither, so a member spellederror?: Functionsetsfn = false. InanalyzeSourceFile'sconsider()that has twocompounding effects:
The shape is counted in neither bucket — not the population, not
noErrorMember, notimpure(that list is only appended after anerrormember is found). It is invisible.Two real red sinks are hidden behind this today, both in
plugin-sharing: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 permitssilence" #9754 forbids. Measured by rewriting the four members to real signatures and
re-running the gate (change reverted byte-identically afterwards,
git hash-objectconfirmed):
errorThe gate then names both, with its standard
drop the ? from warnprescription. Note thetightening itself costs 0 compile errors —
pnpm --filter @objectstack/plugin-sharing typecheckstays green.SharingServiceOptionsandShareLinkServiceOptionsare both publicly exported frompackages/plugins/plugin-sharing/src/index.ts, so makingwarnrequired is a breakingchange for any host passing
{ info, error }. That is a contract decision for the #10556family.
2. The
noErrorMembertally is systematically undercounted by the text prefilterrun()skips a file before parsing unless its raw text matches:That is sound for the enforced population — a sink declaring
errornecessarilycontains
error:,error?:orerror?(. It is unsound for the census line thatcounts sinks declaring no
error, since such a file has no reason to contain the tokenat all. A file whose only sink is a pure
{ info?, warn? }is skipped before the parsersees it and is never counted.
Reproduced while landing #11068: the new
packages/plugins/plugin-sharing/src/logger-shapes.tsdeclares exactly one pure
{ info?, warn? }sink and does not move the tally. Deleting thefile 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 literalerror:elsewhere in thefile. So membership in this tally currently depends on unrelated text.
This matters because the header argues the number is a re-measurable cost:
The
56/54figure is not that measurement; it is a lower bound of unknown slack.Suggested direction (not a decision)
Function,(...args: any[]) => anyand similar catch-all callables asfunction-typed for population purposes, or report them in a third bucket so they are
visible rather than silently dropped. Either way the two
plugin-sharingshapes aboveneed a ruling first, since they go red the moment they become visible.
unconditionally when the
noErrorMembertally is wanted — or drop the tally's claim tobe a count.
Refs
#9754 (the gate and its population rule) · #10556 (the sink ledger) · #10692 / PR #11068 (where this was measured)