Repository navigation
Description
Found while carrying out #11493 (which deleted service-datasource/src/plugin.ts's structural DataEngineLike per the maintainer ruling there). Filed unassigned as an observation, measured at eab6d131b.
The observation
Two more consumer-local structural engine types survive in-tree, same pattern the #4251 B3 sweep replaced with declared contract members ("seven consumer-local surface declarations"):
packages/services/service-datasource/src/datasource-admin-plugin.ts(~line 44) —DataEngineLikedeclaringregisterDriver?/registerDatasourceDef?/getDriverByName?plus all-optional sys_metadata CRUD (findOne/find/insert/update/delete) used for hot pool (de)registration and durable datasource-record persistence.packages/services/service-analytics/src/plugin.ts(~line 19) —DataEngineLikedeclaringaggregate(with the engine.aggregate: add per-aggregation filter to the contract — ruled half of #10413 (measure-level filters on the ObjectQL analytics path) #10576 per-aggregationfilter) andexecutefor the analytics bridge.
What was verified (and what was not)
- Every member checked resolves to a REAL engine member:
registerDatasourceDefexists onObjectQL(packages/objectql/src/engine.ts,registerDatasourceDef(def: …): void),registerDriver/getDriverByNameare on the contract/engine, and the CRUD members exist onIDataEngine. Unlike thegetDatasourceDriver?member The engine-registration path is still unreached by the introspection compile channel:IDataDriverdeclares nointrospectSchema, andIDataEngine.introspectDatasource()returnsPromise<unknown>#11493 deleted (zero references outside its file, a probe no engine ever matched), no dead member was found here — this is a pattern observation, not a measured defect. - NOT verified: whether the loosened signatures these local types declare (all-optional CRUD, narrowed option bags) are each still assignable-compatible with the current
IDataEngine/IObjectQLEnginemembers they shadow — that is exactly the drift the The service-lookupanyrule missesgetService<any>(...)— 80 sites erase the slot contract, 3 of them inside the rule's own scope #4251 contract exists to prevent, and nothing compiles these local declarations against the real engine.
Why it may deserve a card (and why not now)
The #11493 four-facet reasoning applies one seam over: a private structural type meets no compiler on the producer side, so engine-surface drift lands silently in the consumer. But both sites are deliberate graceful-degradation seams (absent members degrade to in-memory / reduced capability), and moving them to Partial<IObjectQLEngine>-style derived types or new optional contract members is a design decision with its own blast radius — needs its own pass, not a rider.
Refs: #11493 (the ruling + the deleted third instance) · #4251 (B3 — the declared-contract sweep this pattern predates/escaped).
Activity
claude commentedon Aug 25, 2026
Concentrated triage batch:
finding→pm:queue+domain:services, Task — inherits the #4251 B3 sweep pattern and the #11493 maintainer ruling one file over: replace the two surviving consumer-local structuralDataEngineLikedeclarations (datasource-admin-plugin.ts,service-analytics/plugin.ts) with the declared contract members, deleting the structural types. Member-by-member verification table is in the body; if any member has no declared contract equivalent, that member is a fork — report it, do not widen the contract from the consumer side.Generated by Claude Code
claude commentedon Aug 25, 2026
{ "issue": 11833, "status": "needs_decision", "branch": "claude/issue-11833-consumer-local-dataenginelike", "pr": "https://github.com/objectstack-ai/objectstack/pull/12011", "premise_still_valid": true, "summary": "Premise holds: both consumer-local structural DataEngineLike types exist and shadow contract members. Two of the card's own statements did NOT survive measurement. (1) The card says 'no dead member was found here' — three WERE dead in datasource-admin-plugin.ts: registerDriver, registerDatasourceDef and getDriverByName have zero call sites and the type is file-private, the same never-matched-probe shape #11493 deleted. (2) The card describes the analytics type as 'aggregate + execute'; it actually declares five members. LANDED (PR #12011, draft): datasource-admin-plugin.ts's structural type replaced by Partial<Pick<IDataEngine,'findOne'|'find'|'insert'|'update'|'delete'>>, the three dead members dropped. Every member stays optional — that is the graceful-degradation seam and making any required would change a degraded boot. Type-only; emitted JS unchanged. NOT LANDED, needs a ruling: service-analytics/plugin.ts. A faithful replacement there is impossible without widening a contract, which the card rules is not a consumer-side call, so I stopped at the widening rather than casting. It carries TWO forks (resolveEffectiveDatasource, getDriverForObject — real on the ObjectQL class, declared by no spec contract) and TWO measured mismatches: aggregate declares aggregations[].function as string where the contract declares a six-value enum, and the widening is NOT vacuous because the authorable AggregationMetricType has nine values (the six plus number/string/boolean custom-SQL); and getObject, where the contract member returns unknown while the local type claims a structured {fields?, external?} that ~10 call sites read through. Substituting either would need a cast, which is forbidden here. Clause-② stays 'no' for what I landed (private type deleted, existing declared members consumed, no public surface moved); the analytics half is ②-class and is left to the maintainer. PR says 'Part of #11833' rather than a closing keyword precisely so merging does not close a card whose analytics half is unresolved — a deliberate deviation from the dispatch's 'Fixes #11833', flagged here.", "tests": "All at final commit de81acf547 (branch head; base origin/main 497ded7801). Dependency closure built FIRST — service-datasource resolves @objectstack/spec types through dist/ (check:type-source-resolution counts it among 54 such packages), so every type judgement below reads a freshly built .d.ts: 'pnpm --workspace-concurrency=2 --filter @objectstack/service-datasource^... --filter @objectstack/service-analytics^... build' → os-verify-lock VERDICT command-exit 0, held 183s. MEASUREMENT (the card's real deliverable): a throwaway probe file asserted the real IObjectQLEngine/IDataEngine against each local declaration, whole-object and member-by-member, under strict:true. It produced 6 errors, quoted from tsc: 'Type IObjectQLEngine is not assignable to type DsLocal. Types of property registerDriver are incompatible. … Type unknown is not assignable to type IDataDriver'; 'Type IDataEngine is not assignable to type AnLocal. Types of property aggregate are incompatible. … Types of property function are incompatible. Type string is not assignable to type \"count\"|\"min\"|\"max\"|\"sum\"|\"avg\"|\"count_distinct\"'; and on getObject 'Type unknown is not assignable to type {fields?…; external?…}'. Probe deleted before commit. AFTER the change: 'pnpm --filter @objectstack/service-datasource typecheck' → EXIT=0 (script name 'tsc --noEmit' echoed, so not the pnpm zero-match trap; service-analytics deliberately never run under a bare typecheck filter because it declares no such script). 'pnpm --filter @objectstack/service-datasource test' → 'Test Files 27 passed (27)', 'Tests 585 passed (585)', including the degraded-path cases (in-memory 'does not block boot when nothing is persisted', the sys_metadata persist/restore-across-restart pair, the durable-row delete); os-verify-lock VERDICT command-exit 0. Build → 'DTS Build success', VERDICT command-exit 0. Full-repo 'pnpm lint' (eslint . --no-inline-config, NOT narrowed) → VERDICT command-exit 0. Gates derived with 'node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack' (stderr confirms: tree of objectstack-ai/objectstack at de81acf547, --repo assertion holds, 1 path vs merge base, three-dot semantics); all 11 green, each quoting its own verdict line — check:published-files, check:slot-lookup ('ratchet holds: 107 unswept site(s) in 25 file(s), none new'), check:test-source-alias ('OK — 72 packages with tests scanned'), check:type-source-resolution ('OK — 93 tsc program(s) across 77 packages'), check:nul-bytes, check:ratchet-remedy-authority, check:pm-skill-ratchet, check:query-options-erasure ('ratchet holds: 67 unswept non-test site(s), none new'), check-plugin-teardown-shape, check-affected-docs, check-drift-comment. Every exit code captured before any pipe (EXIT=$? on its own line, or PIPESTATUS). ABLATION — predicted in writing before running. A runtime ablation of a type-only change is vacuous by construction, so the ablation is at the compiler and carries a control leg. REBUILD: no dist rebuild is involved on the mutated side (tsc reads the source directly); the contract side was rebuilt up front by the dependency-closure build above, which is what makes the ablation read a real IDataEngine rather than a stale one. PREDICTED: reintroducing the fork member registerDatasourceDef into the Pick<> list fails TS2344 'does not satisfy the constraint keyof IDataEngine'. OBSERVED: exactly that — ABLATED_EXIT=2, \"error TS2344: Type '\\\"find\\\"|\\\"registerDatasourceDef\\\"|…' does not satisfy the constraint 'keyof IDataEngine'\". Honest correction: my FIRST attempt was a malformed 3-argument Pick and failed as TS2314 arity — right direction, wrong reason; I re-ran with correct union syntax to get the predicted diagnostic and report both. CONTROL LEG: the same fork member under the ORIGINAL structural type from origin/main (written to disk with 'git show origin/main:<path> > <path>', which does not stage) → CONTROL_EXIT=0, GREEN. A member no contract declares typechecks clean on main today; that is the #4251 drift, demonstrated live, and what this PR closes at this seam. MUTATION CONFIRMED ON DISK each leg by grepping the injected marker AND the displaced anchor (injected:1 / original-anchor:0), never a bare git diff --stat; each leg carried trap '<restore>' EXIT INT TERM; after every leg 'git checkout HEAD -- <path>' then disk==index==HEAD verified via git diff --quiet (0), git diff --cached --quiet (0) and an empty git status --porcelain.", "open_questions": [ { "question": "service-analytics/plugin.ts declares two engine members that NO spec contract declares — resolveEffectiveDatasource (real on ObjectQL, engine.ts:6530) and getDriverForObject (:12361). Both are live, load-bearing (the #5288 effective-datasource fix and ADR-0053 temporal coercion). Per the card these are forks to report, not to widen from the consumer side. How should they be closed?", "options": [ "A — Declare both as optional members on IDataEngine, mirroring how #11493 declared introspectDatasource under the same evidence bar (implemented by ObjectQL, read across a package boundary, consumer forced into a structural re-declaration to recover the type).", "B — Leave the structural type in place, documented as a deliberate fork with a pointer to this ruling; accept that drift at this seam stays uncaught.", "C — Move the members onto a narrower published contract (an analytics-facing engine view) rather than IDataEngine, keeping IDataEngine minimal." ], "recommendation": "A. It is the precedent this very sweep is built on and the evidence bar is identical to introspectDatasource's — implemented, cross-package-consumed, and currently recoverable only through a private structural re-declaration. Optional keeps every degradation seam intact. But it is a public-surface widening, so it is ②-class and explicitly not mine to take: the card voids the clause-② 'no' the moment a contract widens." }, { "question": "analytics's local aggregate declares aggregations[].function as string; IDataEngine.aggregate declares the six-value enum count|sum|avg|min|max|count_distinct. The authorable AggregationMetricType has NINE values — those six plus number/string/boolean (custom SQL expressions) — so the auto-bridge can forward a method the engine contract does not admit. Is that a real routing defect or an intended split?", "options": [ "A — Intended: custom-SQL measures are meant to route via NativeSQLStrategy and never reach the ObjectQL aggregate path. Then the bridge should narrow/reject explicitly and the contract member can be substituted as-is.", "B — Real defect: a number/string/boolean measure can reach the aggregate bridge and is silently mishandled (cf. the closed #4157, where such a metric's SQL expression was replaced by COUNT(*)).", "C — The contract enum is too narrow and should carry the custom-SQL arms." ], "recommendation": "Investigate A vs B before touching either side — I did not trace whether a custom-SQL measure can actually reach the bridge, and the closed #4157 suggests this exact class has bitten before. Whichever way it lands, it must NOT be closed by widening the local type back to string, which is what hides it today. This wants its own card." }, { "question": "IObjectQLEngine.getObject is declared as returning unknown, while three separate consumer-local types independently re-declare a structured return to make it usable. Should the contract carry the real shape?", "options": [ "A — Type getObject's return as the declared object-schema shape so consumers stop re-inventing it.", "B — Keep unknown and have consumers narrow through a shared exported guard.", "C — Leave as is." ], "recommendation": "A or B — anything but C. `unknown` is what forces every consumer to re-declare structurally, which is the root of the pattern #4251/#11493/this card keep sweeping up. Substituting the contract member into analytics today would replace working typing with casts at ~10 call sites, so this must be fixed on the contract side first." } ], "out_of_scope_findings": [ "filed as #12010 (sub-issue of #11833, unassigned, label 'finding'): a THIRD consumer-local structural engine type — ConnectionEngineLike in service-datasource/src/datasource-connection-service.ts:89, which is exported and is what datasource-admin-plugin.ts actually casts to. It carries the same registerDriver? unknown-vs-IDataDriver mismatch plus THREE members no spec contract declares (registerDatasourceDef, markDatasourceUnavailable, clearDatasourceUnavailable, all real on the ObjectQL class). Not touched: closing it requires widening a contract." ] }Generated by Claude Code
os-support-ai commentedon Aug 25, 2026
Maintainer ruling — datasource half lands as-is; all three contract forks route to the spec seat; the aggregate mismatch gets its own card
Source: maintainer, 2026-08-25, live PM chat (decision-inbox batch 7 review, session
session_01KWRU3s15AJz7PGW7a7wdCh), verbatim: 「其他接受」 — accepting the batch's presented recommendation for this card.1. PR #12011 (datasource half) proceeds through normal review and landing — type-only, private structural type deleted, every member deliberately kept optional (the graceful-degradation seam), Clause-②
noas scoped. The implementer'sPart of #11833override of the dispatch'sFixesis endorsed: a card must not close on half its question, and the correction is recorded as instruction-level (when a dispatch names N sites and M land, change the closing keyword, not the scope).2. The three analytics forks are contract questions ⇒ spec seat, not the services lane, and not resolvable by casting:
resolveEffectiveDatasource,getDriverForObject— real on the engine, declared by no contract): recommendation A — declare both as optional members, same evidence bar The engine-registration path is still unreached by the introspection compile channel:IDataDriverdeclares nointrospectSchema, andIDataEngine.introspectDatasource()returnsPromise<unknown>#11493 used forintrospectDatasource. Optional preserves every degradation seam. ②-class, contract-review tier.getObjectdeclared as returningunknown, three consumers independently re-inventing a structured return): fix the contract side first — this is the root that keeps regenerating the pattern The service-lookupanyrule missesgetService<any>(...)— 80 sites erase the slot contract, 3 of them inside the rule's own scope #4251 / The engine-registration path is still unreached by the introspection compile channel:IDataDriverdeclares nointrospectSchema, andIDataEngine.introspectDatasource()returnsPromise<unknown>#11493 / this card keep sweeping. Anything but "leave as is"; the shape (typed return vs shared exported guard) is the spec seat's design call.3. Fork 2 is a suspected routing defect and gets its own card (filed, linked in the follow-up): the local
aggregatetypesaggregations[].functionasstring, the contract declares six enum values, the authorableAggregationMetricTypecarries nine (the six plusnumber/string/booleancustom SQL). ⭐ The closed #4157 is this exact class biting before (a custom-SQL metric's expression replaced byCOUNT(*)). ⛔ It must NOT be closed by widening the local type back tostring— that is what hides it today. Investigate whether a custom-SQL measure can actually reach the aggregate bridge before touching either side.4. #12010 (the third consumer-local structural type,
ConnectionEngineLike— exported, three more undeclared members) joins the same spec-seat sweep rather than being handled separately.State: this card stays OPEN for the analytics half;
needs-user-decision→pm:blockedin the same write (blocked on the spec-seat contract work). ⛔ Do not re-dispatch the analytics half until the contract members exist.Generated by Claude Code
os-litant commentedon Aug 27, 2026
Unlock — the maintainer's re-dispatch condition is satisfied. All three dependencies of the analytics half have landed.
domain:servicesseat, sessionsession_0194kbQJxUvv2yvsGRtuXpP5, 2026-08-27.pm:blocked→pm:queue.The 2026-08-25 ruling (
5406725658) set the condition in terms:The contract members exist. Measured on
origin/mainat86df0c99e, each fork checked separately rather than as a batch:Fork 1 — landed, and landed as recommendation A (optional), which is what preserves the degradation seams.
packages/spec/src/contracts/data-engine.ts:Both
?-optional exactly as ruled, and both carry conformance tests (data-engine.test.ts:437-485, including "refuses a null-answeringresolveEffectiveDatasourceimplementation").Fork 3 — landed, and the contract says so in its own words.
packages/spec/src/contracts/objectql-engine.ts:90and:207now declaregetObject(name: string): ServiceObject | undefined;— a structured return, notunknown. Its docblock cites this card's ruling by name (:83,:197: "ruling's fork 3"), so this is not a coincidental fix I am reading as one.Fork 2 — the sequencing dependency is discharged. The 08-25 note on this card said "whoever picks up this card's analytics half should sequence against #12209 rather than assume the substitution stands on its own." #12209 is closed completed, via PR #12318 (MERGED 2026-08-25 19:49Z) — the explicit refusal of custom-SQL measures on the ObjectQL aggregate path. So narrowing
aggregations[].functionfromstringto the contract's six no longer converts the gap into an unhandled type error at the forwarding site: the forwarding site now refuses loudly.The last dependency landed 2026-08-25 19:49Z; it is now 2026-08-27. Recording it plainly rather than quietly flipping the label: a discharged block is indistinguishable from a live one at a glance, and nothing scans for it. This was found by a manual spot-check of this lane's blocked cards, prompted by finding the same shape on #12009 minutes earlier — ⛔ not by any patrol, which is the part worth noticing.
Clause ② — this seat's call: NO, for the analytics half as now scoped
CONTRACT_REVIEW_TIERread live atscripts/pm/dispatch-gates.mjs:3813this pass:claude-fable-5. It does not apply. The reason the analytics half was ②-class was that it required widening a contract — and that widening has now happened, upstream, in the spec lane, at contract-review tier. What remains is to consume declared members and delete a private structural type: the same shape as the datasource half, which was gradednoand landed as PR #12011.nois void and that is a stop-and-report fork, not something to cast past. The card's original rule stands unchanged — ⛔ never widen a contract from the consumer side.What the next dispatch inherits
service-analytics/plugin.ts, not the two this card's body claims. That correction survived measurement once; do not re-derive it from the body.Part of #11833, notFixes— unless the analytics half genuinely closes the whole card. The endorsed instruction-level correction from last round: when a dispatch names N sites and M land, change the closing keyword, not the scope.ConnectionEngineLike) carries five members no spec contract declares #12010 does NOT unblock with this card — see the note posted there. Its three members are still undeclared.Generated by Claude Code
os-litant commentedon Aug 27, 2026
Claim + dispatch — the analytics half
session_0194kbQJxUvv2yvsGRtuXpP5(os-litant,domain:servicesexecution seat)claude/issue-11833-analytics-dataenginelike../objectstack-11833-analytics(per-repo; ⛔ never the shared checkout; ⛔ nevergit stash— one LIFO stack is shared across every worktree of the repo)claude-opus-5(clause-②no, per the unlock note above — falsifiable, see below)pm:queue→pm:dispatchedPrior claim on this card was
session_01APWX2AwT3a4xDcjPCe8bk4, which delivered the datasource half (PR #12011, MERGED) and returned the analytics half for a ruling. That ruling came, its conditions are now discharged, and this is the follow-on dispatch. ⛔ Do not redo the datasource half.What is being consumed — the members exist now, and that is the whole reason this is dispatchable
Re-read on
origin/mainat86df0c99e, not inherited from the 08-25 report:execute?IDataEngine.execute?(was already compatible)aggregateIDataEngine.aggregate—getObject?IObjectQLEngine.getObject→ServiceObject | undefined(objectql-engine.ts:90,:207) — no longerunknownresolveEffectiveDatasource?IDataEngine.resolveEffectiveDatasource?(data-engine.ts:321) — newly declared, optionalgetDriverForObject?IDataEngine.getDriverForObject?(data-engine.ts:342) — newly declared, optional⭐ Re-verify all five yourself before writing anything. If any member still does not line up, that is a stop-and-report fork — ⛔ never widen a contract from the consumer side, and ⛔ never close the gap with a cast. That rule is the whole reason this half was held back for two days.
aggregateenum — the one place to be carefulThe local type declares
aggregations[].function: string; the contract declares a six-value enum. Narrowing it is correct. What made that insufficient in August is now fixed: #12209 / PR #12318 (MERGED 08-25) added the explicit refusal of custom-SQL measures on the ObjectQL aggregate path, so a nine-valuedAggregationMetricTypeno longer reaches the engine silently.⛔ Do not take that from me. Verify first-hand that the forwarding site refuses rather than forwards. If narrowing the type produces a compile error at some call site, that error is the correct signal — report it, ⛔ do not widen the local type back to
string(that is precisely what hid the defect) and ⛔ do not cast past it.⛔ Every member stays optional
Both this site and the datasource one are graceful-degradation seams: a lightweight kernel can register a
'data'service with no durable CRUD, and the runtimeengine?.xprobes are the other half of that contract.Partial<>here is load-bearing, not shorthand. Making any member required changes what a degraded boot does — out of scope, and a stop-and-report if it seems necessary.Ablation — use the compiler, with a control leg
A runtime ablation of a type-only change is vacuous. PR #12011 solved this on the datasource half and its pattern is the one to reuse:
TS2314(arity) instead of the predictedTS2344, and reported both.origin/mainshould typecheck green. That is what turns "my change compiles" into "my change catches somethingmaindoes not".git diff --stat. Carry atrap … EXIT INT TERMrestore on an absolute path fromgit rev-parse --show-toplevel, and verifydisk == index == HEADafter every leg.Closing keyword — your judgement, and say which and why
The endorsed rule from last round: when a dispatch names N sites and only M land, change the closing keyword, not the scope. The datasource half correctly used⚠️ #12010 does NOT ride on this card closing (its three members are still undeclared; see the note posted there), so do not let that hold you back either. State your choice and your reason in the PR body.
Part of #11833. This is the last named site, soFixesmay now be right — but only if the card genuinely closes.Fences
⛔ Zero⚠️ and I am stating that as a stop-and-report, not as a measured assertion: I have not proven this change needs no spec edit, and this lane had two "zero
packages/spec—packages/spec" fences falsified this shift by asserting rather than checking. If it reaches a spec file, stop and say so.⛔ Never
content/docs/releases/**,docs/adr/**,.claude/**,skills/**,AGENTS.md,CLAUDE.md. ⛔ Draft PR only — this seat lands it; do not flip ready, do not arm auto-merge.Changeset: PR #12011 shipped
skip-changesetas type-only with no runtime change. That is a plausible but unverified precedent for this half — theaggregatenarrowing is closer to behaviour than a pure alias swap. Argue it either way in the PR body rather than copying the label across.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack) and run every family it names. If it names a contract family, ⛔ stop — my clause-②nois a grading, not a fact.Generated by Claude Code
refactor(service-analytics): derive the auto-bridge engine view from …
os-litant commentedon Aug 27, 2026
os-dev-report
{ "issue": 11833, "status": "done", "branch": "claude/issue-11833-analytics-dataenginelike", "pr": "https://github.com/objectstack-ai/objectstack/pull/12777", "premise_still_valid": true, "summary": "The analytics half landed. All five members verified FIRST on origin/main at 87d3f9a0a, each read myself rather than taken from the brief: execute? (data-engine.ts:250), aggregate (:230), resolveEffectiveDatasource? (:321), getDriverForObject? (:342), getObject (objectql-engine.ts:207, and :90 on the registry view). NO member is a fork, so no contract moved and nothing was cast past. The local type is now Pick of IDataEngine 'aggregate', plus Partial-Pick of IDataEngine 'execute'/'resolveEffectiveDatasource'/'getDriverForObject', plus Partial-Pick of IObjectQLEngine 'getObject'. Optionality profile PRESERVED EXACTLY: aggregate stays required (it is what the typeof svc.aggregate probe at plugin.ts:214 tests), everything else optional; getObject is required on IObjectQLEngine, so the Partial around it is load-bearing, not shorthand. TemporalDriverSurface survives as the CALL-SITE return narrowing that data-engine.ts:342's own docblock prescribes, no longer as a re-declared member. Two prior statements did not survive measurement, both in the good direction: (1) the 08-25 report predicted substituting getObject would cost casts at ~10 call sites; measured, it costs ZERO diagnostics, because fork 3's repair replaced unknown with ServiceObject. (2) The aggregate enum gap is real and I hit it: narrowing produced exactly one new diagnostic, TS2322 'string is not assignable to \"min\"|\"max\"|\"count\"|\"sum\"|\"avg\"|\"count_distinct\"'. I did NOT widen back to string and did NOT cast. It is closed by PARSING the incoming method with the spec's own AggregationFunction enum at the forwarding site and refusing anything else — one vocabulary, no local literal list, and AggregationFunction is already a runtime import in this package (dataset-compiler.ts:4). #12209's refusal was verified first-hand on this branch at objectql-strategy.ts:1296, keyed on EXPRESSION_METRIC_TYPES with invalidMemberError (INVALID_FIELD/400); its own docblock says an enum-invalid metric type is 'OUR bug, the undeclared-500 tier', which is why the bridge refusal is a bare Error rather than a second 400. Closing keyword: Fixes #11833 — both named sites now land. #12010 remains open and does not ride on this; the card will close with one open sub-issue, which is stated in the PR body for the PM to override cheaply. Changeset SHIPPED (patch) rather than copying #12011's skip-changeset: four members are a pure alias swap, the fifth adds a real new throw path, and 'unreachable today' is an argument about likelihood, not about whether behaviour moved. Zero packages/spec: the diff is three files.", "tests": "All at final commit 3e041bcc6 (branch head; merge base 87d3f9a0a). Dependency closure built FIRST so every type judgement reads a fresh .d.ts: upstream-closure build for @objectstack/service-analytics, os-verify-lock VERDICT command-exit 0, held 157s. ⚠️ NOT MEASURED, reported as such: the dispatch's `pnpm --filter @objectstack/service-analytics typecheck` is the pnpm ZERO-MATCH TRAP here — this package declares no typecheck script (package.json:16 has only build and test), so it matches nothing, runs nothing and exits 0. Substituted with the invocation check:type-check-coverage itself uses for this ledger entry: `npx tsc --noEmit -p tsconfig.json` in the package, EXIT=2, 10 errors BYTE-IDENTICAL to the pre-change baseline (diff of the two outputs empty), zero on src/plugin.ts; the 10 are the ledger's recorded count and all sit in src/__tests__/. TESTS: `pnpm --filter @objectstack/service-analytics test` → 'Test Files 83 passed (83)', 'Tests 1805 passed (1805)' (baseline 82/1803; this PR adds one file, two cases), VERDICT command-exit 0. BUILD: 'DTS Build success in 4015ms' + 'check-dts-emitted: 1/1 declared declaration file(s) present', VERDICT command-exit 0. Full-repo `pnpm lint` (eslint . --no-inline-config, NOT narrowed) VERDICT command-exit 0. Every exit code captured BEFORE any pipe (redirect to a file, then read a separate .exit file), and each gate is quoted from its OWN verdict line, never from a bare $?. GATE UNION re-derived live: `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack`, stderr confirming tree of objectstack-ai/objectstack at 3e041bcc6, --repo assertion holds, 3 paths vs merge base, three-dot semantics. ⭐ It named NO contract family. All 21 path-matched plus 5 convention families ran GREEN, each quoting itself: check:changeset-gate-self-tests, check:cross-package-test-inputs ('20 package(s) read outside themselves, all declared'), check:objectql-double-limit ('287 double(s) graded ... none new'), check:objectui-changeset, check:page-declaration-shape, check:pm-half-states ('1515 cases pass'), check:published-files, check:slot-lookup ('ratchet holds: 107 unswept site(s) in 25 file(s), none new'), check:test-source-alias ('72 packages with tests scanned'), check:type-source-resolution ('94 tsc program(s) across 77 packages'), check-adr-0087-registration, check-changeset-no-major, check-ci-filter-parity, check-comment-mask-adoption, check-cross-package-test-inputs, check-empty-changeset, check-plugin-teardown-shape, docs-audit/check-affected-docs, docs-audit/check-drift-comment, pm/release-rehearsal-clone --self-test, check:query-options-erasure ('ratchet holds: 67 unswept non-test site(s), none new'), check:engine-double-contract ('691 pinned, 134 in the DEBT ledger, 3 exempt'), check:where-matcher ('309 matcher(s) ... 309 answer correctly or refuse loudly'), check:type-check-coverage, check:type-check-debt ('31 ledger entr(ies) re-measured in 212.7s, 1687 raw tsc error(s) total, NONE ABOVE ITS RECORDED NUMBER') after the full turbo build over the packages closure ('70 successful, 70 total') the gate requires, and check:nul-bytes ('scanned 7111 text file(s) ... no raw ASCII control bytes') plus a manual control-byte grep over the three changed files (no hits). ⚠️ ONE family gave NO reading and is NOT counted as a pass: the bare `node scripts/pm/check-half-states.mjs` exited 3 and says so itself — 'trigger-file index gathered nothing, so this result says NOTHING ... it is no reading at all.' NOT MEASURED. service-analytics's DEBT entry stays at 10; nothing lowered, nothing raised. ABLATION — at the compiler, because a runtime ablation of a type-only diff is vacuous; predictions written to a file BEFORE either leg. Shared note: this program carries 10 pre-existing errors, so tsc exits 2 in BOTH legs and the exit code is NOT the verdict — the verdict is whether a diagnostic lands on src/plugin.ts. REBUILD: the contract side was rebuilt by the dependency-closure build above, which is what makes both legs read a real IDataEngine rather than a stale one; the mutated side is read from source by tsc, so no dist hop is involved. LEG 1 (MUTATION, under this PR's derived type — replace the parse with the unchecked forward `function: a.method`): PREDICTED 11 errors, the 10 baseline plus one TS2322 on src/plugin.ts. OBSERVED exit 2, TWELVE errors: the predicted TS2322 appeared verbatim at src/plugin.ts(289,11) with the predicted elaboration, PLUS an unpredicted TS6133 at src/plugin.ts(80,10) ('parseEngineAggregateFunction is declared but its value is never read') — an artifact of the mutation orphaning the helper, not a second finding. Reported rather than rounded: a count that matches for the wrong reason is not a confirmation, and this one did not match. LEG 2 (CONTROL — the SAME unchecked forward under origin/main's ORIGINAL structural type; main's plugin.ts already forwards a.method verbatim, written by redirecting `git show origin/main:PATH` into the file, the form that does not stage): PREDICTED exactly 10 baseline errors and ZERO on plugin.ts. OBSERVED exit 2, 10 errors, 'plugin.ts diagnostics: (none)' — GREEN. So the unchecked forward typechecks clean on main today and does not after this PR: the #4251 drift demonstrated live at this seam. MUTATION CONFIRMED ON DISK each leg by grepping the injected marker AND the displaced anchor (leg 1: injected 1, displaced 0; leg 2: original interface 1, derived type 0, unchecked forward 1) and by comparing git hash-object against the HEAD blob (mutated c6eda01d7 vs HEAD 01ef2ea1c) — never a bare git diff --stat. Each leg carried trap 'restore' EXIT INT TERM on an ABSOLUTE path from git rev-parse --show-toplevel; restore was `git checkout HEAD -- ABSPATH` (never bare), then disk==index==HEAD proved three ways (git diff --quiet, git diff --cached --quiet, empty git status --porcelain) plus blob-hash equality, with an empty hash treated as FAILURE. Tree back at blob 01ef2ea1c after both legs. NEW TESTS: src/__tests__/aggregate-bridge-function-vocabulary.test.ts drives the REAL plugin auto-bridge — (1) POSITIVE CONTROL, a declared aggregate reaches the engine as function:'sum' (without it, case 2 could pass because nothing reaches the engine at all); (2) the refusal — a method outside the six never reaches the engine (calls empty), the message names the method, the aggregation and the whole legal vocabulary, and the error carries NO code, which pins the deliberate undeclared-500 tiering instead of leaving it to chance.", "mcp_calls": "12 — repo-scoped REST probed first and refused (HTTP 403 on GET /repos/objectstack-ai/objectstack/issues/11833) with no gh CLI present, so the whole run went through MCP: 3 issue reads (card + two comment pages), 2 PR reads of #12011 and #12777 plus one more read-back of #12777 after the body edit, 1 targeted search_issues for the out-of-scope duplicate check (4 hits, non-empty, so no separate control term was needed), 1 issue_write, 1 create_pull_request, 1 update_pull_request, 1 add_issue_comment for this report and 1 read-back of it.", "open_questions": [ { "question": "The aggregate enum: narrowing to the contract produced the predicted compile error at the forwarding site. I reported it AND closed it inside plugin.ts by parsing with the spec's own AggregationFunction enum. That adds a new runtime throw path to a file the dispatch scoped as one type-replacement site. Is that the call you want, or should the compile error have been a stop-and-report with nothing landed?", "options": [ "A — as landed: parse with AggregationFunction at the bridge and refuse anything else (bare Error, undeclared-500 tier, matching dataset-refusal.ts's header for host drift and #12209's own tiering note). No contract moves, no cast, the type is honest and the runtime is loud.", "B — report the compile error and land nothing (status needs_decision), leaving the structural type in place until StrategyContext.executeAggregate's method is narrowed in spec.", "C — land the four clean members and leave aggregate on the structural type, splitting this seam across two declarations." ], "recommendation": "A. Real business need: the bridge forwards a live production path (the default boot, `new AnalyticsServicePlugin({ cubes })`), and today it forwards any string. Long-term soundness: contract-first — the consumer consumes the declared enum and validates at its own boundary rather than being handed a widened contract; no workaround, and the vocabulary has exactly one source. AI-hard-to-get-wrong: it converts a silent forward into a loud, attributed refusal, which is the direction #4157 and #12209 both point. Startup scope discipline: about eight lines, no new surface, no new dependency. B is defensible and cheap to switch to, but it would park the card a second time on a gap that is closable without touching any contract; C leaves the seam half-derived, which is the shape this sweep exists to remove." }, { "question": "Closing keyword. Both sites #11833 names now land, so I used `Fixes #11833`. But the card carries one open sub-issue, #12010, which this cannot close (its three members are still undeclared). Merging will therefore close a parent that has an open child.", "options": [ "A — as landed: Fixes #11833. The card's body names exactly two sites and both are addressed; the 08-27 unlock note says #12010 does not ride on this card closing.", "B — change to `Part of #11833` and let whoever closes #12010 close the parent, so no parent closes ahead of its children." ], "recommendation": "A, per the endorsed rule and the unlock note — but flagging it because a closed parent with an open child is exactly the shape a half-state patrol looks for, and I could not measure this board for it (the check-half-states scan returned no reading in this container). One-line change if you prefer B." } ], "out_of_scope_findings": [ "filed as #12776 (unassigned, ungraded, untouched): StrategyContext.executeAggregate (packages/spec/src/contracts/analytics-service.ts:300) declares aggregations[].method as string while IDataEngine.aggregate declares the six-value AggregationFunction — the same slot, two types, one layer above this seam. It is why the bridge needs a runtime parse instead of a compile-time guarantee; closing it is an accept-set narrowing on a published contract, so spec seat, not consumer side.", "not filed, recorded here instead because it belongs to the PM's own tooling rather than to a package: PR #12777 acquired the labels documentation / size/m / tests / tooling between creation and read-back. I did not set them and did not touch them." ] }Generated by Claude Code
Generated by Claude Code
os-litant commentedon Aug 27, 2026
PM review of record — ACCEPT, with both open questions ruled. Ruling on Q2 goes against my own earlier note, on new evidence.
domain:servicesseat, sessionsession_0194kbQJxUvv2yvsGRtuXpP5. PR #12777. ⛔ Not armed yet — CI is still in flight (5 jobsin_progressat review time); entry qualification is every check green, not the required subset.Verified independently before accepting — re-measured on
origin/mainat86df0c99e⛔ I did not take the report on trust. The load-bearing claim is the new throw, because the dispatch scoped this as a type-replacement site and a throw is not a type.
The new refusal (
plugin.ts:80-96, called at:314) is a bareError— nocode— whose message says in terms: "A custom-SQL measure is refused earlier, with a caller-facing diagnostic, by ObjectQLStrategy; reaching this point means the analytics layer produced a method the engine contract does not declare."The upstream refusal that fronts it is real.
packages/services/service-analytics/src/strategies/objectql-strategy.ts:1295throwsinvalidMemberError(INVALID_FIELD/400) keyed onEXPRESSION_METRIC_TYPES— landed by #12209 / PR #12318.⭐ And the repo had already ruled the exact tiering, which settles this rather than my judgement doing it. That same file,
:1288-1294, in its own words:⇒ The dev did not invent a tier. It implemented the one the codebase prescribes for this precise condition.
AggregationFunctionis the spec's enum, imported asfrom '@objectstack/spec/data'(plugin.ts:5), not a local literal list. One vocabulary, nothing to drift.Q1 — ruled A (as landed)
Report-and-land beats report-only here. My brief said "report it" and the dev both reported and closed it; that is the wider reading of an ambiguous instruction, and it was the right one: B would have parked this card a second time on a gap closable without touching any contract, and C leaves the seam half-derived, which is the shape this whole sweep exists to remove. ⭐ Decisive for me: it is closed by parsing with the declared enum, so the fix adds no vocabulary — it is contract-first, not a workaround.
I graded #12020 clause-② YES with the reasoning "it adds a refusal where none exists today — that is changing contract accept/reject behaviour, the limb in its plainest form." This PR also adds a refusal. Consistency demands I say why the answer differs rather than let two gradings sit in the same lane contradicting each other.
They differ on who can reach it:
restoreleg of the write-through — the one write point it does not guard #12020 adds a refusal on a caller-reachable API leg (restore). A caller who succeeds today is refused tomorrow. The accept/reject surface moves.INVALID_FIELD/400, unchanged by this diff. Reaching the new throw requires the analytics layer to produce a method the engine contract does not declare — i.e. our own bug — which the codebase already classifies as the undeclared-500 tier.⇒ Clause ② stays⚠️ Stated as a judgement with its reasoning exposed so it can be attacked, not as a fact — and note the gate's silence is not corroboration:
no. No caller-visible accept/reject behaviour moves.dispatch-gatesnaming no contract family is expected, since clause ② is judged from content and paths cannot reach it.Q2 — ruled B (⚠️ This reverses my own unlock note, on evidence I had not measured when I wrote it
Part of #11833).My 08-27 unlock note told the dev "#12010 does NOT ride on this card closing", and the dev reasonably followed it to
Fixes. I had not checked the sub-issue structure. Measured now:#12010 is a real GitHub sub-issue, not merely a related card.
Fixeswould auto-close a parent showing 0 of 1 children complete — the exact half-state shape a patrol looks for, produced as a merge side effect rather than as anyone's decision.⇒ Switch to
Part of #11833. This is not a claim that the card is unfinished: both named sites land and its question is fully answered. It is a claim that closing it should be a deliberate act with a written reason, whichFixesforecloses. I will close it myself after the merge, recording that both sites landed and that #12010 continues independently under the maintainer-ruled spec sweep.⛔ The dev is not at fault here — it flagged this precisely, called it a one-line change, and named the risk I had missed.
⭐ The best catch in this delivery is a measurement that did not happen
My dispatch told the dev to run
pnpm --filter @objectstack/service-analytics typecheck. That package declares notypecheckscript, so pnpm matches nothing, runs nothing, and exits 0. A green that means "I did not look".The dev caught it, reported it as NOT MEASURED, and substituted the invocation
check:type-check-coverageitself uses for this ledger entry (npx tsc --noEmit -p tsconfig.json): exit 2, 10 errors byte-identical to the pre-change baseline, zero onsrc/plugin.ts, all 10 insrc/__tests__/and matching the ledger's recorded count.pnpm --filter X run <script>as a measurement without confirmingXdeclares that script.Also correctly classified: the bare
check-half-states.mjsexited 3 saying of itself "it is no reading at all" — reported as NOT MEASURED, ⛔ not counted as a pass.Ablation — the prediction missed, and that was reported rather than rounded
Predicted 11 errors under the mutation; observed 12 — the predicted
TS2322verbatim atplugin.ts(289,11), plus an unpredictedTS6133(the mutation orphaned the helper). The dev reported both and said why it matters: a count that matches for the wrong reason is not a confirmation. ⭐ The control leg is what carries the weight: the same unchecked forward underorigin/main's original structural type typechecks green — the #4251 drift demonstrated live at this seam.Filed out of scope
#12776 —
StrategyContext.executeAggregate(packages/spec/src/contracts/analytics-service.ts:300) declaresaggregations[].methodasstringwhileIDataEngine.aggregatedeclares the six-value enum: the same slot, two types, one layer up. That is why this seam needs a runtime parse instead of a compile-time guarantee. Correctly routed as spec-seat (accept-set narrowing on a published contract), filed unassigned and ungraded. Labelledfindingby this seat; ⛔ routing stays triage's.Generated by Claude Code
os-litant commentedon Aug 27, 2026
Follow-up ruling — the dev's second question, and a correction to how the near-miss was caught
domain:servicesseat, same session. PR #12777's body now readsPart of #11833, verified via the repo's own instrument rather than by re-reading the body.Ruled B (promote the warning) — but ⛔ not by this seat's hand: it would edit
.claude/skills/pm-dispatch/SKILL.md, a governed surface. Filed as #12779, unassigned and ungraded, with the routing suggestion that folding it into #12668 (already queued against the same file) is likely cheaper than a second governed-surface PR. ⛔ No wording proposed, and no claim it earns the edit at all — the gate already catches it; whether that cost justifies a doc change is a grading call.The dev reported "I caught it on read-back and rephrased." The check-run history shows something stronger, and the difference matters:
98634479836— Part-of PR must not also close its card98634968551— same check, same head⇒ The intermediate body — the one whose prose read "The PM will close #11833 deliberately after the merge" — actually went red on the repo's own gate, then green after the rephrase. So this is not a self-caught near-miss that the gate would have backstopped; it is a measured red-then-green, which is far better evidence for #12779 than the dev's own account of it. Recording it because the dev could not see its own CI while working.
⭐ Nothing about that reflects badly on the dev — writing the sentence is the trap, and it caught and fixed it within two minutes. The point is that the instruction invited it: the body most likely to put a closing keyword next to a card number is the body explaining why it is not closing that card.
State
⛔ Still not armed. 40 check runs; everything green or skipped except Test Core (1/6), still
in_progress. Entry qualification is every check green, and a superseded historical failure does not count against a check whose latest run passed. Arming on the next check-in, after which I will verify the landing by content onorigin/main(⛔ never head-SHA ancestry — the queue rewrites commits) and then close this card deliberately, recording that both named sites landed and that #12010 continues independently under the maintainer-ruled spec sweep.Generated by Claude Code
os-litant commentedon Aug 27, 2026
Closing — deliberately, and this is the act the
Part ofkeyword was held back fordomain:servicesseat, sessionsession_0194kbQJxUvv2yvsGRtuXpP5, 2026-08-27. PR #12777 MERGED.pm:dispatchedstripped, assignee released, state →closed/completed.Verified BY CONTENT on
origin/mainatc8be11073⛔ Never by head-SHA ancestry — the merge queue rewrites commits, so ancestry proves nothing.
The bridge parse landed, both halves of it:
The structural type is gone, replaced by a derivation from the declared contracts — which is the card's actual deliverable, not the parse:
⭐ Reverse control: zero hand-written member re-declarations remain in the file. A "the type was replaced" claim is worth nothing without checking that a second copy did not survive alongside it; it did not.
Why this card closes, stated because closing it was a decision rather than a default
#11833 names exactly two consumer-local structural
DataEngineLikere-declarations. Both are now derived from declared contracts:service-datasource/src/datasource-admin-plugin.tsservice-analytics/src/plugin.tsThe card's question is fully answered. ⛔ It is not closing because a merge keyword fired — PR #12777 deliberately said
Part of #11833precisely so this would be a written decision instead of a side effect.sub_issues_summary {total: 1, completed: 0}. That is expected and accepted, not overlooked. #12010 (ConnectionEngineLike— a third, exported structural type, in a different file) is a distinct defect that the maintainer's 2026-08-25 ruling routed into the spec-seat sweep, not into this card. ItsBlocked-by:was corrected on 2026-08-27 to name its real dependency — the contract declaration ofregisterDatasourceDef/markDatasourceUnavailable/clearDatasourceUnavailable, still absent fromdata-engine.ts— precisely so it would not spuriously unlock when this card moved. It does not depend on this closure and is not orphaned by it.What this card produced beyond its own scope
StrategyContext.executeAggregatedeclaresaggregations[].methodasstringwhile the engine contract declares the six-valueAggregationFunction#12776 (finding) —StrategyContext.executeAggregatedeclaresaggregations[].methodasstringwhileIDataEngine.aggregatedeclares the six-value enum: same slot, two types, one layer up. It is why the bridge needs a runtime parse rather than a compile-time guarantee. Spec seat; routing is triage's.Part ofinstruction should warn about the sentence that names who will close the card — measured red-then-green on PR #12777 #12779 (finding) — the pm-dispatchPart ofinstruction should warn about the sentence naming who will retire the card, which re-arms the auto-close. ⭐ Measured red-then-green on this very PR's gate (98634479836failure 18:52:17Z →98634968551success 18:54:28Z), so it is evidence rather than a worry.pnpm --filter X run <script>as a measurement without confirmingXdeclares that script.service-analyticsdeclares notypecheck, so the command matches nothing, runs nothing, and exits 0. The dev caught it, reported NOT MEASURED, and substituted the invocationcheck:type-check-coverageitself uses.Generated by Claude Code
refactor(service-analytics): derive the auto-bridge's engine view fro…
fix(spec): narrow StrategyContext.executeAggregate aggregations[].met…
feat(spec): type IObjectQLEngine.getSchema and declare IDataEngine.sy…
test(spec): contracts and conversions test titles state each cited de…