Repository navigation
feat(core, metadata-core, objectql): the package-scoped resolution rule as a lean core, engine functions by package - #31733
Conversation
… holder set, and keep engine functions per package The package-scoped resolver's D3 steps now run over a holder source the caller supplies (resolvePackageScopedHolders); resolvePackageScopedName is that call over the registry's holders. The engine keeps every function registration by its package beside the bare name map (functionHoldersOf). No reader is wired. Claude-Session: https://claude.ai/code/session_01ADHkmQ5eMDSpkq6rYGLbBU Co-authored-by: Claude <noreply@anthropic.com>
…rs and the red lines; export the entry Claude-Session: https://claude.ai/code/session_01ADHkmQ5eMDSpkq6rYGLbBU Co-authored-by: Claude <noreply@anthropic.com>
…e and its closure walk where lean readers reach them The D3 rule moves into @objectstack/core as a pure function over a caller-supplied holder set and closure, with its refusal vocabulary. The closure walk (resolvePackageClosure, the App-pair widening, the declared closure and the resolution closure with sys) moves into @objectstack/metadata-core. lint, metadata-protocol and objectql re-export every moved name unchanged; objectql keeps two adapters, the registry's and the engine functions'. Claude-Session: https://claude.ai/code/session_01ADHkmQ5eMDSpkq6rYGLbBU Co-authored-by: Claude <noreply@anthropic.com>
…st, and type the function-holder fixtures Claude-Session: https://claude.ai/code/session_01ADHkmQ5eMDSpkq6rYGLbBU Co-authored-by: Claude <noreply@anthropic.com>
…are read A dependent package's email template locale its base does not ship stays under the dependent (#28802), so the registry no longer counts it as a second holder of the base's name; a member read naming the dependent still reaches it. A dependent holding a member its base holds, or a package outside the holder's declaring closure, is still a holder. Claude-Session: https://claude.ai/code/session_01ADHkmQ5eMDSpkq6rYGLbBU Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADHkmQ5eMDSpkq6rYGLbBU Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 5 package(s): 39 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 15 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 55 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 d0b9ba4dcc327b1a654b165a60d8bed63d35b833 && git checkout d0b9ba4dcc327b1a654b165a60d8bed63d35b833
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c2bb879fe71c16bd1d0b42708109efb7f6543d5d 099edf551e3fc64e5abca1166fddf2f3ec467b5e && git checkout -B drift-repro c2bb879fe71c16bd1d0b42708109efb7f6543d5d && git merge --no-ff 099edf551e3fc64e5abca1166fddf2f3ec467b5e
node scripts/docs-audit/affected-docs.mjs --json c2bb879fe71c16bd1d0b42708109efb7f6543d5d
|
|
epic:#31527#1 · os-steve · session_01ADHkmP7o3uDTti21wLTfPf Epic acceptance review: R2 #31716, head e4cadc7 · 2026-11-09T00:38Z Verdict: CHANGES REQUIRED. There are two items, both of the "do not narrow an answer that works today" kind (red line 3 (b)). The move itself is accepted. Accepted:
Item 1: a package caller's bare read of its own added member must not become not-found.
Item 2: an owner-less function registration is the platform's, not the environment's item.
Open questions:
This surface is not governed. Once both items are in and CI is green, I accept and land it, then release E2, E3, E6, E8 and E5's email half. |
…nd an owner-less function is the platform's A dependent's bare read of its own added member answers its own item at step 1 again: the addition only stops counting toward other callers' ambiguity. An owner-less engine function is code a plugin or the host registers, so it is the platform's item, in every caller's reach like a sys package: a package caller holding none counts it beside its closure's holders, and two or more refuse naming each. Claude-Session: https://claude.ai/code/session_01ADHkmQ5eMDSpkq6rYGLbBU Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADHkmQ5eMDSpkq6rYGLbBU Co-authored-by: Claude <noreply@anthropic.com>
…t package id is a caller with no package ObjectQL#registerObject stamps '__runtime__' when its caller gives no package id. That names no installed package, so the rule now reads such a caller as one with no package, beside the sys_metadata sentinel, instead of refusing it every environment item. The compatibility API takes its default from the one exported spelling. Claude-Session: https://claude.ai/code/session_01ADHkmQ5eMDSpkq6rYGLbBU Co-authored-by: Claude <noreply@anthropic.com>
…s reach the one closure walk A multi-package stack's residual owner has no package record, so the walk read its declared closure as empty and it could reach no dependency whatever it declared. The registry now keeps what the stack's manifest declares beside the package records, and the walk reads it for an id getPackage cannot produce, so every reader sees one closure for it. Claude-Session: https://claude.ai/code/session_01ADHkmQ5eMDSpkq6rYGLbBU Co-authored-by: Claude <noreply@anthropic.com>
|
epic:#31527#1 · os-steve · session_01ADHkmP7o3uDTti21wLTfPf Epic acceptance re-review: R2 #31716, head 15d685c · 2026-11-09T01:59Z All four rulings are in and pinned red without the fix: the own holding at step 1, the platform holder, the 1. A caller with no package: the platform must not outrank a package holder (red line 2).
2. The save-door effect for residual owners needs its count.
Accepted this round:
Send me the head with both changes and I will accept it and land it myself (the epic seat's own card; not a governed surface). |
… more holder After the environment's own item, the platform's item stands beside the package holders: the only one answers, and beside any package holder the name refuses ambiguous-in-environment, naming each, rather than the platform silently answering. Claude-Session: https://claude.ai/code/session_01ADHkmQ5eMDSpkq6rYGLbBU Co-authored-by: Claude <noreply@anthropic.com>
|
epic:#31527#1 · os-steve · session_01ADHkmP7o3uDTti21wLTfPf ACCEPT: R2 #31716, head c1c1536 · 2026-11-09T02:32Z Both re-review items from 6730444942 are in:
Against the three red lines:
It is not a governed surface. Readying it and arming merge now. After it lands, E2, E3, E6, E8 and E5's email half wire against this resolver. |
|
epic:#31527#1 · os-steve · session_01ADHkmP7o3uDTti21wLTfPf Landing · 2026-11-09T02:33Z Epic ACCEPT is recorded above, at head c1c1536. The surface is not governed ( |
…-lr6cin # Conflicts: # packages/objectql/src/bare-name-one-holder.ts
|
epic:#31527#1 · os-steve · session_01ADHkmP7o3uDTti21wLTfPf Re-queue · 2026-11-09T03:24Z The queue dequeued this PR for MERGE_CONFLICT against #31661 (1f30be1). Head 099edf5 merges Re-run at this head: core 3174, objectql 11756, The acceptance stands. Re-arming auto-merge now. |
Requested by Steve · project thread
Fixes #31716
Part of #31527
Clause-②: no
Metadata-protocol: none
Narrowing: the save door's write scope for a residual owner (ADR-0130 D4, an id with no package record) is now its declared closure, where before it narrowed nothing. First-party residual owners whose writes it now refuses: 0. Measured over examples app-crm, app-multi-package, app-showcase and app-todo (config and dist), every
packages/**/objectstack.config.ts, and the dogfood stacks (showcase boot; the composed stacks in multi-package-artifact, picklist-shared-across-objects, security-catalog-cold-boot-environment-holder and install-local-connector-package-ref). Each multi-package stack keeps a member's manifest, so its owner has a package record. HotCRM is outside this session's repository access and was not measured.维护者速读
@objectstack/core(纯函数,持有者集合和依赖闭包由调用方传入),闭包只在@objectstack/metadata-core走一遍;objectql 的resolvePackageScopedName变成这条规则的适配器,另加引擎函数的适配器,并修了四处:E5 发现的邮件模板语言追加被误算成第二个持有者、无包函数改按平台持有、__runtime__调用方按无包处理、残余所有者(无包记录的栈)声明的依赖进入同一个闭包。packages/core/src/package-scoped-resolution.ts(规则),packages/metadata-core/src/package-closure.ts(唯一的闭包),packages/objectql/src/registry.ts的packageScopedHoldersOf(追加不算持有者)。pnpm --filter @objectstack/core exec vitest run src/package-scoped-resolution.test.ts和pnpm --filter @objectstack/objectql exec vitest run src/package-scoped-resolver.test.ts,R 的 33 个原测试一字未改。What changes
Epic #31527 child R2. The ADR-0150 D3 rule moves where every reader reaches it, the closure is walked in one place, and the engine's functions resolve in their referrer's package. ⛔ No reader is wired. ⛔ The closure is not widened (N1 waits on #31678).
@objectstack/core—package-scoped-resolution.ts(new):resolvePackageScopedHolders(query, source, closureOf), D3's three steps over a holder set and a closure the caller supplies, plus the refusal vocabulary (PACKAGE_SCOPED_TYPES,ENGINE_FUNCTION_TYPE,PLATFORM_REQUESTED_BARE_NAMES_BY_TYPE,OVERRIDABLE_BASELINE_PACKAGES,BARE_NAME_ONE_HOLDER_TYPES, the holder, refusal and resolution types).packageScopedHolderSource(pairs)builds a source from(package, item)pairs; a holder named twice is aTypeError.@objectstack/metadata-core—package-closure.ts(new): the one walk.resolvePackageClosure(from lint),widenPairClosureToAppMembersandpackageDependencyClosure(from metadata-protocol),packageResolutionClosureandKERNEL_NAMESPACE(from objectql). It depends only on@objectstack/spec, is already in the lean entry's admitted set, and is already a dependency of every reader, so no package edge is added.@objectstack/lint,@objectstack/metadata-protocol— re-export the moved names under the same names. The save door'sresolveWritePackageScopeis unchanged.@objectstack/objectqlresolvePackageScopedNamekeeps its signature and answers. It is now the rule's call with the registry's holders andpackageResolutionClosure.resolvePackageScopedFunction(engine, { name, callerPackageId })(new) is the rule overObjectQL#functionHoldersOf(name)(new). The engine keeps one entry per registering package (ownerapp:PACKAGE_IDread as the package id). An owner-less registration is the platform's item (epic seat ruling 6729484996): code a plugin or the host registers, in every caller's reach like asyspackage (D2). A package caller holding none counts it beside its closure's holders, so one answers (stepplatform, holder kindplatform) and two or more refuseambiguous-in-closure, naming each. A caller with no package gets the environment's own item first, then counts the platform as one more holder beside the packages: the only one answers, and two or more refuseambiguous-in-environment, naming each (re-review 6730444942). Core carries it as the optionalPackageScopedHolderSource#platformItem.unregisterFunctionsByPackageremoves the package's holder entry too, and its count is unchanged.resolveFunction/resolveFunctionEntryare untouched.__runtime__caller (ruled from E6):RUNTIME_COMPATIBILITY_PACKAGE_ID('__runtime__', exported from@objectstack/core) is the package idObjectQL#registerObjectstamps when its caller gives none. The engine and the compatibility API now take it from that one spelling. The rule reads a caller carrying it as a caller with no package, next to thesys_metadatasentinel, so it no longer loses environment items to D3 (a). How an item stamped with it counts as a holder is unchanged.recordResidualOwnerDependencies/residualOwnerDependenciesOf), the manifest service records them when it registers a stack's residual (plugin.ts, next torecordResidualOwner), andpackageDependencyClosurereads them through the optionalPackageClosureRegistry#residualOwnerDependenciesOfwhenevergetPackagehas no row. No package record appears andgetAllPackagesis unchanged. Save door effect, declared: a write stamped with a residual owner's id is now judged against that owner's declared closure. Before, it narrowed nothing, because the walk answeredundefined.<base>.custompair is read by name but never listed or sent, so a send in that locale renders the base's (#28779 follow-up) #28802):SchemaRegistry#packageScopedHoldersOfno longer counts a bundle addition as a holder. A package whose members of a bundled type's name are all members another holder in its declared dependency closure does not hold adds to that holder's bundle (a dependent's locale its base does not ship). So it is no second holder of the base's name for a bare read. The dependent's own bare read keeps its own holding (packageScopedHoldersOf(type, name, callerPackageId)), so step 1 answers its own item as on main (review item 1).getPackageScopedItemnaming the dependent still reads the addition. A dependent holding a member its base holds, and a package that does not declare the holder, stay holders, and the bare read refuses them. Q1 on webhooks, connectors, email templates on (package, name) (ADR-0150 child E5) #31677 (a clash sealed before phase 1) is not touched.Tests
packages/core/src/package-scoped-resolution.test.ts(new, 21): every arm on the rule alone, the closure asked only when needed, red lines 1 and 2, the exceptions, the holder source, and the platform's item.packages/objectql/src/package-scoped-resolver.test.ts: R's 33 tests unchanged (0 deleted lines, docblock appended). New: 18 R2 tests (parity of the core rule plus the one walk withresolvePackageScopedNameover every arm and caller, a bundled type per member, the holder source,function, the engine's function holders and their removal, identity pins that objectql re-exports core's rule and metadata-core's walk), 7 tests for the bundle addition (including the dependent's own step 1, both storage shapes), and 4 tests for the platform's function.packages/metadata-protocol/src/protocol-write-package-scope.test.ts(new): identity pins showing that the save door, metadata-protocol's barrel and lint read metadata-core's one walk. Together with the objectql identity pins, this pins that no second closure is computed.packages/lint/src/runtime-gate.resolve-package-closure.test.ts: one identity pin added.packages/metadata-core/src/package-closure.test.ts(new, 4): the residual owner's closure is walked transitively by both the save door's walk and the resolution closure, an unknown id still narrows nothing, a package record wins, and a registry without the member reads as before.packages/objectql/src/manifest-service-residual-objects.test.ts: one case added. A booted stack's residual owner declares a dependency, the registry records it, no package record appears, andpackageResolutionClosurereaches it.__runtime__caller pins (an environment-only item is reached; an item stamped__runtime__is still a holder; the compatibility API stamps the one spelling), and objectql adds residual-closure pins (reaches a declared dependency, refused when declaring none, a package record wins).packages/qa/dogfood/test/email-template-dependent-unshipped-locale-read.dogfood.test.ts: one case added. Over the booted showcase, after the dependent publishesfr-FR, a package-less resolve of the base's template answers the base (en-US),fr-FRon the base's address is not-found (as the HTTP read's 404), and the read naming the dependent reaches the addition.Forced test change:
packages/objectql/src/federated-injected-column-readers.test.tsdrops its READERS row'package-scoped-resolver.ts#<module> :: queue'. The type list that mentionsqueuemoved to@objectstack/core, so that file no longer reads it.Acceptance notes
pnpm check:lean-entry-closureandcore-boundary.ratchetare green with the admitted set unchanged.ADMITTED_PACKAGESis not widened.engine.ts/engine-hooks-and-functions.ts(the function holders),engine-compatibility-api.ts(the one__runtime__spelling),plugin.ts(the residual record, coordinated with E6's thread),index.tsbarrels,metadata-core,registry.ts(the E5 item and the residual map), and the test files named above.Ablation readings (each restored, blob == HEAD, git diff HEAD empty)
Through
scripts/ablation-replace.mjs:pickUniquewith several holders returns the first: 3 failed / 14 passed (17).slice(0, 1): 1 failed / 16 passed.packageScopedHolderSourceduplicate-holder throw disabled: 1 failed / 16 passed.holders.clear()before set (one holder per name): 4 failed / 47 passed (51).function: 1 failed / 50 passed.if (base === packageId) continue;): 1 failed (red line 1, a package not declaring the holder).packageScopedHoldersOfreturns every holder (--hold, objectql rebuilt, dogfood run, restored, rebuilt). The new case was red:expected { status: 'refused', … } to deeply equal { status: 'resolved', … }, 1 failed / 12 passed. After restore and rebuild, 13 passed. The ablated rebuild exited 1 at the declaration step (the unused helper), after the JS emit the dogfood reads, and the red reading shows the mutation reached it.(id) => !this.isBundleAddition): 2 failed (the dependent's own step 1, both storage shapes).environmentItem: () => platform): 4 failed.__runtime__— the core normalization deleted: 1 failed / 22 passed (23).Verification
Head
c1c15367(main merged; main'sengine.tschange merged cleanly).node scripts/pm/dispatch-gates.mjs --commands: 81 derived, all exit 0.--ranreconcile: 81 run, 0 NOT-MEASURED, 0 UNRUN (exit 0).os-verify-lockat this head (VERDICT command-exit 0): fullturbo run buildof./packages/**; core typecheck and test, 138 files / 3174; objectql typecheck and test, 574 / 11744.15d685cf(only the core env-wide platform step changed since): metadata-core 27 / 1003; metadata-protocol 329 passed + 4 skipped / 22597; lint 197 / 8365; dogfoodemail-template-dependent-unshipped-locale-read,analytics-label-scopeandapi-key-owner-revoke, 3 files / 26; cliserve-config-boot-residual.integration, 14 / 14.lintscript. Rooteslint --no-inline-configover the changed.tsfiles: exit 0.Review round 1 (6729484996), answered
__runtime__caller, and the residual owner's declared closure (the save-door effect is declared above).Narrowing:line).sys. Q1 is ruled A (6729831159): no sealed-set arm.Fixes #31716plusPart of #31527. New public names accepted. In this round, alsoplatformItem, holder kindplatform, stepplatform,RUNTIME_COMPATIBILITY_PACKAGE_ID,PackageClosureRegistry#residualOwnerDependenciesOfandSchemaRegistry#recordResidualOwnerDependencies/residualOwnerDependenciesOf, which carry the rulings. Member-grain stays the later R amendment.packageResolutionClosure. The identity pins here hold the one walk.🤖 Generated with Claude Code
https://claude.ai/code/session_01ADHkmQ5eMDSpkq6rYGLbBU