Skip to content

refactor(objectql): move the ReadableIds composition and permission resolution bodies out of engine.ts (#25522 cut 15) - #26645

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-firh6t
Oct 27, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-firh6t

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Leehom · project thread
Part of #25522
Clause-②: no
Metadata-protocol: none
Ruling-ref: 6348891160

维护者速读

  • 改了什么:把 engine.ts 里「ReadableIds 安全与权限/公式解析」一段的 8 个方法体(ReadableIds 读取范围链的组合、启动时 controlled_by_parent 主表的检查、单个父对象读取范围的询问、一次操作共享的权限解析及其三个使用方、公式故障的一次性告警)原样搬进新文件 engine-readable-ids-permission-formula.ts,类上的方法签名与注释不变,只把方法体换成转交;只被这些方法体用到的常量 READABLE_IDS_MAX_CHAIN_DEPTH 随之搬走,与留下的代码共用的 2 个工具函数和 4 个类型原地加 export、由新文件导入(不复制)。engine.ts 22,092 → 21,947 行。
  • 为什么改:[maintainer] refactor(objectql): split src/engine.ts (23,903 lines, +7,501 in 30 days) along its ObjectQL seams, one move-only slice per PR #25522 按缝拆分 engine.ts 的第 15 刀(18.2.0 发布后重启)。缝表重测后,validate(约 −439)仍受 .d.ts 重排的裁决所限;第二名「包、应用、插件与驱动注册」(约 −233)与在途的 feat!: ownership queues and assignment rules (ADR-0142 phase 2) #26337 重叠(同一个 METADATA_ARRAY_KEYS 及其门禁),所以取不重叠的第三名。
  • 风险与回滚:纯搬移,无行为变化;公开 .d.ts 六个文件逐字节不变(含正控);新文件与 engine.ts 互相导入(运行期循环),已用一次性探针在新旧两版构建产物上逐行对照(78 行完全一致)。回滚 = revert 本 PR。
  • 先看:packages/objectql/src/engine-readable-ids-permission-formula.ts(搬入的方法体与 ReadableIdsPermissionFormulaHost)、packages/objectql/src/engine.ts 里 composeReadableIdsChain 的转交桩(泛型、递归经实例方法回入)与原地 export、packages/core/src/security/row-serving-door-exposure.pin.test.ts(一行改键)。
  • 两分钟验证:pnpm --filter @objectstack/objectql exec vitest run --project local --maxWorkers=2,应见全部测试文件通过。
  • 需要你做的:无,席位落地。

Cut 15 of the packages/objectql/src/engine.ts split (#24586 ruling A, recorded in 6270097840), the first after the release pause (6434935278; restart record 6439182543, @objectstack/spec@18.2.0). The claim is 6439254992. Eight bodies of the "Readable-ids security and permission/formula resolution" section move unedited into a new module, packages/objectql/src/engine-readable-ids-permission-formula.ts: composeReadableIdsChain (the chain of composed ReadableIds read scopes), findUnservableControlledByParentMasters (the boot screen over controlled_by_parent details), readableIdsScopeOf (the scope one leaf is composed with), resolveOptionPermissions, permissionResolution, resolveFormulaPermissions and resolveDefaultPermissions (an operation's one effective-permission resolution and its three consumers), and reportFormulaFault. No behaviour changes. #25522 stays open for the later cuts.

Branch claude/project-thread-firh6t (the card thread's harness-pinned branch), cut from origin/main at f9867020c, which contains cut 14's merge (00874b9e09, #26489) and the 18.2.0 release.

What changes

Why this area

Re-measured on f9867020c with cut 11's measure script and cut 14's price step (price3.mjs), both unchanged: 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, none in this section; module-helper closure). engine.ts is 22,092 lines, 7,009 of them from the last 30 days (git blame --since=2026-09-27T10:15Z, clone deepened to 2026-09-22 first). scripts/pm/git-history.mjs count: 130 commits in 30 days, 35 in 7. class ObjectQL holds 318 members, 255 methods and accessors. Since cut 14, one commit touched the file (#26551, +7 lines in "Action registry and activation"); the release added no new section.

Seam table on f9867020c (41 sections of class ObjectQL; U = untyped receivers)
range section first … last member lines 30-day lines methods inferred public U private statics
4659-4884 Instance state and constructor txStore … constructor 226 50 0 (0 public) - 0 -
4885-4907 Status and registry accessor getStatus … registry 23 0 2 (2 public) getStatus 0 -
4908-5251 Hook and function registry registerHook … triggerHooks 344 6 16 (16 public) registerHook, triggerHooks 1 -
5252-5607 Hook dispatch internals hasHooksFor … assertBulkPerRowHookBudget 356 61 7 (0 public) - 0 -
5608-5770 Action registry and activation registerAction … setActionActive 163 24 10 (10 public) - 1 -
5771-6052 Registered resolvers and write-gate probes _heldFileResolver … registerInsertRecomputeListener 282 116 6 (6 public) - 0 -
6053-6806 Readable-ids security and permission/formula resolution readableIdsSecurityReported … resolveDefaultPermissions 754 546 15 (2 public) - 0 -
6807-6913 Authz invalidation and middleware registration writeEpoch … hasObjectMiddleware 107 18 4 (4 public) - 0 -
6914-7091 Execution plumbing (middleware, session, provenance, users) executeWithMiddleware … buildUser 178 45 7 (0 public) - 0 -
7092-7300 Driver options and engine posture buildDriverOptions … resolveEnginePosture 209 150 6 (0 public) - 0 -
7301-7510 Organization probe and system-insert organization probeInstallOrganizations … transactionCoversDriverFor 210 45 4 (0 public) - 0 -
7511-7876 Field defaults resolveNowDefault … defaultFieldEntries 366 4 6 (0 public) - 0 -
7877-8489 Autonumber applyAutonumbers … seedAutonumber 613 80 6 (0 public) - 1 -
8490-9154 Package, app, plugin and driver registration registrationConversionWarned … registerDriver 665 170 6 (3 public) registerApp, registerDriver 0 -
9155-9363 Datasource registry supplyFileColumnsMovedResolver … listUnavailableDatasources 209 1 8 (7 public) - 3 -
9364-9515 Write guard and realtime publish assertWriteAllowed … publishBulkDataEvent 152 18 4 (1 public) - 0 -
9516-9614 Validation-message i18n setI18nService … objectDisplayLabel 99 14 4 (1 public) - 0 -
9615-10323 Referential integrity and master-detail resolution assertReferencesResolve … masterDeclaredFields 709 308 12 (0 public) - 0 -
10324-10439 Dangling-reference and cascade inspection inspectDanglingReferences … inspectCascadeDelete 116 48 2 (2 public) - 0 -
10440-10534 Crypto and tenancy providers setCryptoProvider … setTenancyPostureProvider 95 20 4 (4 public) - 1 -
10535-10954 Secret and internal-field handling encryptSecretFields … resolveInternalField 420 61 9 (3 public) - 0 -
10955-11023 Schema lookup and filter judging getSchema … judgeFilter 69 48 3 (2 public) - 0 -
11024-11289 Datasource routing LIFECYCLE_DATASOURCE … setDatasourceMapping 266 20 8 (3 public) setDatasourceMapping 0 -
11290-11457 Lifecycle init and health init … resolvePrimaryDatasource 168 16 3 (3 public) init 0 -
11458-11531 Media and value-shape presence objectHasMediaField … valueShapeStrictFor 74 13 4 (0 public) - 0 mediaFieldPresence, coveredValueFieldPresence
11532-12121 Migration flags and attestations isFileReferencesMigrationVerified … wasDatastoreCreatedFromEmpty 590 96 17 (8 public) - 0 -
12122-12144 destroy destroy … destroy 23 0 1 (1 public) destroy 0 -
12145-12163 Expand/cascade limits (statics) MAX_EXPAND_DEPTH … MAX_BULK_PER_ROW_HOOK_ROWS 19 0 0 (0 public) - 0 MAX_EXPAND_DEPTH, MAX_CASCADE_DEPTH
12164-12614 Roll-up summaries autonumberCounters … recomputeSummaries 451 0 7 (1 public) - 0 -
12615-12868 Related-record expansion expandRelatedRecords … expandRelatedRecords 254 10 1 (0 public) - 0 -
12869-13066 File-reference hydration resolveFileReferences … readParentDerivedFiles 198 183 5 (0 public) - 0 -
13067-13545 Query lowering resolveWhereTokens … requireFindOnePredicate 479 272 9 (0 public) - 0 -
13546-14102 find / findOne find … staticReadonlyCreateStrip 557 142 4 (2 public) - 0 -
14103-14760 validate validate … validate 658 447 1 (1 public) - 0 -
14761-16107 insert / insertMany readUpdatePriorRow … insertMany 1347 439 3 (2 public) - 1 -
16108-18041 update update … update 1934 275 1 (1 public) - 0 -
18042-19582 Delete cascade and reference walking planCascadeAtomicity … organizationOwnedRowCounts 1541 733 22 (1 public) - 0 storedReferenceIncludes, remainderAfterMemberRemoval, referenceCheckContext, outlivesOrganization, keepsOrganizationReference, organizationCountContext
19583-20152 delete delete … delete 570 47 1 (1 public) - 0 -
20153-20772 count / aggregate count … aggregate 620 192 3 (2 public) - 0 -
20773-21081 execute and transactions execute … enforceTransactionOrigin 309 74 8 (2 public) - 0 -
21082-21374 Compatibility API registerObject … create 293 52 16 (16 public) - 0 -

The per-member screen (movable bodies, what stays and why) and the full price output are in the project files, pm-orders/server/25522-cut15-seam-table.md.

price (net lines) section taken?
-439 validate no: the .d.ts reorder ruling (exporting insertStaticReadonlySubject), unchanged since cut 14
-233 Package, app, plugin and driver registration no: overlaps open #26337, which edits METADATA_ARRAY_KEYS (read only by registerMetadataCollections, so it would move) and its gate row in scripts/check-stack-collection-maps.mjs
-156 priced, -145 landed Readable-ids security and permission/formula resolution yes, without resolveFormulaRelated (below)
-124 Delete cascade and reference walking pays less
-71 or less count / aggregate, Query lowering, the rest pays less

Why resolveFormulaRelated stays. Its return type FormulaRelated is a non-exported type that top-level helpers that stay also use (formulaHopScope, applyFormulaPlan, hydrateWriteFormulas and StoredExpressionServing), so moving the body means exporting it in place. Measured by exporting each shared declaration on its own on the base and building (dts-export-variant-cut15.sh): writeFailureLogMeta, isExpressionDefault, PermissionResolution, FormulaPlanEntry, ReadableIdsSecurity and ReadableIdsChain each leave all six declaration files byte-identical; FormulaRelated, which names RelatedRecordBinding from validation/rule-validator, makes the declaration bundler pull the validator declarations in earlier: the same declarations in another order, with the chunk renamed (util-DOIDsRyc to util-BWosSWLp). That is the same mechanism that keeps validate out, and the claim's byte-identical bar forbids it, so resolveFormulaRelated stays (39 body lines, about -30 of the priced -156). A first full move that included it is how this was found: its build reordered the chunk, and the variant measurement named the one export responsible.

The shape

  • The class keeps all eight methods with name, modifiers, signature, docblock and position. Seven bodies become one host literal of exactly the ReadableIdsPermissionFormulaHost members they read, plus a .call, marked [#25522 cut 15], as cuts 8 and 10 to 14 did. Fields go as getters over a readX closure (_registry, logger, _effectiveObjectPermissionsResolver, formulaFaultReported); methods go as late-bound closures (refuseUncomposableReadableIdsClasses, readableIdsScopeOf, composeReadableIdsChain, uncomposableReadableIdsParent, buildEvalUser, defaultFieldEntries), 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).
  • composeReadableIdsChain is generic and recursive: its stub calls the function instantiated with its type parameter N and .calls it on the host, the spelling [maintainer] refactor(metadata-protocol): split src/protocol.ts (33,065 lines, +10,169 in 30 days) along its ObjectStackProtocolImplementation seams, one move-only slice per PR #25467's generic cuts used, and the host's composeReadableIdsChain closure re-enters through the method, so the recursion still passes the instance (the probe counts both levels).
  • resolveOptionPermissions reads no member of the class, so its method calls the function directly (return resolveOptionPermissions(schema, payloads, resolution);).
  • Helpers and types, under the cut-14 seat ruling. READABLE_IDS_MAX_CHAIN_DEPTH is read only by composeReadableIdsChain, so it moved with it, docblock included and byte for byte. writeFailureLogMeta (read too by reportFindFailure, insert, update, delete) and isExpressionDefault (applyFieldDefaults) stay where they were, byte for byte, and gain export in place; so do the types the moved bodies share with staying code: PermissionResolution, FormulaPlanEntry, ReadableIdsSecurity and ReadableIdsChain (each export alone was measured .d.ts-neutral, above). UnservableControlledByParentMaster was already exported. The module imports the two functions as values and the five types as types from ./engine.js; it is a new module, so before that it imported nothing from ./engine.js. index.ts and core.ts name their exports singly, so nothing new is published (below).
  • The run-time cycle (measured). That value import makes engine.ts and the module import each other. The module runs nothing at evaluation (declarations only), and engine.ts imports it right after ./engine-master-detail-related.js, after every module the moved bodies import, so the evaluation order of every other module is unchanged. Measured on the built bundles (index.mjs, core.mjs, index.js, core.js): with the // src/engine.ts region markers set aside, the // src/... region order at head equals the base's with one line added, the new module, between engine-master-detail-related and in-memory-aggregation. One engine.ts region disappears: the one that held only the external @objectstack/spec/data import of ReadableIds, RLS_DENY_FILTER, markFilterSubtreeProvenance and isControlledByParentMasterField, whose readers all moved; that import now sits in the module's region (an external package already loaded by earlier regions; ESM imports are hoisted). 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).
  • Imports. The module imports ReadableIds, RLS_DENY_FILTER, isControlledByParentMasterField, markFilterSubtreeProvenance, FilterCondition (type), ExecutionContext (type), toEvalPermissions, EvalError and EvalPermissions (types), collectComposableReadableIds, composeReadableIdsLeaves, describeReadableIdsLeaf, readableIdsRefusalError, ComposableReadableIdsSite (type), optionVisibilityReadsPermissions and readsPermissionPredicate. engine.ts drops composeReadableIdsLeaves, the whole ReadableIds / RLS_DENY_FILTER / markFilterSubtreeProvenance / isControlledByParentMasterField import line, and toEvalPermissions, whose last readers moved. It keeps optionVisibilityReadsPermissions and readsPermissionPredicate: no code in engine.ts reads them any more, but the docblocks that stay on resolveOptionPermissions and resolveFormulaPermissions {@link} them, the same use DatasourceUnavailableError already has there at base (TypeScript counts a {@link} as a use under noUnusedLocals), and that import line stays byte-identical.
  • The module header names what moved, what stays and why, and how the helpers were handled.

Net lines (the seat's rule)

engine.ts goes from 22,092 to 21,947 lines, net -145 (59 added, 204 removed). The module is 362 lines. With the one path reader below, the diff against origin/main is 628 changed lines over 3 files (+423 / -205).

Readers of the moved lines

  • packages/core/src/security/row-serving-door-exposure.pin.test.ts lists every non-test file that calls a security service's canReadObject(. engine.ts's only call was in readableIdsScopeOf, so its row moves to the new module, with its not-a-door classification and reason unchanged (the reason already described exactly this ask: "the ReadableIds composition (ADR-0139 D2) …"). Without the re-key the census would read engine.ts as stale and the module as unclassified.
  • No other census, ADR anchor, docs row or test key names a moved member or helper by engine.ts path: objectql/src/engine.ts#NAME pointers under packages/spec/**, docs/**, content/** and scripts/** name none of them; content/docs/permissions/system-context.mdx and its NON_READ_ANCHORS have no row for them (no moved body reads isSystem); federated-injected-column-readers.test.ts keys none of them (its resolveFormulaRelated row stays with that body in engine.ts); no moved body calls referenceTargetOf( (core's second-object-read-exposure.pin.test.ts is unchanged); no moved body holds a query-options/no-any-erasure site (8 at base and head), a tenant-audit untyped receiver or a private static read. Mentions by bare name (plugin-security's READABLE_IDS_MAX_CHAIN_DEPTH comments, plugin.ts's ObjectQL#findUnservableControlledByParentMasters) name the constant and the method, which still exist under those names.
  • No ADR anchor paragraph describes a moved body (the engine.ts entry is the transaction-seam paragraph), so the module needs no anchor entry of its own.
  • Read-seam census (the cut-21 trap of [maintainer] refactor(metadata-protocol): split src/protocol.ts (33,065 lines, +10,169 in 30 days) along its ObjectStackProtocolImplementation seams, one move-only slice per PR #25467). check-durability-degradation-log-level --list at base and head is identical once line numbers are masked: 48 durability seams and 82 read seams, the same entries. No moved body is named in a read-seam entry.
  • Decision-letter count: check-adr-anchors at base f9867020c (clean detached worktree): 91 anchored files, 1,881 decision-letter citations on the anchored surface, 59,819 resolving citations across 8,353 files. At head: 91 anchored files, 1,880, 59,820 across 8,354. The one letter that leaves the anchored surface is [ADR-0139 D6, as amended — the nested master chain] in READABLE_IDS_MAX_CHAIN_DEPTH's docblock, which moved with the constant into the module, which no anchor entry names; the citation still resolves (it is in the 59,820), it is just no longer counted on the anchored surface. The +1 resolving citation and +1 file are the new module (its header names ADR-0139 once). The same one-letter move was disclosed on cuts 8 and 12 of [maintainer] refactor(metadata-protocol): split src/protocol.ts (33,065 lines, +10,169 in 30 days) along its ObjectStackProtocolImplementation seams, one move-only slice per PR #25467; anchoring the module to keep the count would mean writing a new invariant paragraph, which is not a move.

Why

#25522 splits engine.ts one 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 row above: the move is proved by identity, not by new behaviour.

Public .d.ts byte-identical, with a positive control

Built @objectstack/objectql at the base (f9867020c, engine.ts blob 9c44e9da88b5) and at head (engine.ts blob 19a84bece0b0), each from a clean tree (dts-cut15.sh), and hashed every declaration file. All six are byte-identical:

file sha256 (base = head)
core.d.mts e2913d082b71f3342f9013f7e75afad720488366f922beef28bbeee67a8cc81b
core.d.ts 79919dabaf5a0bf7a81be2c09eee1535b789699f8e6363064e40fa8007f26c14
index.d.mts e6368a2e3c25d19f03cfbc471fbdd763daf9ca19de8cf5ce22f4493c8eca5f28
index.d.ts 74cb25f41cf0aa8d94e495967ed228dcaaeeba8ba8fa8c54fab10eb01ddc1b51
util-DOIDsRyc.d.mts 791b203b5e36d557cd0d59f15c317b9d85ef644e5ed730b5f3a6aa801fd37adf
util-DOIDsRyc.d.ts 791b203b5e36d557cd0d59f15c317b9d85ef644e5ed730b5f3a6aa801fd37adf

The builds are the moved code, not a stale cache: composeReadableIdsChain.call(host occurs 0 times in the base dist/index.mjs and once in the head's.

Positive control. scripts/ablation-replace.mjs dropped private from private async readableIdsScopeOf( on the head (anchor count 1 to 0; engine.ts blob 19a84bece0b0 to c8dcf0a369f7) and rebuilt: every declaration hash changed (core.d.mts 7f089f7719d3…, core.d.ts ef7645cf34d2…, index.d.mts 6927c24b7c73…, index.d.ts 3d457f36059b…) and the chunk was renamed util-BFa4erHR.d.ts, which now publishes readableIdsScopeOf(object: string, operation: string, parent: string, svc: ReadableIdsSecurity, …) (line 5646). So the comparison sees a one-word change to a moved member's signature. The restore leg put the blob back to 19a84bece0b0 (git diff HEAD empty), and the head was rebuilt before every later step.

AST compare (move-only)

astcmp.mjs (TypeScript compiler API, comments and whitespace ignored, tokens compared) on base engine.ts against head engine.ts plus the module:

  • Moved bodies, all eight SAME (parameters, return type, type parameters, async, body tokens, and every body line equal after one level of indent): composeReadableIdsChain 380 tokens / 46 lines, findUnservableControlledByParentMasters 201 / 12, readableIdsScopeOf 253 / 29, resolveOptionPermissions 149 / 19, permissionResolution 80 / 11, resolveFormulaPermissions 178 / 25, reportFormulaFault 101 / 14, resolveDefaultPermissions 223 / 21.
  • class ObjectQL: 318 of 318 members kept, in the same order; 310 byte-identical, and the 8 stubs keep docblock, modifiers and signature identical.
  • Top level: 217 of 228 base statements byte-identical. The other 11 (exportcmp.mjs): the 3 import edits above; 6 declarations that gained export and are otherwise byte-identical with their leading docblocks (writeFailureLogMeta, isExpressionDefault, PermissionResolution, FormulaPlanEntry, ReadableIdsSecurity, ReadableIdsChain); READABLE_IDS_MAX_CHAIN_DEPTH, byte-identical in the module (1,137 characters with its docblock); and the class. The head adds one statement, the module import.
Late-binding probe (base and head dist)

zz-probe-cut15.mjs (a one-off, not committed) loads the built ObjectQL and drives all eight methods with doubles set on the instance for every member the hosts read (_registry, logger, _effectiveObjectPermissionsResolver, formulaFaultReported, refuseUncomposableReadableIdsClasses, readableIdsScopeOf, composeReadableIdsChain, uncomposableReadableIdsParent, buildEvalUser, defaultFieldEntries, plus hasObjectMiddleware and resolveDatasourceBinding under them), then prints results, the call order on the doubles and the log lines. Cases: the ReadableIds chain through composeReadableIdsAtBoundary (a nested two-level chain, which re-enters composeReadableIdsChain through the instance; a cycle, refused; a denied parent; no restriction; getReadFilter throwing; the admission throwing; the explain fallback; the deny sentinel; a non-filter answer; a readableIdsScopeOf double replaced after construction); findUnservableControlledByParentMasters over both D5 classes; permissionResolution with no user, with a user (asked once for two get()s) and with no resolver; resolveOptionPermissions, resolveFormulaPermissions and resolveDefaultPermissions with no resolution, a resolution and a throwing one; reportFormulaFault twice on one pair (one line), and with a formulaFaultReported double set after construction. The 78 output lines are identical on base and head (the engine's own construction log lines, which carry timestamps, set aside), 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.sh on head 4d72947a5 (after merging main):

  • Closure build of @objectstack/objectql, @objectstack/rest, @objectstack/runtime, @objectstack/client, @objectstack/client-react and @objectstack/core with their dependencies: exit 0.
  • typecheck of @objectstack/objectql and @objectstack/core: exit 0 each.
  • @objectstack/objectql vitest --project local: 472 files, 8,795 tests passed, exit 0.
  • Every packages/*/vitest.repo-tests.json project: core 6 files / 83 tests (it holds the re-keyed census), objectql 1 / 5, rest 5 / 280 passed and 18 skipped, runtime 3 / 947, spec 80 / 1,293, types 1 / 11. Exit 0 each.
  • The .d.ts pair and the late-binding probe were run on both bases: f9867020c before the merge and ce9bc104e after it, with the same six hashes and the same 78 probe lines both times (engine.ts is the same blob at both bases).

Verification

Head 4d72947a56925b533ac8ad37659bcf47c615ea75. The gate list is node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on this diff at that head (56 commands, merge base ce9bc104e, no STALE warning), each run with its exit code captured before any pipe, then reconciled with --ran.

  • Derived gates: 56 of 56 exit 0. --ran reconciliation: 56 derived famil(ies) accounted for — 56 run, 0 NOT-MEASURED. check:dual-build-cjs-loads refused on its first pass (exit 3, a package's dist/ not yet built) and was re-run once the build closure was complete: 112 entry points across 69 packages load, exit 0. check-engine-split-ratio and check-plugin-teardown-shape ran after git fetch --shallow-since=2026-07-22 origin main and git fetch origin 621a487607881c66b2899b7e3477115229a156b4.
  • Hand gates, exit 0 each: 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 as pnpm lint runs it (--no-inline-config) on packages/objectql and packages/core.

Acceptance notes

  • Serial order and restart. Nothing was edited before the claim 6439254992 named this branch. The branch is cut from f9867020c, which holds cut 14's merge (00874b9e09) and the 18.2.0 release; main (ce9bc104e) was merged in once before opening (clean; no merged commit touches engine.ts, which is the same blob at both bases).
  • Overlap. Open PRs touching packages/objectql/** or the re-keyed core pin, listed at start and again just before opening this: feat!: ownership queues and assignment rules (ADR-0142 phase 2) #26337 (engine.ts and other objectql files), feat(spec)!: retirement kits whose conversion retired at or below 16.x age out; the strict unknown-key refusal names the protocol upgrade guide #26599 (validation/rule-validator.test.ts only) and chore: version packages #26517 (the release PR: CHANGELOG.md, package.json). feat!: ownership queues and assignment rules (ADR-0142 phase 2) #26337 edits engine.ts in the imports, OperationContext, METADATA_ARRAY_KEYS, after SECURITY_SERVICE_SOURCE_SLOT, and in readUpdatePriorRow / insert / update; none of those is moved here, and it does not touch the core pin. git merge-tree of this head with feat!: ownership queues and assignment rules (ADR-0142 phase 2) #26337's current head (cbd27d9a6c) exits 0, no conflict. Whichever lands second merges main.
  • resolveFormulaRelated stays, measured. Moving it needs FormulaRelated exported, and that export alone reorders the published declaration chunk (Why this area). It is the same question the restart carries for validate (may a move-only cut reorder declarations inside a published .d.ts chunk when the declared surface is unchanged?), so a ruling on that question would free both: validate (-439) and resolveFormulaRelated (about -30).
  • Package, app, plugin and driver registration is next once feat!: ownership queues and assignment rules (ADR-0142 phase 2) #26337 lands (about -233). It was not taken here only because feat!: ownership queues and assignment rules (ADR-0142 phase 2) #26337 edits METADATA_ARRAY_KEYS and its check-stack-collection-maps.mjs row; that row's file: and the constant would move with registerMetadataCollections.
  • Receivers. The host literals carry _registry, logger, _effectiveObjectPermissionsResolver and formulaFaultReported as getters, so each body reads whatever the instance holds when it reads it: the probe replaces formulaFaultReported and readableIdsScopeOf on the instance after construction and sees the moved bodies use the replacements. No body assigns a field, so there is no setter.
  • Left as is. Two docblocks that stay in engine.ts (composeReadableIdsAtBoundary's and composeReadableIdsChain's) still say {@link READABLE_IDS_MAX_CHAIN_DEPTH}, which now lives in the module, and optionVisibilityReadsPermissions / readsPermissionPredicate stay imported in engine.ts for the {@link}s on resolveOptionPermissions and resolveFormulaPermissions (above). Editing a staying docblock would break its byte identity, so these are noted here rather than touched, as cut 14 noted staticReadonlyStripTakes.
  • Decision-letter count 1,881 → 1,880, explained under "Readers of the moved lines": one [ADR-0139 D6 …] citation moved with its constant off the anchored surface; nothing was rewritten or dropped.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (Not touched.)
  • 2. Every name I added uses the casing for its kind (ReadableIdsPermissionFormulaHost, engine-readable-ids-permission-formula.ts).
  • 3. No formatter ran; pnpm lint passes on the touched packages.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. No publish-visible change, so skip-changeset; both declaration lines are filled in above.
  • 6. packages/spec not touched.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English, apart from the maintainer quick-read block the project asks for.
  • The Before Submitting list in CONTRIBUTING.md is done.

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_01FEkQ5nM7fbhbE84nNy6VkE

…esolution bodies out of engine.ts

Eight bodies of the "Readable-ids security and permission/formula
resolution" section move unedited into
engine-readable-ids-permission-formula.ts behind
ReadableIdsPermissionFormulaHost; the class keeps every method, signature and
docblock. READABLE_IDS_MAX_CHAIN_DEPTH moves with its one reader;
writeFailureLogMeta, isExpressionDefault and four shared types gain export in
place and are imported back. core's row-serving census row for the
ReadableIds admission ask moves with its call.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5nM7fbhbE84nNy6VkE
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/objectql, touching 33 documentable anchor(s).

14 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/error-catalog.mdx (via controlled_by_parent (literal, a string literal in findUnservableControlledByParentMasters))
  • content/docs/data-modeling/objects.mdx (via controlled_by_parent (literal, a string literal in findUnservableControlledByParentMasters))
  • content/docs/deployment/cli.mdx (via controlled_by_parent (literal, a string literal in findUnservableControlledByParentMasters))
  • content/docs/getting-started/common-patterns.mdx (via controlled_by_parent (literal, a string literal in findUnservableControlledByParentMasters))
  • content/docs/kernel/runtime-services/sharing-service.mdx (via controlled_by_parent (literal, a string literal in findUnservableControlledByParentMasters))
  • content/docs/permissions/attachments-access.mdx (via controlled_by_parent (literal, a string literal in findUnservableControlledByParentMasters))
  • content/docs/permissions/authorization.mdx (via controlled_by_parent (literal, a string literal in findUnservableControlledByParentMasters))
  • content/docs/permissions/index.mdx (via controlled_by_parent (literal, a string literal in findUnservableControlledByParentMasters))
  • content/docs/permissions/permissions-matrix.mdx (via controlled_by_parent (literal, a string literal in findUnservableControlledByParentMasters))
  • content/docs/permissions/rls.mdx (via controlled_by_parent (literal, a string literal in findUnservableControlledByParentMasters))
  • content/docs/permissions/sharing-rules.mdx (via controlled_by_parent (literal, a string literal in findUnservableControlledByParentMasters), getReadFilter (literal, a string literal in ReadableIdsSecurity))
  • content/docs/permissions/system-context.mdx (via canReadObject (literal, a string literal in ReadableIdsSecurity), getReadFilter (literal, a string literal in ReadableIdsSecurity))
  • content/docs/protocol/kernel/error-handling.mdx (via controlled_by_parent (literal, a string literal in findUnservableControlledByParentMasters))
  • content/docs/protocol/objectql/security.mdx (via controlled_by_parent (literal, a string literal in findUnservableControlledByParentMasters))

⛔ 11 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/implementation-status.mdx (via controlled_by_parent (literal, a string literal in findUnservableControlledByParentMasters))
  • content/docs/releases/v15.mdx (via controlled_by_parent (literal, a string literal in findUnservableControlledByParentMasters), getReadFilter (literal, a string literal in ReadableIdsSecurity))
  • content/docs/releases/v17/17-1.mdx (via controlled_by_parent (literal, a string literal in findUnservableControlledByParentMasters))
  • content/docs/releases/v17/17-2.mdx (via controlled_by_parent (literal, a string literal in findUnservableControlledByParentMasters))
  • content/docs/releases/v17/17-3.mdx (via controlled_by_parent (literal, a string literal in findUnservableControlledByParentMasters))
  • content/docs/releases/v17/17-5.mdx (via canReadObject (literal, a string literal in ReadableIdsSecurity))
  • content/docs/releases/v17/17-6.mdx (via canReadObject (literal, a string literal in ReadableIdsSecurity), getReadFilter (literal, a string literal in ReadableIdsSecurity))
  • content/docs/releases/v17/index.mdx (via controlled_by_parent (literal, a string literal in findUnservableControlledByParentMasters))
  • content/docs/releases/v18/18-0.mdx (via controlled_by_parent (literal, a string literal in findUnservableControlledByParentMasters))
  • content/docs/releases/v18/18-1.mdx (via controlled_by_parent (literal, a string literal in findUnservableControlledByParentMasters))
  • content/docs/releases/v18/18-2.mdx (via controlled_by_parent (literal, a string literal in findUnservableControlledByParentMasters))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 55 of 207 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 19 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 03e898c1d3e24575310f1d4abd3798696bfe0cc0 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 5c37956545ca84214e45cd00fb3a8bdfebb3c158 — the merge of head 4d72947a56925b533ac8ad37659bcf47c615ea75 into base 03e898c1d3e24575310f1d4abd3798696bfe0cc0, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 5c37956545ca84214e45cd00fb3a8bdfebb3c158 && git checkout 5c37956545ca84214e45cd00fb3a8bdfebb3c158
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 03e898c1d3e24575310f1d4abd3798696bfe0cc0 4d72947a56925b533ac8ad37659bcf47c615ea75 && git checkout -B drift-repro 03e898c1d3e24575310f1d4abd3798696bfe0cc0 && git merge --no-ff 4d72947a56925b533ac8ad37659bcf47c615ea75

node scripts/docs-audit/affected-docs.mjs --json 03e898c1d3e24575310f1d4abd3798696bfe0cc0

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 03e898c1d3e24575310f1d4abd3798696bfe0cc0 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · domain:server seat 2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo · 2026-10-27T11:51Z · head 4d72947a56 · #25522 cut 15

The maintainer's standing authorisation for landing reviewed PRs, verbatim: 「按照 pm-dispatch 规则复核过的都授权」.

  1. Shape: a draft against main. The body opens with Part of #25522 (not Fixes). Clause-②: no and Metadata-protocol: none are line-initial. skip-changeset applies because this is a move. check-governed-merges --pr 26645 reads NOT governed, 628 changed lines. None of the four contract faces is touched, so no at-tier review is owed. This is the first cut after the 18.2.0 restart (6439182543), under claim 6439254992, which names this PR's branch.

  2. Premise: the seam table re-measured after cut 14 is in the body and in pm-orders/server/25522-cut15-seam-table.md.

    • "Package, app, plugin and driver registration" (−233) pays most but overlaps feat!: ownership queues and assignment rules (ADR-0142 phase 2) #26337 (METADATA_ARRAY_KEYS and its check-stack-collection-maps row), so it was skipped as the order says.
    • "Readable-ids security and permission/formula resolution" lands at −145 (22,092 → 21,947). Eight bodies move into engine-readable-ids-permission-formula.ts behind ReadableIdsPermissionFormulaHost.
    • The ADR-0139 bodies named by path (composeReadableIdsAtBoundary, refuseUncomposableReadableIdsClasses) stay in engine.ts. The module reaches refuseUncomposableReadableIdsClasses through the host.
    • resolveFormulaRelated stays, because exporting FormulaRelated alone reorders the published .d.ts chunk (measured per export).
  3. Move check (the seat's own): every line removed from engine.ts was compared with the new module after stripping indentation. All 203 non-blank lines are present there except nine:

    • Three import lines lose the names only the moved bodies read; the module imports those names itself.
    • Six declarations shared with staying code gain export in place under cut 14's shared-helper ruling: writeFailureLogMeta, PermissionResolution, isExpressionDefault, FormulaPlanEntry, ReadableIdsSecurity and ReadableIdsChain.

    Everything added to engine.ts is the delegate methods with read-only host getters (_registry, logger, _effectiveObjectPermissionsResolver, formulaFaultReported). No moved body assigns a field: reportFormulaFault only calls .add on the existing Set, so no setter is owed. The dev reports:

    • the six published .d.ts files byte-identical, with a positive control;
    • the AST comparison at 8 of 8 SAME;
    • the late-binding probe reading the same 78 lines on base and head.
  4. Path readers: core's row-serving-door-exposure.pin.test.ts re-keys engine.ts's one canReadObject( site to the new module, with its not-a-door reason unchanged. The body records the sweep for every other census, docs row and test key, and none of them names a moved body.

  5. ADR anchors: check:adr-anchors reads 1,881 decision-letter citations on the anchored surface at base and 1,880 at head. The one [ADR-0139 D6 …] citation moved with READABLE_IDS_MAX_CHAIN_DEPTH's docblock into a module no anchor entry names. It still resolves (59,819 → 59,820 resolving), and nothing was rewritten or dropped. This is the same disclosed one-letter move as [maintainer] refactor(metadata-protocol): split src/protocol.ts (33,065 lines, +10,169 in 30 days) along its ObjectStackProtocolImplementation seams, one move-only slice per PR #25467 cuts 8 and 12; anchoring the module would mean writing a new invariant paragraph, which is not a move. Accepted as disclosed.

  6. Gates: at this stamp every check-run on the head reads success or skipped; none reads failed or in progress. Locally the dev reports:

    • 56 of 56 derived gates exit 0;
    • objectql at 472 files and 8,795 tests;
    • all six --project repo census projects green.

    main ce9bc104e is merged in.

  7. Out of scope and open:

    • The carried .d.ts question (may a move-only cut reorder declarations inside a published chunk when the declared surface is unchanged?) is now ruled: 6441032442 (letter B, under the delegation that produced 6348891160), with the statement-level normaliser bar. It does not change this cut, whose six .d.ts files are byte-identical. It frees validate (−438) as the next paying cut, and resolveFormulaRelated can join a later one.
    • Two staying docblocks still {@link READABLE_IDS_MAX_CHAIN_DEPTH}. They are kept for byte identity, as cut 14 kept staticReadonlyStripTakes, and fold into the next cut.
    • Package registration (−233) follows once feat!: ownership queues and assignment rules (ADR-0142 phase 2) #26337 lands.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 27, 2026 11:52
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 27, 2026 11:52
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 27, 2026
Merged via the queue into main with commit 378c940 Oct 27, 2026
51 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-firh6t branch October 27, 2026 12:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/l skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants