Found while writing the overflow-tail coverage on #16289 (not addressed there — different defect class, and widening a public signature is its own verification surface).
What
packages/drivers/driver-sql/src/sql-driver.ts. Both entry points that take a caller's object list declare the same structural parameter type:
registerObjectMetadata(objects: Array<{ name: string; fields?: Record<string, any>; tenancy?: any }>): void // :9629
async initObjects(objects: Array<{ name: string; fields?: Record<string, any>; tenancy?: any }>): Promise<void> // :9641
indexes is not in that type. It IS read out of the very same objects, one call deep:
this.managedObjectIndexes.set(tableName, (obj as any).indexes); // :9510
and managedObjectIndexes is what syncDeclaredIndexes renders every declared UNIQUE from. So the driver's whole index-sync path is driven by a key its own signature says does not exist.
The trap, and why nothing has tripped over it
TypeScript's excess-property check fires on a FRESH object literal and not on one bound to a variable first, so the same object is accepted or rejected depending only on where it is spelled:
// REJECTED by tsc — TS2353: Object literal may only specify known properties,
// and 'indexes' does not exist in type
// '{ name: string; fields?: Record<string, any> | undefined; tenancy?: any; }'
await driver.initObjects([{ ...bare, indexes: [] }]);
// ACCEPTED, and the index is synced
const withoutIndex = { ...bare, indexes: [] };
await driver.initObjects([withoutIndex]);
Measured on packages/drivers/driver-sql at dfaa8325a: pnpm --filter @objectstack/driver-sql exec tsc --noEmit reported exactly that error for the inline form, and zero for the variable form. Every existing caller in the package's own tests happens to bind a variable first, which is why the package typechecks clean today — the workaround is load-bearing and nobody wrote it down.
An author (or an AI) reading the signature concludes indexes is not accepted by initObjects and drops it. That is a silently unsynced UNIQUE constraint, decided at authoring time by a type that contradicts the runtime.
The contract text is already in the file, three lines above the signature
This exact defect was fixed once, for a different key, and the reason is recorded in the source:
tenancy is part of what this method READS — each object flows into computeAndRecordTenantField, which consumes obj.tenancy to pick the tenant column and to set or clear the sticky explicit-opt-out. It went undeclared here until #4311 (registerExternalObject and computeAndRecordTenantField both had it), so a caller spelling the key correctly was rejected by the type while the driver read it regardless.
That paragraph describes indexes today, word for word, sitting immediately above the signature that still omits it.
A sibling method on the same class already declares the key it reads — detectManagedDrift(objects?: Array<{ name: string; fields?: Record<string, any>; indexes?: any[] }>) — so the two halves of the same class disagree about the shape of the same input.
Suggested shape, not asserted
Add indexes?: any[] to both parameter types (matching detectManagedDrift's spelling) and drop the as any at :9510, or state the narrow type deliberately and make the index path read from somewhere the signature admits. Whichever way, the (obj as any) cast is the evidence that the declared type and the read disagree.
⛔ Filed unassigned, as an observation for triage. Grade deliberately not asserted: the visible symptom is a compile error on a correct call, and the worst case is a dropped UNIQUE that the author never learns was dropped.
Found while writing the overflow-tail coverage on #16289 (not addressed there — different defect class, and widening a public signature is its own verification surface).
What
packages/drivers/driver-sql/src/sql-driver.ts. Both entry points that take a caller's object list declare the same structural parameter type:indexesis not in that type. It IS read out of the very same objects, one call deep:and
managedObjectIndexesis whatsyncDeclaredIndexesrenders every declared UNIQUE from. So the driver's whole index-sync path is driven by a key its own signature says does not exist.The trap, and why nothing has tripped over it
TypeScript's excess-property check fires on a FRESH object literal and not on one bound to a variable first, so the same object is accepted or rejected depending only on where it is spelled:
Measured on
packages/drivers/driver-sqlatdfaa8325a:pnpm --filter @objectstack/driver-sql exec tsc --noEmitreported exactly that error for the inline form, and zero for the variable form. Every existing caller in the package's own tests happens to bind a variable first, which is why the package typechecks clean today — the workaround is load-bearing and nobody wrote it down.An author (or an AI) reading the signature concludes
indexesis not accepted byinitObjectsand drops it. That is a silently unsynced UNIQUE constraint, decided at authoring time by a type that contradicts the runtime.The contract text is already in the file, three lines above the signature
This exact defect was fixed once, for a different key, and the reason is recorded in the source:
That paragraph describes
indexestoday, word for word, sitting immediately above the signature that still omits it.A sibling method on the same class already declares the key it reads —
detectManagedDrift(objects?: Array<{ name: string; fields?: Record<string, any>; indexes?: any[] }>)— so the two halves of the same class disagree about the shape of the same input.Suggested shape, not asserted
Add
indexes?: any[]to both parameter types (matchingdetectManagedDrift's spelling) and drop theas anyat:9510, or state the narrow type deliberately and make the index path read from somewhere the signature admits. Whichever way, the(obj as any)cast is the evidence that the declared type and the read disagree.⛔ Filed unassigned, as an observation for triage. Grade deliberately not asserted: the visible symptom is a compile error on a correct call, and the worst case is a dropped UNIQUE that the author never learns was dropped.