Found while implementing #8140. Out of its declared scope and deliberately not fixed there —
recording it because it is larger than the population that card worked from, and it is invisible to
every grep either the card or its census used.
Measured at 1f6d04703, in packages/client/src/index.ts.
The four spellings #8140 worked from, and the one underneath them
#8140's census enumerated four spellings of return-type erasure, all of which put the any
in a Promise< … > return annotation where a grep can see it:
| spelling |
count at head |
): Promise< any > => |
32 |
): Promise< any[] > => |
5 |
): Promise< { …: any[]; … } > => |
4 |
< T = any >(…) on fixed-shape methods |
14 |
A fifth population has no return annotation at all. The method's public return type is
inferred from the type argument handed to the private unwrap helper:
list: async (filters?: { status?: string; type?: string; enabled?: boolean }) => {
// …
return this.unwrapResponse< { packages: any[]; total: number } >(res);
},
That method's published type is Promise< { packages: any[]; total: number } >, exactly as erased
as the annotated siblings — but it matches no Promise< … > grep, because the text Promise never
appears in it.
Count: 38 methods. Commands and raw output:
$ grep -cE '(unwrapResponse|_unwrap)<[^>]*\bany\b' packages/client/src/index.ts
68
68 sites total; 30 of them sit inside methods that DO carry an annotation (those are #8140's
population, now bound). The remaining 38 carry none. Reproducer for the split — it finds each
site's enclosing name: async ( declaration and asks whether the signature contains ): Promise<:
$ python3 - <<'PY'
import re
lines = open('packages/client/src/index.ts').read().split('\n')
sites = [i+1 for i,l in enumerate(lines) if re.search(r'(unwrapResponse|_unwrap)<[^>]*\bany\b', l)]
def decl_start(n):
for j in range(n-1, max(0,n-120), -1):
if re.match(r'\s*[A-Za-z_][A-Za-z0-9_]*\s*:\s*async\s*(?:<[^>]*>)?\s*\(', lines[j]): return j
return None
noann = []
for s in sites:
d = decl_start(s)
if d is None: continue
sig = '\n'.join(lines[d:s]); head = sig.split('=> {')[0] if '=> {' in sig else sig
if not re.search(r'\)\s*:\s*Promise<', head):
noann.append((s, re.match(r'\s*([A-Za-z_][A-Za-z0-9_]*)\s*:', lines[d]).group(1)))
print('unwrap-any sites total :', len(sites))
print('…with NO Promise< annotation :', len(noann))
PY
unwrap-any sites total : 68
…with NO Promise< annotation : 38
Which families, and why they were not swept into #8140
| family |
n |
notes |
meta.* history / diagnostics — getPublished, listDrafts, migrateStored, getDiagnostics, getReferences, getBookTree, getAudit, rollbackItem, diffItem |
9 |
bare unwrapResponse< any > |
packages.* — list, get, install, enable, disable, update, publish, discardDrafts, listCommits, revertCommit, rollback, export, adoptOrphans, duplicate |
14 |
mostly { package: any } / { packages: any[]; total } / { commits: any[] } |
cloud projects.* — list, create, update, activate, rotateCredential, updateHostname, updateVisibility, retryProvisioning |
8 |
{ project: any }, { project: any; database: any }, { credential: any } |
environment-scoped packages — list, install, get, enable, disable, upgrade |
6 |
{ package: any } / { packages: any[]; total } |
ScopedProjectClient.packages.get |
1 |
{ package: any } |
#8140's dispatch scoped it to the four annotated spellings, and its clause-② narrowing analysis was
answered against those. Widening mid-card would have shipped a narrowing nobody had reasoned about.
⭐ Note one of these was reachable and is now bound —
ScopedProjectClient.packages.list carried BOTH an annotation and the type argument, so it fell in
scope and became { packages: InstalledPackage[]; total: number }. Its immediate neighbour
packages.get did not, purely because it lacks the annotation. That asymmetry inside one object
literal is the clearest statement of the problem.
Why it is worth its own card
Generated by Claude Code
Found while implementing #8140. Out of its declared scope and deliberately not fixed there —
recording it because it is larger than the population that card worked from, and it is invisible to
every grep either the card or its census used.
Measured at
1f6d04703, inpackages/client/src/index.ts.The four spellings #8140 worked from, and the one underneath them
#8140's census enumerated four spellings of return-type erasure, all of which put the
anyin a
Promise< … >return annotation where a grep can see it:): Promise< any > =>): Promise< any[] > =>): Promise< { …: any[]; … } > =>< T = any >(…)on fixed-shape methodsA fifth population has no return annotation at all. The method's public return type is
inferred from the type argument handed to the private unwrap helper:
That method's published type is
Promise< { packages: any[]; total: number } >, exactly as erasedas the annotated siblings — but it matches no
Promise< … >grep, because the textPromiseneverappears in it.
Count: 38 methods. Commands and raw output:
68 sites total; 30 of them sit inside methods that DO carry an annotation (those are #8140's
population, now bound). The remaining 38 carry none. Reproducer for the split — it finds each
site's enclosing
name: async (declaration and asks whether the signature contains): Promise<:Which families, and why they were not swept into #8140
meta.*history / diagnostics —getPublished,listDrafts,migrateStored,getDiagnostics,getReferences,getBookTree,getAudit,rollbackItem,diffItemunwrapResponse< any >packages.*—list,get,install,enable,disable,update,publish,discardDrafts,listCommits,revertCommit,rollback,export,adoptOrphans,duplicate{ package: any }/{ packages: any[]; total }/{ commits: any[] }projects.*—list,create,update,activate,rotateCredential,updateHostname,updateVisibility,retryProvisioning{ project: any },{ project: any; database: any },{ credential: any }list,install,get,enable,disable,upgrade{ package: any }/{ packages: any[]; total }ScopedProjectClient.packages.get{ package: any }#8140's dispatch scoped it to the four annotated spellings, and its clause-② narrowing analysis was
answered against those. Widening mid-card would have shipped a narrowing nobody had reasoned about.
⭐ Note one of these was reachable and is now bound —
ScopedProjectClient.packages.listcarried BOTH an annotation and the type argument, so it fell inscope and became
{ packages: InstalledPackage[]; total: number }. Its immediate neighbourpackages.getdid not, purely because it lacks the annotation. That asymmetry inside one objectliteral is the clearest statement of the problem.
Why it is worth its own card
InstalledPackage(@objectstack/spec/kernel) already types thepackages.*rows — client SDK drops the precise spec types at its boundary: 32 methods returnPromise< any >on a package that already depends on@objectstack/spec#8140 bound it for the one method in scope, so 20 of the 38 plausibly land ona type that exists today. The
projects.*cloud family is the one that likely needs contractsauthored first (same class as Four client SDK routes answer a shape no published contract declares —
automation.create/automation.update/search/data.clone#11924).Promise< any >on a package that already depends on@objectstack/spec#8140: published return types get narrower, no runtime change,and the risk sits with external SDK consumers.
Promise< any >on a package that already depends on@objectstack/spec#8140's census closed with "whoever scopesthe implementation half must work from 49, not 32, or they will 'finish' the card with 17 sites
still erased." The same sentence applies one level out: an audit anchored on the word
Promisecannot see a method that never writes it.
Generated by Claude Code