Skip to content

Commit ddcdc80

Browse files
refactor(scripts,objectql): codemod-split-engine moves bodies that hand the engine on through a facade host port, and computing hand-stub slots; move eight #30020 bodies (#30987)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} --> _Requested by **Elon** · [project thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9DBD7HivCp9UrtDXykvN12jC)_ domain:server#3 · os-elon-musk · session_016WV4d1YRzpYfqDDnUGmMWK Refs #30020 Clause-②: no Metadata-protocol: none Skills-docs: none — an internal move inside `@objectstack/objectql`; nothing a metadata author writes changes ## 维护者速读 - 改了什么:`scripts/codemod-split-engine.mjs` 的第四遍(`--members`)学会三条新规则:方法体里的裸 `this` 改读宿主的 `facade` 项(R1),`this[k]` 这类计算键在键都是字面字段名时经宿主读写(R3),`engine.ts` 已导出的名字(`ScopedContext`)从 `./engine.js` 导入(R4);新增第五遍 `--hand-slots`,把手写桩里会计算的宿主槽位搬进它的手写模块(H2)。随后用它把 `ObjectQL` 上八个把引擎本身传出去的方法体搬进各自的 facet,并删掉登记表里对应的八行例外(46 → 38)。这是 #30020 设计 §5 系列的第一个 PR(E1)。 - 为什么改:#30020 要把两个门面类上剩下的例外行换成委托。这八行被 codemod 以 `bare-this` 拒绝。Elon 于 2026-11-06 14:58Z 裁定规则 A:门面仍是唯一的状态持有者,宿主加一个 `facade` 端口,codemod 在逐字节撤销证明下把 `this` 改写为 `this.facade`。 - 风险与回滚:纯搬移,运行时行为不变。撤销证明仍是逐字节比较,不做任何空白或 import 归一化;两遍各有自己的撤销证明。`.d.ts` 的成员行不变,五个私有方法的 docblock 跟着方法体走,所以共享声明块少了这五段注释。回滚即 revert 本 PR。 - 先看:`scripts/codemod-split-engine.mjs` 的 `identityBodyText` / `identityUndoText`、`literalFieldKeys`、`importsEngineValues`、`runStyles` 和 `generateHandSlots` / `undoHandSlots`;`packages/objectql/src/engine-defaults-and-autonumbers.ts` 的模块头和 import 块。 - 两分钟验证:`node scripts/codemod-split-engine.mjs --self-test` 预期 169 cases in 11 batteries pass;`node scripts/check-facade-delegation-only.mjs` 预期 38 named exception(s)。 - 需要你做的:无,席位落地。 ## What changes - **`scripts/codemod-split-engine.mjs`, fourth pass (`--members`), three rules (#30020 design §3):** - **R1, the identity port.** A moved body that reads a bare `this` (handing the engine on: `bindHooksToEngine(this, …)`, `new ScopedContext(ctx, this)`) now reads `this.facade`, and the pass adds a `facade: engine` entry to the host interface and its install line. The undo turns `this.facade` back into `this` and drops the entry. `typeof this` in a type position stays as written (`this-type`), since it is a type query, not the value. The first and scoped passes are unchanged and still refuse `bare-this`. - **R3, computed field keys.** `this[k]` where every value `k` can take is a literal field name (`readMigrationFlagRowMemoized`'s cache slots) reads and writes those fields through host getters and setters, the same way a named field does. - **R4, a name `engine.ts` exports.** `createContext` and `buildHookApi` construct `ScopedContext`, which `engine.ts` exports. The receiving module imports it from `./engine.js` (the cycle is the one the module header already names for values), and the header's import paragraph switches from the types-only sentence to the values one; the undo switches it back. Refused in a routed module. - **Hand-grown import lines.** `engine-security-resolvers.ts` carries an import line #30227 grew past the wrap width. `runStyles` reads each statement's form (wrapped or one line) and the move and the undo both keep it, so the proof stays byte for byte. - **`scripts/codemod-split-engine.mjs`, fifth pass (`--hand-slots STUB`, H2).** A hand-written stub whose host literal carries a slot that computes (an arrow with a body, not a forwarder) has that slot moved into the stub's hand module as `export const SLOT: HOST['SLOT'] = ARROW;`, one indentation level out, so its contextual types are unchanged; the slot becomes the shorthand `SLOT,`. It has its own undo proof, against the tree `--members` left. Used once here: `serveReferenceNames`' `formulaReadsReference` slot moves into `engine-reference-name-visible.ts`. - **The eight moves** (E1 of the design's §5 series), written by the codemod on the latest `main`: - `bindHooks` into `engine-hooks-and-functions.ts`; - `resolveReadableIdsSecurity` into `engine-security-resolvers.ts`; - `buildHookApi` into `engine-defaults-and-autonumbers.ts`; - `readMigrationFlagRowMemoized` into `engine-migration-state.ts` (R3); - `recomputeSummaries` into `engine-summaries-and-expansion.ts`; - `ownershipQueueService` into `engine-insert.ts`; - `createContext` into `engine-compatibility-section.ts`; - `serveReferenceNames`' slot into `engine-reference-name-visible.ts` (fifth pass). The stubs, facet members, host interfaces, host factories and imports are the passes' own output. `engine.ts` goes from 11,153 to 10,977 lines. - **`scripts/facade-delegation-registry.json`**: the eight `#30020` rows for these members are removed; `check:facade-delegation-only` is green with 38 named exceptions (46 on `main`). - **`.changeset/30020-objectql-identity-port-bodies.md`**: `@objectstack/objectql` patch, following #30571 / #30716 (source maps ship). ## Tests The codemod's self-test (`node scripts/codemod-split-engine.mjs --self-test`): 169 cases in 11 batteries pass. Two batteries are new, each floor pinned at its case count: 1. **identity port, computed keys and exported names (fourth pass, #30020)**, 23 cases: - the moves prove, and two runs write byte-identical trees; - R1 rewrites each bare `this` (an object-literal method keeps its own) and adds the host's identity port under its docblock; - R3 reads a computed key over field names through the host and adds a setter for a key the body assigns through; - R4 imports an exported class from `engine.ts` and switches the header; - a key of any string or over a method name, and `this` in a type position, stay; - refusals: a staying name `engine.ts` does not export, a body that already reads `this.facade`, a class that declares `facade`; - failures: a `this.facade` the rewrite did not write, a site put back to a bare `this`, a factory that does not hand the engine, a port without its docblock, a header left saying "types only"; - a leaf import a hand grew on one line reads as `line` and stays on one line when a move adds a name; the first pass's wrapped form reads as `wrap`; a statement in neither form reads as null. 2. **hand-series host slots (fifth pass, #30020)**, 17 cases: - the plan names the stub's one computing slot; the move proves; two runs write byte-identical trees; - the slot names its const, the hand module holds the arrow one level out under its docblock with the imports it reads, and `engine.ts` drops an import only the slot read; - failures: one edited character in the arrow, an edited docblock, an import binding nothing reads, an `engine.ts` that kept the slot's import, a head that moved nothing; - refusals: a slot that reads the stub's `this` or a local, a stub whose slots all forward, a slot holding a multi-line template, a method that is not a hand stub. <details><summary>Reverse verification (one-time, run on the committed change, restored to the HEAD blob)</summary> Each ablation went red, then `git checkout HEAD -- scripts/codemod-split-engine.mjs` and `git status --porcelain` came back empty: - the members undo without the identity inverse; - the identity port turned off (the real tree refuses `bare-this` again); - `runStyles` with no `line` form (the `engine-security-resolvers.ts` move refuses at its import block, and the new one-line case goes red); - the hand-slots undo with no re-indent. </details> ## Verification The final head merges `origin/main` `1b3e22f0`. `engine.ts` is unchanged between that `main` and the previous merge (`ae82d057`, after #30780 and #30800); `packages/objectql` moved (#30836's bare-name module, `registry.ts`). The moves were regenerated on `1b3e22f0` and compared with the head, not text-merged. - Regeneration, with the two proofs chained: on a clean `1b3e22f0` tree, `--members bindHooks,resolveReadableIdsSecurity,buildHookApi,readMigrationFlagRowMemoized,recomputeSummaries,ownershipQueueService,createContext --write`, then `--members --verify --base origin/main` (exit 0: "7 moved method(s) …; engine.ts 11153 -> 10985 lines; undoing the moves does rebuild the base engine.ts and its modules byte-for-byte once the 6 import binding(s) only moved bodies read are pruned from the base"). That tree was recorded as a scratch commit; `--hand-slots serveReferenceNames --write`, then `--hand-slots --verify --base <that commit>` (exit 0: "1 moved (serveReferenceNames#formulaReadsReference); engine.ts 10985 -> 10977 lines; undoing the moves does rebuild the base engine.ts and its hand modules byte-for-byte once the 1 import binding(s) only moved slots read are pruned from the base (formulaPlanEntry)"). The result equals the head's nine `packages/objectql/src` files byte for byte. The registry's delta against `main` is exactly the eight `#30020` rows removed, none added. - Public `.d.ts` for `@objectstack/objectql`: built at base `1b3e22f0` and at head. With the shared chunk's hash normalised (`util-XXXXXXXX`), `dist/index.d.ts` and `dist/core.d.ts` are byte-equal (sha256 `584d6d85…`, `c48657f3…` on both sides). The shared chunk goes from 10,030 to 9,947 lines: the 83 lines are the five private methods' docblocks, in five hunks, all deletions; no other line differs. Control: `SecurityResolversFacet.resolveReadableIdsSecurity` occurs 0 times in the base `dist/index.js` and once in the head's. - After `pnpm install --frozen-lockfile && pnpm build` on the merged head: `pnpm --filter @objectstack/objectql typecheck` exit 0 (including `check:test-typecheck`); `pnpm --filter @objectstack/objectql test` 559 files, 11,200 tests passed; `pnpm --filter @objectstack/spec check:generated` all 14 artifacts up to date (with `NODE_OPTIONS=--max-old-space-size=8192`: its `check:test-typecheck` runs out of heap at the default 2 GB here; this PR does not touch `packages/spec`). - `node scripts/pm/dispatch-gates.mjs --commands`: 74 families derived at head `923ebb82`; all 74 exit 0, including `pnpm check:dual-build-cjs-loads` (every package's `dist/` was built this time). `--ran`: 74 derived, 74 run, 0 NOT-MEASURED, 0 UNRUN. The codemod's and the facade gate's self-tests, `pnpm --filter @objectstack/spec check:liveness`, `pnpm check:adr-anchors` and eslint over the changed files are outside the derived set and were run separately, exit 0. ## Acceptance notes - **Rule A**, Elon's ruling on #30020 (2026-11-06 14:58Z, forwarded by the seat): the facade stays the one owner of state; hosts gain a `facade` port, and the codemod rewrites `this` to `this.facade` under the byte-exact undo. A facet declares and reaches the state it needs through its host. No field moves. - **Series:** this is E1 of the design's §5 order. E2 (`find`, `findOne`) waits for this PR to land. - **R7 is deferred to E6**, its only caller (`update`, which starts `engine-update.ts`). Shipping it here would leave a rule with no caller. - **`.d.ts` is not byte-equal**: the five private methods' docblocks moved with their bodies (the first pass's docblock rule), so the shared declaration chunk loses those docblocks. No member line changes. The design's "byte-equal" expectation was too strong. - Surface widened by the seat for this card: `scripts/codemod-split-engine.mjs`. This PR touches no `packages/spec/liveness/*.json` and no `system-context.mdx` register. ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (Not touched.) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` is CI's run. - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed. (Not touched.) - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English (the 维护者速读 block is the lane's maintainer summary). - [x] The *Before Submitting* list in CONTRIBUTING.md is done. --- _Generated by [Claude Code](https://claude.ai/code/session_016WV4d1YRzpYfqDDnUGmMWK)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent e7ce606 commit ddcdc80

12 files changed

Lines changed: 1279 additions & 327 deletions
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@objectstack/objectql': patch
3+
---
4+
5+
refactor(objectql): eight `ObjectQL` bodies that hand the engine on move into their facets
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
- The bodies of `bindHooks`, `createContext`, `buildHookApi`, `resolveReadableIdsSecurity`, `ownershipQueueService`, `readMigrationFlagRowMemoized` and `recomputeSummaries` move out of `engine.ts` into the facet module of their section, and the `formulaReadsReference` slot of `serveReferenceNames`' host moves into `engine-reference-name-visible.ts`. Each method stays on the class as a one-call stub with its signature, and a public method keeps its docblock there. A body that handed the engine itself on (`bindHooksToEngine(this, …)`, `new ScopedContext(ctx, this)`, a symbol slot read off the engine) reads the same engine through its host's `facade` entry.
11+
- No behaviour changes and nothing to do on upgrade. The built declaration files keep every member and every public or protected docblock; the five private methods' docblocks moved with their bodies, so they no longer appear in the shared declaration chunk, and the source maps move.

‎packages/objectql/src/engine-compatibility-section.ts‎

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* [#29986 codemod] The "Compatibility / convenience API" section of `ObjectQL`:
55
* the bodies of `unregisterObject`, `getObject`, `getDriverByName`,
66
* `getDriverForObject`, `dropObjectSchema`, `datasource`, `on`, `close`,
7-
* `create` moved out of `engine.ts` whole by
7+
* `createContext`, `create` moved out of `engine.ts` whole by
88
* `scripts/codemod-split-engine.mjs`.
99
*
1010
* A MOVE, not a redesign. {@link CompatibilitySectionFacet} is a stateless facet of `ObjectQL`
@@ -34,8 +34,11 @@
3434

3535
import type { HookContext, ServiceObject } from '@objectstack/spec/data';
3636
import { StorageNameMapping } from '@objectstack/spec/system';
37+
import { ExecutionContextSchema } from '@objectstack/spec/kernel';
38+
import type { ExecutionContext } from '@objectstack/spec/kernel';
39+
import { carryAppAutomationElevation } from '@objectstack/core';
3740
import type { IDataDriver } from '@objectstack/core';
38-
import { ObjectQL } from './engine.js';
41+
import { ObjectQL, ScopedContext } from './engine.js';
3942

4043
/**
4144
* The `ObjectQL` members the bodies of {@link CompatibilitySectionFacet} read — every one of
@@ -48,6 +51,8 @@ export interface CompatibilitySectionHost {
4851
readonly _registry: ObjectQL['_registry'];
4952
destroy: ObjectQL['destroy'];
5053
readonly drivers: ObjectQL['drivers'];
54+
/** The engine itself: a moved body reads `this.facade` where the method read a bare `this` (#30020). */
55+
readonly facade: ObjectQL;
5156
getDriver: ObjectQL['getDriver'];
5257
getDriverForObject: ObjectQL['getDriverForObject'];
5358
getSchema: ObjectQL['getSchema'];
@@ -113,6 +118,18 @@ export class CompatibilitySectionFacet {
113118
return this.destroy();
114119
}
115120

121+
/** The body of `ObjectQL#createContext`; the method's docblock there is its contract. */
122+
static createContext(this: Pick<CompatibilitySectionHost, 'facade'>, ctx: Partial<ExecutionContext>): ScopedContext {
123+
// [#25653] The parse keeps declared keys only, so the app-automation
124+
// elevation mark an action body's envelope carries is carried across it:
125+
// dropped here, the body's `ctx.api` writes would reach the
126+
// identity-assignment refusal unmarked.
127+
return new ScopedContext(
128+
carryAppAutomationElevation(ctx, ExecutionContextSchema.parse(ctx)),
129+
this.facade
130+
);
131+
}
132+
116133
/** The body of `ObjectQL#create`; the method's docblock there is its contract. */
117134
static async create(config: {
118135
datasources?: Record<string, IDataDriver>;

‎packages/objectql/src/engine-defaults-and-autonumbers.ts‎

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
/**
44
* [#29986 codemod] The "Field defaults and autonumbers" section of `ObjectQL`:
55
* the bodies of `resolveNowDefault`, `normalizeExpressionDefault`,
6-
* `resolveOptionDefault`, `applyFieldDefaults`, `defaultFieldEntries`,
7-
* `applyAutonumbers`, `adoptExplicitAutonumber`, `createWithAutonumberResync`,
8-
* `isIssuedAutonumberCollision`, `warnAutonumberReadersUnserved`,
9-
* `seedAutonumber` moved out of `engine.ts` whole by
10-
* `scripts/codemod-split-engine.mjs`.
6+
* `buildHookApi`, `resolveOptionDefault`, `applyFieldDefaults`,
7+
* `defaultFieldEntries`, `applyAutonumbers`, `adoptExplicitAutonumber`,
8+
* `createWithAutonumberResync`, `isIssuedAutonumberCollision`,
9+
* `warnAutonumberReadersUnserved`, `seedAutonumber` moved out of `engine.ts`
10+
* whole by `scripts/codemod-split-engine.mjs`.
1111
*
1212
* A MOVE, not a redesign. {@link DefaultsAndAutonumbersFacet} is a stateless facet of `ObjectQL`
1313
* (ruling 6624345766): each static method of it is the body of the `ObjectQL`
@@ -28,12 +28,15 @@
2828
* class, and a field a body assigns goes with a SETTER that assigns it on the
2929
* engine.
3030
*
31-
* The module imports `engine.ts` for types only, so the two modules form no
32-
* evaluation cycle. `index.ts` re-exports nothing from this module.
31+
* Values that stayed in `engine.ts` are imported from it (lever (a) of
32+
* ruling 6591753301); every use is inside a function body, so the import
33+
* cycle is never read at evaluation time. `index.ts` re-exports nothing from
34+
* this module.
3335
*/
3436

3537
import { isCurrentUserDefaultToken, isNowDefaultToken, missingFieldValues, parseAutonumberFormat, renderAutonumber, resolveAutonumberFormat } from '@objectstack/spec/data';
3638
import type { EngineQueryOptions } from '@objectstack/spec/data';
39+
import type { IDataEngine } from '@objectstack/core';
3740
import { isUniqueViolationError, keysetWalk, uniqueViolationColumn } from '@objectstack/types';
3841
import type { KeysetPageQuery } from '@objectstack/types';
3942
import type { ExecutionContext } from '@objectstack/spec/kernel';
@@ -46,6 +49,7 @@ import {
4649
readStoredAutonumberCounter, recomputeAutonumberReaders,
4750
} from './engine-top-level.js';
4851
import type { AutonumberReaders, FormulaPlanEntry, IssuedAutonumber } from './engine-top-level.js';
52+
import { ScopedContext } from './engine.js';
4953
import type { ObjectQL } from './engine.js';
5054

5155
/**
@@ -61,6 +65,8 @@ export interface DefaultsAndAutonumbersHost {
6165
readonly autonumberCounters: ObjectQL['autonumberCounters'];
6266
readonly autonumberReadersUnservedReported: ObjectQL['autonumberReadersUnservedReported'];
6367
defaultFieldEntries: ObjectQL['defaultFieldEntries'];
68+
/** The engine itself: a moved body reads `this.facade` where the method read a bare `this` (#30020). */
69+
readonly facade: ObjectQL;
6470
find: ObjectQL['find'];
6571
getSchema: ObjectQL['getSchema'];
6672
isIssuedAutonumberCollision: ObjectQL['isIssuedAutonumberCollision'];
@@ -189,6 +195,17 @@ export class DefaultsAndAutonumbersFacet {
189195
return this.resolveNowDefault(fieldType, value);
190196
}
191197

198+
/**
199+
* Build a HookContext.api: a ScopedContext that hooks can use to
200+
* read/write other objects within the same execution context.
201+
* Falls back to a system-elevated empty context when no execCtx
202+
* is supplied (e.g. system-triggered hooks).
203+
*/
204+
static buildHookApi(this: Pick<DefaultsAndAutonumbersHost, 'facade'>, execCtx?: ExecutionContext): ScopedContext {
205+
const safeCtx: ExecutionContext = execCtx ?? ({ isSystem: true } as any);
206+
return new ScopedContext(safeCtx, this.facade as unknown as IDataEngine);
207+
}
208+
192209
/**
193210
* The initial value a field's OPTION LIST declares — the option marked
194211
* `default: true` — or `undefined` when it declares none (#7246).

‎packages/objectql/src/engine-hooks-and-functions.ts‎

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@
66
* `ObjectQL`: the bodies of `recordResidualOwner`, `residualOwnerSource`,
77
* `getStatus`, `registerHook`, `unregisterHooksByPackage`, `registerFunction`,
88
* `resolveFunction`, `resolveFunctionEntry`, `unregisterFunctionsByPackage`,
9-
* `setDefaultBodyRunner`, `getDefaultBodyRunner`, `setDefaultActionRunner`,
10-
* `getDefaultActionRunner`, `triggerHooks`, `hasHooksFor`,
11-
* `opensAfterAbortUnit`, `dispatchUnscopedMultiWriteHooks` moved out of
12-
* `engine.ts` whole by `scripts/codemod-split-engine.mjs`.
9+
* `bindHooks`, `setDefaultBodyRunner`, `getDefaultBodyRunner`,
10+
* `setDefaultActionRunner`, `getDefaultActionRunner`, `triggerHooks`,
11+
* `hasHooksFor`, `opensAfterAbortUnit`, `dispatchUnscopedMultiWriteHooks` moved
12+
* out of `engine.ts` whole by `scripts/codemod-split-engine.mjs`.
1313
*
1414
* A MOVE, not a redesign. {@link HooksAndFunctionsFacet} is a stateless facet of `ObjectQL`
1515
* (ruling 6624345766): each static method of it is the body of the `ObjectQL`
@@ -42,6 +42,8 @@ import { HookTargetRebindError } from './hook-target-rebind-errors.js';
4242
import { declaredHookCondition } from './hook-wrappers.js';
4343
import { CoreServiceName } from '@objectstack/spec/system';
4444
import { bindHookNotice } from './hook-notice.js';
45+
import { bindHooksToEngine } from './hook-binder.js';
46+
import type { BindHooksOptions } from './hook-binder.js';
4547
import {
4648
DISPATCHABLE_HOOK_EVENTS, assertDispatchableHookEvent, assertHookScopeNotSelfCancelling,
4749
assertValidHookExcludeObjects, assertValidHookObject, assertValidUnscopedMultiWriteFlag, hookMatchesObject,
@@ -61,6 +63,8 @@ export interface HooksAndFunctionsHost {
6163
_defaultBodyRunner: ObjectQL['_defaultBodyRunner'];
6264
readonly defaultDriver: ObjectQL['defaultDriver'];
6365
readonly drivers: ObjectQL['drivers'];
66+
/** The engine itself: a moved body reads `this.facade` where the method read a bare `this` (#30020). */
67+
readonly facade: ObjectQL;
6468
readonly functions: ObjectQL['functions'];
6569
getDriver: ObjectQL['getDriver'];
6670
readonly hooks: ObjectQL['hooks'];
@@ -229,6 +233,33 @@ export class HooksAndFunctionsFacet {
229233
return removed;
230234
}
231235

236+
/** The body of `ObjectQL#bindHooks`; the method's docblock there is its contract. */
237+
static bindHooks(this: Pick<HooksAndFunctionsHost, '_defaultBodyRunner' | 'facade' | 'logger'>, hooks: any[] | undefined, opts?: {
238+
packageId?: string;
239+
functions?: Record<string, HookHandler>;
240+
bodyRunner?: any;
241+
strict?: boolean;
242+
warnLegacyHandler?: boolean;
243+
metrics?: any;
244+
/** Wraps each in-process handler after its name resolves — see {@link BindHooksOptions.wrapHandler}. */
245+
wrapHandler?: BindHooksOptions['wrapHandler'];
246+
}): void {
247+
const merged = { ...(opts ?? {}), logger: this.logger } as any;
248+
if (!merged.bodyRunner && this._defaultBodyRunner) {
249+
merged.bodyRunner = this._defaultBodyRunner;
250+
}
251+
if (merged.strict === undefined && (this.facade as any)._strictHookBinding) {
252+
merged.strict = true;
253+
}
254+
if (merged.warnLegacyHandler === undefined && (this.facade as any)._warnLegacyHandler) {
255+
merged.warnLegacyHandler = true;
256+
}
257+
if (!merged.metrics && (this.facade as any)._hookMetricsRecorder) {
258+
merged.metrics = (this.facade as any)._hookMetricsRecorder;
259+
}
260+
bindHooksToEngine(this.facade, hooks, merged);
261+
}
262+
232263
/** The body of `ObjectQL#setDefaultBodyRunner`; the method's docblock there is its contract. */
233264
static setDefaultBodyRunner(this: Pick<HooksAndFunctionsHost, '_defaultBodyRunner' | 'logger'>, runner: any): boolean {
234265
if (this._defaultBodyRunner) {

‎packages/objectql/src/engine-insert.ts‎

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
/**
44
* [#29986 codemod] The "Write preparation and insert" section of `ObjectQL`:
55
* the bodies of `staticReadonlyCreateStrip`, `updateByIdExpecting`,
6-
* `assignment`, `readUpdatePriorRow`, `insertMany` moved out of `engine.ts`
7-
* whole by `scripts/codemod-split-engine.mjs`.
6+
* `ownershipQueueService`, `assignment`, `readUpdatePriorRow`, `insertMany`
7+
* moved out of `engine.ts` whole by `scripts/codemod-split-engine.mjs`.
88
*
99
* A MOVE, not a redesign. {@link InsertFacet} is a stateless facet of `ObjectQL`
1010
* (ruling 6624345766): each static method of it is the body of the `ObjectQL`
@@ -33,7 +33,8 @@ import type { WriteObservabilityOptions } from '@objectstack/spec/contracts';
3333
import type { ExecutionContext } from '@objectstack/spec/kernel';
3434
import type { IDataDriver } from '@objectstack/core';
3535
import { ConcurrentUpdateError } from './concurrent-update-error.js';
36-
import { ASSIGNMENT_RULE_TYPE, AssignmentRuleEvaluator } from './assignment-rules.js';
36+
import { ASSIGNMENT_RULE_TYPE, AssignmentRuleEvaluator, OWNERSHIP_QUEUE_SOURCE_SLOT } from './assignment-rules.js';
37+
import type { AssignmentQueueService } from './assignment-rules.js';
3738
import type { EvalPermissions } from '@objectstack/formula';
3839
import { stripReadonlyFields } from './validation/rule-validator.js';
3940
import type { staticReadonlyInsertSubject } from './validation/rule-validator.js';
@@ -55,6 +56,8 @@ export interface InsertHost {
5556
assignmentEvaluator: ObjectQL['assignmentEvaluator'];
5657
buildDriverOptions: ObjectQL['buildDriverOptions'];
5758
count: ObjectQL['count'];
59+
/** The engine itself: a moved body reads `this.facade` where the method read a bare `this` (#30020). */
60+
readonly facade: ObjectQL;
5861
find: ObjectQL['find'];
5962
getDriver: ObjectQL['getDriver'];
6063
insert: ObjectQL['insert'];
@@ -180,6 +183,21 @@ export class InsertFacet {
180183
return driver.findOne(object, { object, where: { id }, limit: 1 } as QueryAST, options);
181184
}
182185

186+
/** The registered ownership-queue service, through the plugin's slot — `null` on every other state. */
187+
static ownershipQueueService(this: Pick<InsertHost, 'facade'>): AssignmentQueueService | null {
188+
const source = (this.facade as unknown as Record<symbol, unknown>)[OWNERSHIP_QUEUE_SOURCE_SLOT];
189+
if (typeof source !== 'function') return null;
190+
try {
191+
const svc = (source as () => unknown)();
192+
return svc && typeof (svc as AssignmentQueueService).resolveQueue === 'function'
193+
&& typeof (svc as AssignmentQueueService).expandMembers === 'function'
194+
? (svc as AssignmentQueueService)
195+
: null;
196+
} catch {
197+
return null;
198+
}
199+
}
200+
183201
/** The body of `ObjectQL#assignment`; the method's docblock there is its contract. */
184202
static assignment(this: Pick<InsertHost, '_registry' | 'assignmentEvaluator' | 'count' | 'find' | 'insert' | 'logger' | 'ownershipQueueService' | 'update'>): AssignmentRuleEvaluator {
185203
if (!this.assignmentEvaluator) {

‎packages/objectql/src/engine-migration-state.ts‎

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77
* `mediaValueShapeStrictFor`, `objectHasCoveredValueField`,
88
* `valueShapeStrictFor`, `isFileReferencesMigrationVerified`,
99
* `haveFileColumnsMoved`, `readMigrationFlagMemoized`,
10-
* `valueShapeViolationsAdmitted`, `admittedViolationSink`,
11-
* `recordObservedDeviation`, `retractCreationAttestation`,
12-
* `invalidateDataMigrationFlags`, `destroy` moved out of `engine.ts` whole by
13-
* `scripts/codemod-split-engine.mjs`.
10+
* `readMigrationFlagRowMemoized`, `valueShapeViolationsAdmitted`,
11+
* `admittedViolationSink`, `recordObservedDeviation`,
12+
* `retractCreationAttestation`, `invalidateDataMigrationFlags`, `destroy` moved
13+
* out of `engine.ts` whole by `scripts/codemod-split-engine.mjs`.
1414
*
1515
* A MOVE, not a redesign. {@link MigrationStateFacet} is a stateless facet of `ObjectQL`
1616
* (ruling 6624345766): each static method of it is the body of the `ObjectQL`
@@ -46,7 +46,7 @@ import { DATA_MIGRATION_FLAG_OBJECT, FILE_REFERENCES_MIGRATION_ID, isDataMigrati
4646
import type { ExecutionContext } from '@objectstack/spec/kernel';
4747
import { isScannableValueShapeField } from './validation/record-validator.js';
4848
import type { AdmittedValueShapeViolationSink } from './validation/record-validator.js';
49-
import type { AdmittedValueShapeViolationTally } from './engine-top-level.js';
49+
import type { AdmittedValueShapeViolationTally, MigrationFlagRead } from './engine-top-level.js';
5050
import { ObjectQLStatics as ObjectQL } from './engine.js';
5151
import type { ObjectQL as ObjectQLClass } from './engine.js';
5252

@@ -75,6 +75,7 @@ export interface MigrationStateHost {
7575
objectHasMediaField: ObjectQLClass['objectHasMediaField'];
7676
readFileReferencesFlagRow: ObjectQLClass['readFileReferencesFlagRow'];
7777
readMigrationFlagRowMemoized: ObjectQLClass['readMigrationFlagRowMemoized'];
78+
readMigrationFlagVerified: ObjectQLClass['readMigrationFlagVerified'];
7879
readonly recordedDeviations: ObjectQLClass['recordedDeviations'];
7980
readonly retractedCreationAttestations: ObjectQLClass['retractedCreationAttestations'];
8081
unregisterDriver: ObjectQLClass['unregisterDriver'];
@@ -195,6 +196,30 @@ export class MigrationStateFacet {
195196
return (await this.readMigrationFlagRowMemoized(slot, migrationId, verifiedLog)).verified;
196197
}
197198

199+
/**
200+
* The same memoized read, handed back WHOLE (#15989).
201+
*
202+
* The row carries two facts a consumer can want — is the migration verified,
203+
* and have the columns moved — and they must come from one read: two reads
204+
* can straddle a `--apply` and answer out of one another's date, which for
205+
* these two facts means "verified, columns not moved" (the JSON arm) and
206+
* "not verified, columns moved" (an unreachable state) both become
207+
* observable. {@link readMigrationFlagMemoized} is this function projected
208+
* onto its first field, so the two can never disagree.
209+
*/
210+
static async readMigrationFlagRowMemoized(this: Pick<MigrationStateHost, 'fileReferencesMigrationVerified' | 'readMigrationFlagVerified' | 'valueShapesMigrationVerified'>, slot: 'fileReferencesMigrationVerified' | 'valueShapesMigrationVerified', migrationId: string, verifiedLog: string): Promise<MigrationFlagRead> {
211+
const cached = this[slot];
212+
if (cached) return await cached;
213+
const pending = this.readMigrationFlagVerified(migrationId, verifiedLog);
214+
this[slot] = pending;
215+
const result = await pending;
216+
// Only a read that actually consulted the ledger may be remembered. Clear
217+
// by identity so a concurrent `invalidateDataMigrationFlags()` (or a
218+
// re-read that already replaced this slot) is not undone here.
219+
if (!result.conclusive && this[slot] === pending) this[slot] = null;
220+
return result;
221+
}
222+
198223
/** The body of `ObjectQL#valueShapeViolationsAdmitted`; the method's docblock there is its contract. */
199224
static valueShapeViolationsAdmitted(this: Pick<MigrationStateHost, 'admittedValueShapeViolations'>): Record<string, AdmittedValueShapeViolationTally> {
200225
const out: Record<string, AdmittedValueShapeViolationTally> = {};

‎packages/objectql/src/engine-reference-name-visible.ts‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ import type { ExecutionContext } from '@objectstack/spec/kernel';
7878
import { withoutOperationPrivateKeys } from '@objectstack/core';
7979
import type { ObjectQL } from './engine.js';
8080
import { isReadRefusal } from './engine-file-reference-hydration.js';
81+
import { formulaPlanEntry } from './engine-top-level.js';
8182

8283
/**
8384
* The `ObjectQL` members {@link serveReferenceNames} reads, every one of them
@@ -263,3 +264,19 @@ async function servableNameField(
263264
return undefined;
264265
}
265266
}
267+
268+
/**
269+
* [#30020 codemod] The `formulaReadsReference` slot of the host
270+
* `ObjectQL#serveReferenceNames` hands {@link serveReferenceNames}: moved here
271+
* whole by `--hand-slots` of `scripts/codemod-split-engine.mjs`, and the slot
272+
* names it.
273+
*/
274+
export const formulaReadsReference: ReferenceNameVisibleHost['formulaReadsReference'] = (name, def, fields) => {
275+
try {
276+
const entry = formulaPlanEntry(name, def, fields);
277+
return entry.hops !== undefined || entry.hopConflict !== undefined;
278+
} catch {
279+
// An expression the planner cannot compile is not served by name.
280+
return true;
281+
}
282+
};

0 commit comments

Comments
 (0)