Skip to content

[finding] Ruling A on #16231 narrows IScopedObjectRepository, but ctx.api.object(name) resolves through the CLASS ObjectRepository — the hook-facing door keeps Promise<any>, and updateById keeps it too #16786

Description

@os-musk

Filed by the domain:engine execution PM seat, from the #16231 round (PR #16783). The implementer reported it and ⛔ deliberately did not fold it in; the measurements below are the seat's own, re-taken on origin/main.

⛔ Filed unassigned and unlabelled, as an observation for triage. Grade deliberately not asserted.

Why this is not just "one more any"

Maintainer ruling A on #16231 (5572153316, 「同意」) extends #15823's find() rule to findOne / update / delete, naming "packages/spec/src/contracts/data-engine.ts:276-279 and the scoped-context mirrors at scoped-context.ts:148/164". PR #16783 narrows exactly those.

⚠️ The door a hook author actually touches is not among them. ScopedContext is HookContext.api, so the hook-facing call is ctx.api.object(name).findOne(…) — and that call does not resolve through the narrowed interface.

Measured on origin/main ed7243d52b

where reading
packages/spec/src/contracts/scoped-context.ts:181 object(name: string): IScopedObjectRepository; — the interface does return the interface
packages/objectql/src/engine.ts:14662 object(name: string): ObjectRepository { — the implementation returns the CLASS
packages/objectql/src/engine.ts:14479-14480 the file's own docblock confirms this is "what ScopedContext.object(name) returns, and what a hook reaches as ctx.api.object(name)"

⇒ TypeScript's implements accepts a wider declared return, so ObjectRepository's own Promise<any> members satisfy the narrowed interface while the call site keeps seeing any. Narrowing the interface therefore does not reach ctx.api.object(name).findOne(…).

⚠️ How I nearly filed this wrong, recorded because it is the more useful half: my first measurement grepped packages/spec/src/contracts/scoped-context.ts, found object(name): IScopedObjectRepository, and looked like a falsification of the report. It was the wrong file — the claim is about the implementation class, not the spec interface. ⇒ ⛔ a reading that refutes a claim about implementation by measuring the declaration has measured a different thing.

Second, smaller gap in the same family: IScopedObjectRepository.updateById (scoped-context.ts:167) stays updateById(id: string | number, data: any): Promise<any> while its update sibling narrows under ruling A. A by-id update has the same record-or-null answer as the update by-id exit. The ruling names :148 / :164 and not :167.

What this costs, in the ruling's own terms

Ruling A exists because "every consumer that has to decide whether a normalizer limb over one of these is dead pays a reachability argument instead of reading a type" — the #15094 census counted 104 array-or-envelope normalizer blocks. ⇒ for hook authors, who reach these verbs through ctx.api, that cost is not paid down by PR #16783: the type they read is still any.

⭐ And the class is live, not theoretical. The same round measured seven test doubles answering shapes their own IDataDriver contract does not have{ deleted: n }, an array from a Promise<number> door, undefined from a bare vi.fn() — every one invisible while the door said any. That is the drift a declaration stops, and the hook-facing door is still undeclared.

Suggested shape, not asserted

The implementer's own reading: "a one-line annotation change plus its own consumer census." ⇒ annotate ScopedContext.object with IScopedObjectRepository, and narrow updateById alongside update.

⚠️ Two things a taker must not skip:

  1. The census is the cost, not the annotation. Ruling A's precondition was a measured consumer census posted before any declaration moved, and it found 18 files / 92 compile errors for the three verbs. This annotation newly type-checks every ctx.api.object(…) call in the repo against the narrowed members; that number is unmeasured and could be larger.
  2. Do not assume this rides on [finding] ObjectQL.findOne / update / delete return hookContext.result under a Promise[any] declaration — nothing to guard, because nothing is declared #16231's ruling. The maintainer named three anchors and these are not among them — whether the same answer extends here is a question to put, ⛔ not an inference to make. This seat learned that the expensive way tonight on [finding] The InMemory driver silently ignores the engine's tenant scope for objects that OMIT a tenancy block — its guard only refuses an explicit enabled: true, so memory-driver runs show cross-organization rows a SQL driver refuses #16589, where a direction settled by a standing criterion turned out to have been ruled the other way a month earlier.

Boundaries

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions