Skip to content

Commit 13aa6a3

Browse files
refactor(objectql): move the ObjectQL constructor body into the hooks-and-functions facet (E3) (#31151)
<!-- claude-projects-attribution: {"github_login":"marchtian"} --> _Requested by **田騏** · [project thread](https://claude.ai/code/project/chan_01VmuUhXU9SSUzqJYyXSmYSi?thread=cmsg_01VmuUhXU9SSUzqJYyXSmYSiP9RS1KjicEPyef7VNhFzFa)_ domain:server#2 · marchtian · session_017ihL2fL8RrJDkowuFhXUPG Refs #30020 Clause-②: no Metadata-protocol: none Skills-docs: none — an internal move inside `@objectstack/objectql` plus a build script; nothing a metadata author writes changes ## 维护者速读 - 改了什么:给 `scripts/codemod-split-engine.mjs` 加了构造函数能力和新规则 R8(确定赋值 wiring),再用它把 `ObjectQL` 构造函数体搬进 `engine-hooks-and-functions.ts` 的 `construct`;构造函数只留签名、一行 `this.logger = …constructLogger(hostContext)` 和一次 `construct.call(…)`。登记表删掉构造函数这一行例外(17 → 16)。这是 #30020 §5 系列的 E3。 - 为什么改:构造函数是登记表里一条 "state-owning" 例外。`logger` 是严格模式下只能在构造函数里赋值的字段,整句挪出就是 TS2564,所以席位裁定走方案 A:R8 只把这一句的赋值留在类上、右值挪进 facet,其余语句按 R1 照搬。 - 风险与回滚:纯搬移,同样的语句、同样的顺序执行。撤销证明逐字节通过;三个公开 `.d.ts` 与 main 构建结果逐字节相同;objectql 全量测试通过。回滚即 revert 本 PR。 - 先看:`packages/objectql/src/engine.ts` 的构造函数(3 行);`engine-hooks-and-functions.ts` 的 `constructLogger` / `construct`;codemod 里 `definiteAssignmentsOf`、`ctorStubBody`、`verifyCtorMove`、`ctorUndoText`。 - 两分钟验证:`node scripts/codemod-split-engine.mjs --members --verify --base origin/main` 预期 exit 0,输出 “1 moved method(s) (constructor); engine.ts 10739 -> 10712 lines; undoing the moves does rebuild the base … byte-for-byte”。 - 需要你做的:无,席位落地。codemod 新增了一条规则,是否要契约复审由席位决定。 ## What changes - **Codemod capability: the constructor (`--members constructor`).** The members pass now plans, writes and proves a move of `ObjectQL`'s constructor. The body becomes `static construct(this: Pick<Host, …>, …): void` in its section's facet. It takes only the parameters it reads, because the package builds with `noUnusedParameters`. The constructor keeps its signature and becomes the marker line, the R8 assignments, and one `Facet.construct.call(host.of(this), …)`. - **New rule R8, definite-assignment wiring (seat ruling: option A).** A field a strict compiler requires the constructor to assign is one with a declared type, no initializer, no `?`/`!`, and a type that does not admit `undefined`. That assignment stays on the class. Each **leading** `this.F = EXPR;` whose EXPR reads no `this`, `super` or `arguments` and no top-level name of `engine.ts` becomes `this.F = Facet.constructF(<params EXPR reads>)`. `constructF` returns EXPR, typed as `ObjectQL['F']`. The facet body keeps a marker line where the statement stood, and the undo turns the marker back into the statement. Any other write to such a field keeps the constructor whole (stay reason `definite-assignment`). A class that already declares `construct` refuses (`ctor-facet-name`). - **The move (E3).** `node scripts/codemod-split-engine.mjs --members constructor --write` on `main` `af3451b08b`. Output tree sha256 `f0552217…`, the same on a second run from the base. It writes: - in `engine.ts`, the 3-line constructor and three new host-factory entries (`_registry`, `manifests`, `registerMiddleware`); - in `engine-hooks-and-functions.ts`, the two facet members, three `HooksAndFunctionsHost` members and four imports; - four import bindings pruned from `engine.ts` (`STORED_METADATA_TYPE_READ_GATE_OBJECTS`, `createLogger`, `startSummaryRefreshOnEnable`, `storedMetadataTypeReadGate`). Nothing was hand-edited. `engine.ts` goes from 10,739 to 10,712 lines. - **`scripts/facade-delegation-registry.json`**: the `ObjectQL` · `constructor` row (`#30020`) is removed. Exceptions 17 → 16. The `#30020` rows left in `engine.ts` are `update`, `insert`, `delete`, `[SEED_USER_GRANTS]` and `transaction`. The gate accepts the new constructor as wiring: `this.x = <forwarding call>` plus a forwarding call. - **`.changeset/30020-objectql-constructor-facet.md`**: `@objectstack/objectql` patch (the source maps ship). - **Anchors:** none to repoint. No `packages/spec/liveness/*.json` or `docs/qa/platform-checklist/**` anchor cites any of the four pruned names as `engine.ts#…`, or the constructor. `packages/spec/**` is untouched. ### Constructor statements and how each moved | # | Statement | Rule | Where it is now | |---|---|---|---| | 1 | `this.logger = hostContext.logger \|\| createLogger({…});` | **R8** (`logger` is `private logger: Logger;`, a required field) | The assignment stays on the class: `this.logger = HooksAndFunctionsFacet.constructLogger(hostContext);`. The right side is in `constructLogger`. | | 2 | `this._registry.onPackageUninstalled((id) => { this.manifests.delete(id); });` | host read (`_registry`, `manifests` getters) | `construct`, byte for byte | | 3 | `this._registry.onPackageEnabled((id) => startSummaryRefreshOnEnable(this, this.logger, id));` | **R1** (`this` → `this.facade`) | `construct` | | 4 | `if (OBJECTQL_STRICT_HOOKS) (this as any)._strictHookBinding = true;` | **R1** (`(this.facade as any)…`) | `construct` | | 5 | `if (OBJECTQL_WARN_LEGACY_HANDLER) (this as any)._warnLegacyHandler = true;` | **R1** | `construct` | | 6 | `for (…) this.registerMiddleware(storedMetadataTypeReadGate, { object });` | host method (`registerMiddleware`) | `construct` | | 7 | `this.logger.info('ObjectQL Engine Instance Created');` | host read (`logger` getter) | `construct` | Measured before the move: - No written field is `readonly`. - No field initializer reads a field the constructor writes, and the class has no `super`. - The constructor runs after every field initializer, as before; statement order is unchanged. `logger` is assigned first, then `construct` runs the rest, so `this.logger` reads the same value it read before. - Statements 4–5 write through the engine itself (`this.facade`), not a host setter, so the flags land on the engine as before. ### Rules applied - **Ruling A (6687865760)**: the facade stays the sole state owner. No field moves. - **R1** (identity port): three sites, statements 3–5. - **R8** (new, this PR): statement 1. - **R3, R4, H2, R7**: not reached. - **Stop condition**: not hit. Statement 1 is the case the order names (a field only a constructor may assign). The seat ruled option A, a codemod capability, instead of a partial move or a terminal row; B (a `!` on the field) was rejected as weakening the compiler's check. ## Tests - **Codemod self-test**: a new battery, "constructor and definite-assignment wiring (fourth pass, #30020 R8)", 17 cases, floor pinned at 17. - Positive: the constructor moves and proves; two runs are byte-identical; exact stub, R8 member and `construct` shapes; imports pruned and moved; setters only for fields the body writes. - Refusals: a required field written after another statement; an R8 right side reading `this`; one reading a top-level name of `engine.ts`; a class declaring `construct`. - Tampered heads that must fail: an edited R8 right side; a stub missing its R8 line; a marker naming another member; a statement moved above the marker. - No package test changes: a byte-proven move with no behaviour change. ## Verification Readings were taken at `b03d09ffb5` (base `af3451b08b`). `3a2c4b4027` merges `origin/main` `9f61e06e6f`, two commits that touch none of these files. At the merge, the undo proof (`--verify --base origin/main`, exit 0, same line) and `--ran` (75/75) were re-run. <details><summary>Undo proof (byte-exact), determinism, R8 ablation</summary> - `node scripts/codemod-split-engine.mjs --members --verify --base origin/main`: exit 0. "undo proof: 1 moved method(s) (constructor); engine.ts 10739 -> 10712 lines; undoing the moves does rebuild the base engine.ts and its modules byte-for-byte once the 4 import binding(s) only moved bodies read are pruned from the base (STORED_METADATA_TYPE_READ_GATE_OBJECTS, createLogger, startSummaryRefreshOnEnable, storedMetadataTypeReadGate)". - Determinism: regenerating from the base files with the final codemod writes `engine.ts` (`b612db91…`) and `engine-hooks-and-functions.ts` (`e6599e4b…`) byte-identical to the committed ones (`cmp` clean). Input sha256 `9f090702…` = `git show af3451b:packages/objectql/src/engine.ts | sha256sum`. - **R8 ablation** (temporary local patch, files restored and hash-checked afterwards): - R8 off: `definiteAssignmentsOf` returns nothing. The codemod first refuses on its own ("host member logger is read-only there, and a moved body assigns it"). Forcing past that refusal and the undo gate, the generated constructor is the bare `construct.call(…, hostContext)`, and `tsc --noEmit` in `packages/objectql` fails: `src/engine.ts(529,11): error TS2564: Property 'logger' has no initializer and is not definitely assigned in the constructor.` (exit 2). - R8 on: the committed tree, `tsc --noEmit` exit 0. - `node scripts/codemod-split-engine.mjs --self-test`: exit 0, 186 cases in 12 batteries (169 in 11 before). </details> <details><summary>Public declaration files</summary> `@objectstack/objectql` was built with `pnpm --filter @objectstack/objectql build`, with the two source files at base `af3451b08b` and then at head. `dist/core.d.ts` (`a8fd9093…`), `dist/index.d.ts` (`3a798237…`) and `dist/util-BK7LVy6k.d.ts` (`24ed9187…`) are byte-equal: `diff -r` is empty. Control: the stub text `HooksAndFunctionsFacet.construct.call(hooksAndFunctionsHost.of(this))` occurs 0 times in the base `dist/index.mjs` and once in the head's. </details> <details><summary>Package suites and gates</summary> - `pnpm --filter @objectstack/objectql typecheck` (tsc plus `check:test-typecheck`) and `pnpm --filter @objectstack/objectql test`, under `os-verify-lock`: VERDICT command-exit 0. Test Files 564 passed (564), Tests 11422 passed (11422). - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`: 75 families, all 75 exit 0, recorded as `CMD :: exit N`. `--ran`: 75 derived, 75 run, 0 NOT-MEASURED, 0 UNRUN, at `b03d09ffb5` and again at `3a2c4b4027`. The facade gate is among them (`pnpm check:facade-delegation-only`, 16 named exceptions). - Outside the derived set: - `node scripts/check-facade-delegation-only.mjs --self-test`: exit 0, 37 cases. - `pnpm lint`: exit 0. - `packages/spec` `scripts/liveness/evidence.test.ts`: 48/48, since four names leave `engine.ts`. - `pnpm check:platform-checklist`: exit 0, 1145/1155 symbol anchors resolved, 10 on the named #16898 residual. </details> 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6a01718 commit 13aa6a3

5 files changed

Lines changed: 492 additions & 66 deletions

File tree

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): the `ObjectQL` constructor body moves into the hooks-and-functions facet
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
- The constructor's body moves out of `engine.ts` into `engine-hooks-and-functions.ts` as `HooksAndFunctionsFacet.construct`. The constructor keeps its signature and does two things: it assigns `logger` (a field the compiler requires the constructor itself to assign) from `HooksAndFunctionsFacet.constructLogger(hostContext)`, then calls `construct` with the facet's host. Where the body handed the engine itself on (the package-enable summary refresh, the two env hardening flags), it reads the same engine through the host's `facade` entry.
11+
- No behaviour changes and nothing to do on upgrade: the same statements run in the same order. The built declaration files are unchanged; the source maps move.

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

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@
44
* [#29986 codemod] The
55
* "Class head: units, hooks, functions, body runners, hook dispatch" section of
66
* `ObjectQL`: the bodies of `recordResidualOwner`, `residualOwnerSource`,
7-
* `getStatus`, `registerHook`, `unregisterHooksByPackage`, `registerFunction`,
8-
* `resolveFunction`, `resolveFunctionEntry`, `unregisterFunctionsByPackage`,
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`.
7+
* `constructLogger`, `construct`, `getStatus`, `registerHook`,
8+
* `unregisterHooksByPackage`, `registerFunction`, `resolveFunction`,
9+
* `resolveFunctionEntry`, `unregisterFunctionsByPackage`, `bindHooks`,
10+
* `setDefaultBodyRunner`, `getDefaultBodyRunner`, `setDefaultActionRunner`,
11+
* `getDefaultActionRunner`, `triggerHooks`, `hasHooksFor`,
12+
* `opensAfterAbortUnit`, `dispatchUnscopedMultiWriteHooks` moved out of
13+
* `engine.ts` whole by `scripts/codemod-split-engine.mjs`.
1314
*
1415
* A MOVE, not a redesign. {@link HooksAndFunctionsFacet} is a stateless facet of `ObjectQL`
1516
* (ruling 6624345766): each static method of it is the body of the `ObjectQL`
@@ -37,7 +38,10 @@
3738
import { driverSupportsTransactions } from '@objectstack/spec/data';
3839
import type { HookContext } from '@objectstack/spec/data';
3940
import type { ExecutionContext } from '@objectstack/spec/kernel';
41+
import { STORED_METADATA_TYPE_READ_GATE_OBJECTS, storedMetadataTypeReadGate } from './stored-metadata-type-read-gate.js';
42+
import { createLogger } from '@objectstack/core';
4043
import { hookStoppedRefusal } from './summary-errors.js';
44+
import { startSummaryRefreshOnEnable } from './summary-enable-refresh.js';
4145
import { HookTargetRebindError } from './hook-target-rebind-errors.js';
4246
import { declaredHookCondition } from './hook-wrappers.js';
4347
import { CoreServiceName } from '@objectstack/spec/system';
@@ -61,6 +65,7 @@ import type { ObjectQL } from './engine.js';
6165
export interface HooksAndFunctionsHost {
6266
_defaultActionRunner: ObjectQL['_defaultActionRunner'];
6367
_defaultBodyRunner: ObjectQL['_defaultBodyRunner'];
68+
readonly _registry: ObjectQL['_registry'];
6469
readonly defaultDriver: ObjectQL['defaultDriver'];
6570
readonly drivers: ObjectQL['drivers'];
6671
/** The engine itself: a moved body reads `this.facade` where the method read a bare `this` (#30020). */
@@ -70,6 +75,8 @@ export interface HooksAndFunctionsHost {
7075
readonly hooks: ObjectQL['hooks'];
7176
isClosedUnitHandle: ObjectQL['isClosedUnitHandle'];
7277
readonly logger: ObjectQL['logger'];
78+
readonly manifests: ObjectQL['manifests'];
79+
registerMiddleware: ObjectQL['registerMiddleware'];
7380
readonly residualOwnerSources: ObjectQL['residualOwnerSources'];
7481
readonly txStore: ObjectQL['txStore'];
7582
}
@@ -87,6 +94,46 @@ export class HooksAndFunctionsFacet {
8794
return this.residualOwnerSources.get(ownerId);
8895
}
8996

97+
/** R8 (#30020): the value `ObjectQL`'s constructor assigns to `logger`, a field only a constructor may assign. */
98+
static constructLogger(hostContext: Record<string, any> = {}): ObjectQL['logger'] {
99+
return hostContext.logger || createLogger({ level: 'info', format: 'pretty' });
100+
}
101+
102+
/** The body of `ObjectQL`'s constructor; the constructor there keeps its signature and docblock. */
103+
static construct(this: Pick<HooksAndFunctionsHost, '_registry' | 'facade' | 'logger' | 'manifests' | 'registerMiddleware'>): void {
104+
// Use provided logger or create a new one
105+
// [#29986 codemod] R8: `this.logger` is assigned on the class (`constructLogger`).
106+
// [#30215] `registerApp` writes `manifests`; an uninstall withdraws the
107+
// package through the registry's own verb, on every door, so the entry goes
108+
// with the registry's record. Left behind, ADR-0070's `isWritablePackage`
109+
// (which reads this map first) kept an uninstalled package read-only until
110+
// a restart.
111+
this._registry.onPackageUninstalled((packageId) => { this.manifests.delete(packageId); });
112+
// [#30377] A roll-up a package adds by extension is withheld with it, so
113+
// the child writes made during a disable recompute nothing: an enable
114+
// recomputes it (`summary-enable-refresh.ts`), on every enable door.
115+
this._registry.onPackageEnabled((packageId) => startSummaryRefreshOnEnable(this.facade, this.logger, packageId));
116+
// Pick up production hardening switches from env so deployers can
117+
// enforce strict-body without code changes:
118+
// OBJECTQL_STRICT_HOOKS=1 → unresolved hooks throw at bind time
119+
// OBJECTQL_WARN_LEGACY_HANDLER=1 → log a deprecation per legacy bind
120+
if (process?.env?.OBJECTQL_STRICT_HOOKS === '1') {
121+
(this.facade as any)._strictHookBinding = true;
122+
}
123+
if (process?.env?.OBJECTQL_WARN_LEGACY_HANDLER === '1') {
124+
(this.facade as any)._warnLegacyHandler = true;
125+
}
126+
// [#27884] The stored-row read gate: a caller-context read of a
127+
// stored-metadata table serves a capability-gated type's documents only to
128+
// holders of that capability — the row the `/meta` type gate reads. In the
129+
// constructor, so no engine serves those tables without it. See
130+
// `stored-metadata-type-read-gate.ts`.
131+
for (const object of STORED_METADATA_TYPE_READ_GATE_OBJECTS) {
132+
this.registerMiddleware(storedMetadataTypeReadGate, { object });
133+
}
134+
this.logger.info('ObjectQL Engine Instance Created');
135+
}
136+
90137
/** The body of `ObjectQL#getStatus`; the method's docblock there is its contract. */
91138
static getStatus(): {
92139
name: "data";

‎packages/objectql/src/engine.ts‎

Lines changed: 7 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,6 @@ import { buildDriverOptions, driverReadScope, driverWriteTarget, engineLayer0Ver
124124
import { announceOpenMigrationGates, isOrganizationOwnershipCertified, isValueShapesMigrationVerified, noteAdmittedValueShapeViolation, readFileReferencesFlagRow, readMigrationFlagVerified, supplyTenantKeyCertifiedResolver, wasDatastoreCreatedFromEmpty } from './engine-migration-flags.js';
125125
import { ExecutionContext, type InstalledPackageSource } from '@objectstack/spec/kernel';
126126
// [#27884] The stored-row read gate, registered on the family's tables below.
127-
import { STORED_METADATA_TYPE_READ_GATE_OBJECTS, storedMetadataTypeReadGate } from './stored-metadata-type-read-gate.js';
128127
// Imported from spec directly rather than through `@objectstack/core`'s
129128
// re-export block: that block is labelled backward-compatibility, and this
130129
// contract is new (#5945).
@@ -138,7 +137,6 @@ import {
138137
type EngineTransactionInfo,
139138
type EngineTransactionOptions,
140139
Logger,
141-
createLogger,
142140
type RetryOptions,
143141
recordNotFoundError,
144142
resolveUserAuthzGrants,
@@ -152,7 +150,7 @@ import {
152150
withoutDeletedSelfParents,
153151
type SummaryDescriptor,
154152
} from './summary-aggregate.js';
155-
import { refreshSummariesExtendedBy, startSummaryRefreshOnEnable, summaryRefreshesSettled, type SummaryEnableRefreshReport } from './summary-enable-refresh.js';
153+
import { refreshSummariesExtendedBy, summaryRefreshesSettled, type SummaryEnableRefreshReport } from './summary-enable-refresh.js';
156154
import { ReadonlyFieldRejectedError } from './readonly-strict-errors.js';
157155
import { HookTargetRebindError } from './hook-target-rebind-errors.js';
158156
import {
@@ -707,37 +705,9 @@ export class ObjectQL implements IObjectQLEngine {
707705
private _registry: SchemaRegistry = new SchemaRegistry();
708706

709707
constructor(hostContext: Record<string, any> = {}) {
710-
// Use provided logger or create a new one
711-
this.logger = hostContext.logger || createLogger({ level: 'info', format: 'pretty' });
712-
// [#30215] `registerApp` writes `manifests`; an uninstall withdraws the
713-
// package through the registry's own verb, on every door, so the entry goes
714-
// with the registry's record. Left behind, ADR-0070's `isWritablePackage`
715-
// (which reads this map first) kept an uninstalled package read-only until
716-
// a restart.
717-
this._registry.onPackageUninstalled((packageId) => { this.manifests.delete(packageId); });
718-
// [#30377] A roll-up a package adds by extension is withheld with it, so
719-
// the child writes made during a disable recompute nothing: an enable
720-
// recomputes it (`summary-enable-refresh.ts`), on every enable door.
721-
this._registry.onPackageEnabled((packageId) => startSummaryRefreshOnEnable(this, this.logger, packageId));
722-
// Pick up production hardening switches from env so deployers can
723-
// enforce strict-body without code changes:
724-
// OBJECTQL_STRICT_HOOKS=1 → unresolved hooks throw at bind time
725-
// OBJECTQL_WARN_LEGACY_HANDLER=1 → log a deprecation per legacy bind
726-
if (process?.env?.OBJECTQL_STRICT_HOOKS === '1') {
727-
(this as any)._strictHookBinding = true;
728-
}
729-
if (process?.env?.OBJECTQL_WARN_LEGACY_HANDLER === '1') {
730-
(this as any)._warnLegacyHandler = true;
731-
}
732-
// [#27884] The stored-row read gate: a caller-context read of a
733-
// stored-metadata table serves a capability-gated type's documents only to
734-
// holders of that capability — the row the `/meta` type gate reads. In the
735-
// constructor, so no engine serves those tables without it. See
736-
// `stored-metadata-type-read-gate.ts`.
737-
for (const object of STORED_METADATA_TYPE_READ_GATE_OBJECTS) {
738-
this.registerMiddleware(storedMetadataTypeReadGate, { object });
739-
}
740-
this.logger.info('ObjectQL Engine Instance Created');
708+
// [#29986 codemod] The body moved to `engine-hooks-and-functions.ts` (see `HooksAndFunctionsHost`).
709+
this.logger = HooksAndFunctionsFacet.constructLogger(hostContext);
710+
HooksAndFunctionsFacet.construct.call(hooksAndFunctionsHost.of(this));
741711
}
742712

743713
/**
@@ -9877,6 +9847,7 @@ export class ObjectQL implements IObjectQLEngine {
98779847
set _defaultActionRunner(v) { engine._defaultActionRunner = v; },
98789848
get _defaultBodyRunner() { return engine._defaultBodyRunner; },
98799849
set _defaultBodyRunner(v) { engine._defaultBodyRunner = v; },
9850+
get _registry() { return engine._registry; },
98809851
get defaultDriver() { return engine.defaultDriver; },
98819852
get drivers() { return engine.drivers; },
98829853
facade: engine,
@@ -9885,6 +9856,8 @@ export class ObjectQL implements IObjectQLEngine {
98859856
get hooks() { return engine.hooks; },
98869857
isClosedUnitHandle: (...a) => engine.isClosedUnitHandle(...a),
98879858
get logger() { return engine.logger; },
9859+
get manifests() { return engine.manifests; },
9860+
registerMiddleware: (...a) => engine.registerMiddleware(...a),
98889861
get residualOwnerSources() { return engine.residualOwnerSources; },
98899862
get txStore() { return engine.txStore; },
98909863
}));

0 commit comments

Comments
 (0)