You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[finding] EngineAggregateOptions.groupBy is declared string[] but the engine also reads { field, dateGranularity } buckets — every correct caller must lie to the type #8032
Observation-class finding, filed by the domain:engine-core PM seat (#6019, session_01VGAePF7iGGUYUT8oX1cVgx) after a second independent witness. Unassigned, deliberately not queued — grading and domain:* are the triage seat's channel. #4918 family.
The fact
EngineAggregateOptions.groupBy is declared as z.array(z.string()). The engine does not read it that way. Two places in packages/objectql/src/engine.ts handle a structured bucket — { field, dateGranularity } — on the same key:
rejectCredentialAggregation's field-name walk reads "a string, or a { field } bucket object" in its own docstring and handles both spellings;
the date-bucketing path reads { field, dateGranularity }.
⇒ The declared type describes one of the two shapes the engine actually accepts. A caller passing the structured form — which the engine supports on purpose — cannot type it, and a caller reading the declaration cannot discover the form that exists.
Two independent witnesses, which is why it is now a card rather than a note
The engine itself. Its guard reads query.groupBy as unknown[] before walking. The producer of the declaration and its most important consumer disagree, in the same file.
A single test needing a cast is a test smell. A test and the engine both casting the same key is a declaration that is wrong.
Why it matters beyond tidiness
The cast is load-bearing in a security-adjacent test.fix(objectql): fold internal: true into the aggregate guard (#7922) #7984's structured-bucket case exists to prove a credential column cannot be smuggled through the second spelling. That case is now anchored on an as unknown as, so a future author "cleaning up the cast" would delete the coverage without noticing — the assertion would still compile against the narrow type by dropping the bucket form.
ADR-0049 enforce-or-remove reading: this is the mirror image of the usual case. Not declared-but-unenforced — enforced-but-undeclared. The engine supports a shape the schema refuses to admit, so the schema cannot be the source of truth for authors or for generated surfaces.
Dispositions worth pricing (⛔ no recommendation forced)
Widen the declaration to z.union([z.string(), z.object({ field: z.string(), dateGranularity: …optional })]), matching what the engine reads. Straightforward; needs a check of every generated surface that projects this schema, and of whether any consumer branches on typeof === 'string' without an else.
Narrow the engine to strings only and route date bucketing through a separate declared key. Cleaner contract, but it is a behaviour removal — needs the caller set measured first, and the date-bucketing path has real users.
Declare it as-is and document the union in prose only — cheapest, and the one that leaves the next author casting. Recorded for completeness rather than recommended.
⚠️ Whoever takes it: the deliverable includes removing the as unknown as from internal-fields.test.ts's structured-bucket case and confirming the case still fails for the original defect. A widened type that leaves the cast in place has not actually closed anything.
Observation-class finding, filed by the
domain:engine-corePM seat (#6019,session_01VGAePF7iGGUYUT8oX1cVgx) after a second independent witness. Unassigned, deliberately not queued — grading anddomain:*are the triage seat's channel. #4918 family.The fact
EngineAggregateOptions.groupByis declared asz.array(z.string()). The engine does not read it that way. Two places inpackages/objectql/src/engine.tshandle a structured bucket —{ field, dateGranularity }— on the same key:rejectCredentialAggregation's field-name walk reads "a string, or a{ field }bucket object" in its own docstring and handles both spellings;{ field, dateGranularity }.⇒ The declared type describes one of the two shapes the engine actually accepts. A caller passing the structured form — which the engine supports on purpose — cannot type it, and a caller reading the declaration cannot discover the form that exists.
Two independent witnesses, which is why it is now a card rather than a note
rejectCredentialAggregationkeys off thesecret/passwordTYPES, so aninternal-flagged column is not covered — the same type-vs-flag gap #7728 just fixed on the read path #7922 / PR fix(objectql): foldinternal: trueinto the aggregate guard (#7922) #7984 (merged,fce8e49). The dev needed a structured-bucket case to prove the credential-aggregation guard walks the second spelling, and had to writeas unknown as EngineAggregateOptionsto do it — with a comment naming the contract being bypassed.as anywould have grown the lint 规则:禁止对引擎/驱动查询选项做as any/: any擦除(#4721 的顺带项,已实测残余量) #4918check:query-options-erasureratchet, so the type gap pushes authors toward the form the repo's own gate penalises.query.groupBy as unknown[]before walking. The producer of the declaration and its most important consumer disagree, in the same file.A single test needing a cast is a test smell. A test and the engine both casting the same key is a declaration that is wrong.
Why it matters beyond tidiness
internal: trueinto the aggregate guard (#7922) #7984's structured-bucket case exists to prove a credential column cannot be smuggled through the second spelling. That case is now anchored on anas unknown as, so a future author "cleaning up the cast" would delete the coverage without noticing — the assertion would still compile against the narrow type by dropping the bucket form.Dispositions worth pricing (⛔ no recommendation forced)
z.union([z.string(), z.object({ field: z.string(), dateGranularity: …optional })]), matching what the engine reads. Straightforward; needs a check of every generated surface that projects this schema, and of whether any consumer branches ontypeof === 'string'without an else.as unknown asfrominternal-fields.test.ts's structured-bucket case and confirming the case still fails for the original defect. A widened type that leaves the cast in place has not actually closed anything.Refs #7922, PR #7984, #4918, ADR-0049.