Repository navigation
refactor(objectql): move the master-detail and related-record bodies out of engine.ts into engine-master-detail-related.ts (#25522 cut 14) - #26489
Conversation
…out of engine.ts into engine-master-detail-related.ts Four bodies of the "Referential integrity and master-detail resolution" section move unedited into a new module behind MasterDetailRelatedHost: settleMasterDetailLanding, resolvePreviewParents, resolvePredicateRelated and resolveOneHopRelated. Each method stays on the class with its signature and docblock and hands its body the members it reads. withoutKey and staticReadonlyStripTakes, read only by the moved landing body, move with it. masterIdOf, servedAsStored, undeclaredRelatedFields and staticReadonlyStoredView, shared with bodies that stay, gain export in place and are imported back. Path readers re-keyed: the system-context census row 29b, the federated injected-column reader keys, and core's second-object-read CALLERS row. Claude-Session: https://claude.ai/code/session_01FEkQ5oZ6me4dwmHNpWg6kT Co-authored-by: Claude <noreply@anthropic.com>
…-39lx12 Claude-Session: https://claude.ai/code/session_01FEkQ5oZ6me4dwmHNpWg6kT Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 19 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin c48b0de9994ac153888a0da933ef1bc810840582 && git checkout c48b0de9994ac153888a0da933ef1bc810840582
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d0a7e748a933a7fb549c38c3105c1c49f5935dec 85d27dca7b0789e71c1d8074bd985bbdb0646ee0 && git checkout -B drift-repro d0a7e748a933a7fb549c38c3105c1c49f5935dec && git merge --no-ff 85d27dca7b0789e71c1d8074bd985bbdb0646ee0
node scripts/docs-audit/affected-docs.mjs --json d0a7e748a933a7fb549c38c3105c1c49f5935dec
|
席位核对ACCEPT · The maintainer's standing authorisation for landing reviewed PRs, verbatim: 「按照 pm-dispatch 规则复核过的都授权」.
Generated by Claude Code |
Requested by Leehom · project thread
Part of #25522
Clause-②: no
Metadata-protocol: none
Ruling-ref: 6348891160
维护者速读
engine.ts里「引用完整性与主从解析」一段的 4 个方法体(主从外键落地、写入预检的主记录、校验规则谓词读取的关联记录、一跳关联读取)原样搬进新文件engine-master-detail-related.ts,类上方法签名与注释不变,只把方法体换成转交;只被这些方法体用到的 2 个小工具函数随之搬走,与留下的方法共用的 4 个工具函数原地加export、由新文件导入(不复制)。engine.ts22,423 → 22,085 行。src/engine.ts(23,903 lines, +7,501 in 30 days) along itsObjectQLseams, one move-only slice per PR #25522 按缝拆分engine.ts的第 14 刀。缝表重测后净减行最多的是validate(约 −438),但它要共用的一个工具函数一旦导出,发布的.d.ts就会重新排序(内容相同、字节不同、分块文件名改变),不符合本刀「字节相同」的硬线,所以本刀改取第二名。.d.ts六个文件逐字节不变;新文件与engine.ts互相导入(运行期循环),已用一次性探针在新旧两版构建产物上逐行对照(33 行完全一致),且发布包把两者打成一个文件。回滚 = revert 本 PR。packages/objectql/src/engine-master-detail-related.ts(搬入的方法体、两个随迁工具函数与MasterDetailRelatedHost)、packages/objectql/src/engine.ts里resolveOneHopRelated的转交桩与四处原地export、packages/core/src/security/second-object-read-exposure.pin.test.ts(新文件的一行分类)。pnpm --filter @objectstack/objectql exec vitest run --project local --maxWorkers=2,应见 472 个测试文件全部通过。Cut 14 of the
packages/objectql/src/engine.tssplit (#24586 ruling A, recorded in6270097840). The claim is6432376812, which carries the seat's shared-module-helper ruling for this cut. Four bodies of the "Referential integrity and master-detail resolution" section move unedited into a new module,packages/objectql/src/engine-master-detail-related.ts:settleMasterDetailLanding(the UPDATE payload a master-detail FK repoint actually lands with),resolvePreviewParents(the write preview's master-detail headers),resolvePredicateRelated(the related rows a validation-rule predicate reads) andresolveOneHopRelated(the one-hop reference reads it and formula fields go through). No behaviour changes. #25522 stays open for the later cuts.Branch
claude/project-thread-39lx12(the card thread's harness-pinned branch), cut fromorigin/mainat5507225e3, which contains cut 13's merge (b7f8a2ebdb, #26403), withmainmerged in once at6746140361(clean; no merged commit touchespackages/objectql/**, andengine.tsis the same blob at both bases). Nothing was edited before that merge landed: the move was measured and dry-run on #26403's head while it was in the queue.engine.tsat5507225e3is the same blob (4e64ab23e37a) as at #26403's head, and the five files committed here are byte-identical to that dry run (git hash-object, 5 of 5; the move script regenerates both source files byte for byte).What changes
Why this area
Re-measured on
5507225e3(after cut 13's merge) with cut 11's measure script, unchanged, and a price step under this cut's seat ruling (price3.mjs: a helper a paying body shares with a staying body is exported in place and blocks nothing; one read only by moving bodies moves with them). Both earlier screens still apply (query-options erasure sites, 8 at base; module-helper closure).engine.tsis 22,423 lines, 7,148 of them from the last 30 days (git blame --since=2026-09-27T03:05Z).scripts/pm/git-history.mjs count: 131 commits in 30 days, 36 in 7.class ObjectQLholds 318 members, 255 methods and accessors.Seam table on
5507225e3(41 sections ofclass ObjectQL; U = untyped receivers)txStore…constructorgetStatus…registryregisterHook…triggerHookshasHooksFor…assertBulkPerRowHookBudgetregisterAction…setActionActive_heldFileResolver…registerInsertRecomputeListenerreadableIdsSecurityReported…resolveDefaultPermissionswriteEpoch…hasObjectMiddlewareexecuteWithMiddleware…buildUserbuildDriverOptions…resolveEnginePostureprobeInstallOrganizations…transactionCoversDriverForresolveNowDefault…defaultFieldEntriesapplyAutonumbers…seedAutonumberregistrationConversionWarned…registerDriversupplyFileColumnsMovedResolver…listUnavailableDatasourcesassertWriteAllowed…publishBulkDataEventsetI18nService…objectDisplayLabelassertReferencesResolve…masterDeclaredFieldsinspectDanglingReferences…inspectCascadeDeletesetCryptoProvider…setTenancyPostureProviderencryptSecretFields…resolveInternalFieldgetSchema…judgeFilterLIFECYCLE_DATASOURCE…setDatasourceMappinginit…resolvePrimaryDatasourceobjectHasMediaField…valueShapeStrictForisFileReferencesMigrationVerified…wasDatastoreCreatedFromEmptydestroy…destroyMAX_EXPAND_DEPTH…MAX_BULK_PER_ROW_HOOK_ROWSautonumberCounters…recomputeSummariesexpandRelatedRecords…expandRelatedRecordsresolveFileReferences…readParentDerivedFilesresolveWhereTokens…requireFindOnePredicatefind…staticReadonlyCreateStripvalidate…validatereadUpdatePriorRow…insertManyupdate…updateplanCascadeAtomicity…organizationOwnedRowCountsdelete…deletecount…aggregateexecute…enforceTransactionOriginregisterObject…createThe full per-method screen and price list are in the project file
pm-orders/server/25522-cut14-seam-table.md; the move script ispm-orders/server/evidence/25522/move-cut14.mjs.insertStaticReadonlySubjectalone changes the published.d.tsbytes (below)registerMetadataCollections166)METADATA_ARRAY_KEYS, read only byregisterMetadataCollectionsand so moving with it, gains'queues', 'assignmentRules'in that PRresolveReadableIdsSecuritystays (thisas a value), the two ADR-0139 bodies stayWhy
validateis out. Measured by exporting each of its seven shared helpers on its own on the base and building (dts-helper-export-variant.sh): six leave all six declaration files byte-identical;insertStaticReadonlySubject, whose return type isReturnType<typeof staticReadonlyInsertSubject>, makes the declaration bundler pull therule-validatordeclarations in earlier. The result is the same declarations in another order (sorted lines equal forindex.d.tsandcore.d.ts; the chunk differs only in the order of two import specifier lists), butindex.d.ts,core.d.tsand the shared chunk change bytes and the chunk's file name changes (util-CVzOGxAntoutil-lvlZ9vs9). The seat's ruling for this cut asks for byte-identical, so this cut takes the next area. Thevalidatedry run (move-cut14-validate-dryrun.mjs; 472 files / 8,789 tests green, AST compare SAME) is kept in the project files for whoever decides it; see Acceptance notes.The shape
MasterDetailRelatedHostmembers they read, plus a.call, marked[#25522 cut 14], as cuts 8 and 10 to 13 did. Fields go as getters over areadXclosure (logger,_registry,formulaHopReads); methods go as late-bound closures (find,resolveMasterDetailParents,materializeParentHeader,buildReferentialFieldClear,resolveOneHopRelated,resolveEnginePosture,masterDetailHeadersCallerReads), so a spy or double set on the engine is still the one a call reaches. No moved body assigns a field, so there is no setter (cut 13's setter ruling has nothing to apply to).settleMasterDetailLandingreads no member of the class, so its method calls the function directly with its type parameter (return settleMasterDetailLanding<H>(args);), the shape cuts 3 and 16 of [maintainer] refactor(metadata-protocol): splitsrc/protocol.ts(33,065 lines, +10,169 in 30 days) along itsObjectStackProtocolImplementationseams, one move-only slice per PR #25467 used for a body that reads nothis.withoutKeyandstaticReadonlyStripTakesare read only bysettleMasterDetailLanding, so they moved with it, docblocks included and byte for byte (staticReadonlyStripTakesreadsstaticReadonlyStoredView). Four helpers are read by these bodies AND by bodies that stay:masterIdOf(resolveMasterDetailParent,resolveMasterDetailParents,update),servedAsStored(validate),undeclaredRelatedFields(resolveFormulaRelated) andstaticReadonlyStoredView(update, and the movedstaticReadonlyStripTakes). Each stays declared where it was, byte for byte, and gainsexportin place; the module imports the four from./engine.js. Before that import the module imported only the typeObjectQLfrom./engine.js.index.tsandcore.tsname their exports singly, so nothing new is published (below).engine.tsand the module import each other. The module runs nothing at evaluation (declarations only), andengine.tsimports it right after./federated-object.js, after every module the moved bodies import, so the evaluation order of every other module is unchanged. Measured on the built bundles: the// src/...region order ofindex.mjs,core.mjs,index.jsandcore.jsat head equals the base's with one line added, the new module, betweenfederated-objectandin-memory-aggregation(100 to 101 regions inindex.mjs). The bundles hold both files in one module, so the published build has no cycle. The late-binding probe answers the same on base and head (Tests).referenceTargetOf,EngineQueryOptions(type),isStoredMetadataBodyObject,ExecutionContext(type),postureEnforcesWall,resolveTenantFieldName,carriesOrganization,collectPredicateRelationships,referentialClearBinding,readonlyWhenFkJudgementReadsParent, the two binding types,resolveMasterDetailRelationandisFederatedObject.engine.tsdropspostureEnforcesWall,collectPredicateRelationships,referentialClearBindingandreadonlyWhenFkJudgementReadsParent, whose last readers moved; the last one's own two-line comment moves with its import into the module.Net lines (the seat's rule)
engine.tsgoes from 22,423 to 22,085 lines, net -338 (40 added, 378 removed). The module is 512 lines. With the three path readers below, the diff againstorigin/mainis 949 changed lines over 5 files (+565 / -384).Readers of the moved lines
content/docs/permissions/system-context.mdxrow 29b anchoredpackages/objectql/src/engine.ts#resolveOneHopRelated, the elevation read site that moved; it now anchorsengine-master-detail-related.ts#resolveOneHopRelated.check-system-context-censuspasses at 153 read sites, as at base. NoNON_READ_ANCHORSrow names a moved member.federated-injected-column-readers.test.tskeys its rowsFILE#FUNCTION :: SEAM: the threeresolveOneHopRelatedrows and theresolvePredicateRelatedrow are re-keyed to the new file, dispositions and reasons unchanged.packages/core/src/security/second-object-read-exposure.pin.test.tslists every non-test file that callsreferenceTargetOf(: the new module gets anot-a-served-readrow. Its reason carries the half of theengine.tsrow'salsotext that describedresolvePredicateRelated, which now has one home with the body, plusresolveOneHopRelated, the one-hop loader those reads and the formula fields go through as the caller's ownfind(as the federated test's row for it says). Theengine.tsrow keeps itsexecutesclassification forexpandRelatedRecords, which stays, and the sentence on the engine's privileged callers.objectql/src/engine.ts#NAMEpointer underpackages/spec/**,docs/adr/**,docs/qa/**,content/**orscripts/**names a moved member or helper. No ADR anchor paragraph describes a moved body (theengine.tsentry is the transaction-seam paragraph). No moved body comment cites an ADR decision letter. No moved body holds aquery-options/no-any-erasuresite (8 at base and head), a tenant-audit untyped receiver, a private static read, or a.canReadObject(call.src/protocol.ts(33,065 lines, +10,169 in 30 days) along itsObjectStackProtocolImplementationseams, one move-only slice per PR #25467).check-durability-degradation-log-levelnames two moved bodies in read-seam entries (guards find() in resolvePreviewParents()andin resolveOneHopRelated()). Its--listat head names the same 82 seams with those two now under the new file, and nothing else differs once line numbers are masked; the gate passes.check-adr-anchorsat base5507225e3(clean detached worktree): 91 anchored files, 1,877 decision-letter citations, 59,295 resolving citations across 8,376 files. Atmain6746140361: 1,876 (main's own movement since). At head85d27dca7(thatmainmerged in): 1,876, the same. On the pre-merge pair it was 1,877 and 1,877. The count is unchanged because the moved bodies carry no decision-letter citation.Why
#25522 splits
engine.tsone seam at a time so that a change to one area reads, reviews and conflicts as that area alone. This cut takes the section that pays most under this cut's rulings and that no open PR touches (table above). Nothing about what the engine does changes.Tests
No test is added or changed apart from the re-keyed rows above: the move is proved by identity, not by new behaviour.
Public
.d.tsbyte-identical, with a positive controlBuilt
@objectstack/objectqlat the base and at head, each from a clean tree, twice: on the pre-merge pair (base5507225e3, heade77aac466) and again after mergingmain(base6746140361, head85d27dca7), with the same hashes both times, and hashed every declaration file (dts-cut14.sh). All six are byte-identical:core.d.mtsef178bf0725eccc2f358f082ce13dc5122a6d5a53c19ef630017256f67de03b4core.d.ts1d20303a455a909e4b55fd0802fb0b0bcb59ec51784daf27d5ef4c0659410953index.d.mtsccf900240aa137abe72f82b8abfc872d42c5645467400134c580285f2a3b3090index.d.ts0454d5e70b156d8effbf27b8915587c7cbee241dcd7d698013c0a4c369a02aa5util-CVzOGxAn.d.mts85ef1cbec2b016d5f7afabaa21689c86c6dffc573076fdfdeb8ea0f1a1b1ad53util-CVzOGxAn.d.ts85ef1cbec2b016d5f7afabaa21689c86c6dffc573076fdfdeb8ea0f1a1b1ad53The builds are the moved code, not a stale cache:
resolvePreviewParents.call(hostoccurs 0 times in the basedistand once in the headdist.Positive control.
scripts/pm/ablation-replace.mjs --holddroppedprivatefromprivate async resolvePreviewParents(on the head (anchor count 1 to 0;engine.tsblob3039d4e812dcto0cbfb05d94c8). Rebuilt, every declaration hash changed (core.d.mts051b65f5…,core.d.ts358f0ddf…,index.d.mts43c1a222…,index.d.tsf0ef609c…) and the chunk was renamedutil-m88nBnDJ.d.ts, which now publishesresolvePreviewParents(...)(line 7673). So the comparison sees a one-word change to a moved member's signature.--restoreput the blob back to3039d4e812dc(git diff HEADempty), and a rebuild matched the head build again.AST compare (move-only)
astcmp.mjs(TypeScript compiler API, comments and whitespace ignored, tokens compared) on baseengine.tsagainst headengine.tsplus the module:settleMasterDetailLanding204 tokens / 22 lines,resolvePreviewParents607 / 51,resolvePredicateRelated463 / 70,resolveOneHopRelated1,158 / 184.class ObjectQL: 318 of 318 members kept, in the same order; 314 byte-identical, and the 4 stubs keep docblock, modifiers and signature identical.export(byte-identical after it), the 2 helpers that moved (byte-identical in the module:withoutKey526 characters,staticReadonlyStripTakes1,464) and the class. The head adds one statement, the module import.Late-binding probe (base and head
dist)zz-probe-cut14.mjs(a one-off, not committed) loads the builtObjectQLand drives all four methods with doubles set on the instance for every member the host reads (find,resolveMasterDetailParents,materializeParentHeader,buildReferentialFieldClear,resolveOneHopRelated,masterDetailHeadersCallerReads,_registry,formulaHopReads,logger), then prints results, the call order on the doubles and the log lines. Cases: the repoint and system arms ofsettleMasterDetailLanding;resolvePreviewParentsfor a caller, for system, and with a throwingfind(the warn arm);resolvePredicateRelatedfor a caller, for system, and under the referential-clear binding;resolveOneHopRelatedthrough the predicate path, the caller path, and with aformulaHopReadsdouble. The 33 output lines are identical on base and head, which is the check the seat's ruling asks for on the run-time cycle. The module-order measurement on the bundles is under "The run-time cycle" above.Suites
Under
scripts/pm/os-verify-lock.shon head85d27dca7(again after mergingmain; the pre-merge run one77aac466was green too):^...closure build of@objectstack/objectql, plus@objectstack/rest^...,@objectstack/runtime^...,@objectstack/client...and@objectstack/client-react...: exit 0.typecheckof@objectstack/objectqland@objectstack/core: exit 0 each.@objectstack/objectqlvitest--project local: 472 files, 8,789 tests passed, exit 0.packages/*/vitest.repo-tests.jsonproject: core 6 files / 83 tests, objectql 1 / 5, rest 5 / 280 passed and 18 skipped, runtime 3 / 947, spec 80 / 1,293, types 1 / 11. Exit 0 each.Verification
Head
85d27dca7b0789e71c1d8074bd985bbdb0646ee0. The gate list isnode scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackon this diff at that head (89 commands, no STALE warning), each run with its exit code captured before any pipe, then reconciled with--ran.--ranreconciliation:89 derived, 89 run, 0 NOT-MEASURED, 0 UNRUN. That includescheck:skill-examples(265 prose examples type-check) andcheck:dual-build-cjs-loads(112 entry points across 69 packages load), which refuse without a build and so ran after the client closure build; andcheck-engine-split-ratio --days 90andcheck-plugin-teardown-shape --self-test, which refuse on a shallow clone and so ran aftergit fetch --shallow-since=2026-07-22 origin mainandgit fetch origin 621a487607881c66b2899b7e3477115229a156b4.node scripts/check-adr-anchors.mjs(counts above),node scripts/check-tenant-audit-census.mjs,node scripts/check-adr-symbol-anchors.mjs,node scripts/check-platform-checklist.mjs --changed-from origin/main,node scripts/check-commit-card-trailers.mjs --range origin/main..HEAD,node scripts/check-durability-degradation-log-level.mjs --list, and eslint aspnpm lintruns it (--no-inline-config) onpackages/objectqland the core pin test.e77aac466, beforemainmoved, derived 95 commands: 93 exit 0 and the two build-reading gates refused (exit 3, not measured). The re-run above replaces it.Acceptance notes
origin/mainheld cut 13's merge (b7f8a2ebdb). The branch is cut from5507225e3, which contains it.packages/objectql/**or the three re-keyed files, listed just before opening this: feat!: ownership queues and assignment rules (ADR-0142 phase 2) #26337 and feat(spec,cli)!: retire the ≤16.x retirement kits; the strict unknown-key refusal names the protocol upgrade guide #26138 (validation/rule-validator.test.tsonly, not touched here). feat!: ownership queues and assignment rules (ADR-0142 phase 2) #26337 editsengine.tsin the imports,OperationContext,METADATA_ARRAY_KEYS, afterSECURITY_SERVICE_SOURCE_SLOT, and inreadUpdatePriorRow/insert/update, none of them moved here, and it edits a different row offederated-injected-column-readers.test.ts.git merge-treeof this head with feat!: ownership queues and assignment rules (ADR-0142 phase 2) #26337's current head (290a435afc) exits 0, no conflict. Whichever lands second mergesmain.validateis left for a decision, not taken. It pays the most (-438) but needsinsertStaticReadonlySubjectexported, which reorders the published declarations (Why this area). Two ways a later cut could take it, neither tried here: a ruling that accepts a declaration reorder with sorted-line equality as the identity bar, or a ruling that lets a helper whose return type names another module's symbol move with the body under a different rule. The dry run is in the project files.resolveOneHopRelatedcarriesformulaHopReadsas a getter, so the body reads whatever set the instance holds when it reads it: the probe replaces it on the instance after construction and sees the moved body calladdon the replacement. No body assigns a field, so there is no setter.staticReadonlyStoredView's docblock inengine.tsstill says{@link staticReadonlyStripTakes}, which now lives in the module. Editing the docblock would break byte identity of a staying statement, so it is noted here rather than touched.not-a-served-readrow is a re-key: its reason is the sentence theengine.tsrow already carried forresolvePredicateRelated, plus the existing classification ofresolveOneHopRelatedfrom the federated readers test. No classification is new.Human Minimum
packages/specholds no runtime logic. (Not touched.)MasterDetailRelatedHost,engine-master-detail-related.ts).pnpm lintpasses on the touched files.main, one pull request.skip-changeset; both declaration lines are filled in above.packages/specnot touched.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.skip-changeset: move-only inside@objectstack/objectql, with the published declarations byte-identical and no behaviour change, so there is nothing for a release note to say.🤖 Generated with Claude Code
https://claude.ai/code/session_01FEkQ5oZ6me4dwmHNpWg6kT
Generated by Claude Code