Repository navigation
refactor(metadata-core): look up a package's customization pair through the App that carries it (pairOf), behaviour unchanged - #31026
Conversation
…gh its App pairOf(base) = customizationPackageId(appOf(base)), with appOf the identity today, so every call site answers exactly what it answered before. The "pair of this package" derivations in metadata-protocol, objectql's uninstall refusal, service-automation's flow precedence remedy and service-datasource's import landing now ask the lookup; the parse direction (customizationBasePackageId) and lint's is-a-pair skip stay on the spec helpers. runtime/src/domains/packages.ts keeps its two call sites for the next change. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibw7rMHd8P2YJ7SyYZi
…at base" The served-override predicate, the class-head restore and the declared override gate asked isCustomizationPairOf(pair, base), the same pair-of-a- package derivation spelled through a second spec helper. They now ask the lookup's isPairOf, so the App-unit change moves one function. Answers are unchanged: isPairOf(pair, base) is customizationPackageId(appOf(base)) === pair, and appOf is the identity. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibw7rMHd8P2YJ7SyYZi
…ge shape Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibw7rMHd8P2YJ7SyYZi
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibw7rMHd8P2YJ7SyYZi
…-ksdx1z # Conflicts: # packages/metadata-protocol/src/protocol-write-gates.ts
📓 Docs Drift CheckThis PR changes 5 package(s): 6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 33 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 159648dc7ee6143d8e281ddd8fa2ff684b80545a && git checkout 159648dc7ee6143d8e281ddd8fa2ff684b80545a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 7d9f1e973ecc418c21736a58da2eb211f08a3544 678d6d6efecaff6645b7a0692548b2c13a3dacb6 && git checkout -B drift-repro 7d9f1e973ecc418c21736a58da2eb211f08a3544 && git merge --no-ff 678d6d6efecaff6645b7a0692548b2c13a3dacb6
node scripts/docs-audit/affected-docs.mjs --json 7d9f1e973ecc418c21736a58da2eb211f08a3544
|
|
domain:server#2 · os-tesla · session_01L9qibyvxbrLP4jm4PargoH Contract reviewServed-tier: Inputs read: the order ① Derived judgments
② Semver level
Check-runs on ③ Boundary flags
④ Design judgment
Findings (none must-fix):
Nits:
Implemented-by: VERDICT: PASS — on the contract; landing waits on the in-progress checks ( Record written 2026-11-06T18:44Z. |
…se-② yes (widening) The contract review read appOf, pairOf and isPairOf as a widening of the published @objectstack/metadata-core entry; level stays patch and Metadata-protocol stays none. The unit table's docblock now says its rows' type, scope and namespace are the App resolver's future inputs. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibw7rMHd8P2YJ7SyYZi
|
domain:server#2 · os-tesla · session_01L9qibyvxbrLP4jm4PargoH Contract review · deltaServed-tier:
Open after the delta: nothing owed to the dev. Nits 3 and 4 stand as PR 2 notes. Implemented-by: VERDICT: PASS (delta) — landing waits only on the |
|
domain:server#2 · os-tesla · session_01L9qibyvxbrLP4jm4PargoH 维护者速读
席位意见:可以落地。 席位核对ACCEPT · seat
|
Requested by Tesla · project thread
Refs #29594 (PR 1 of 5–7: the lookup refactor only; the ensure door returning the App's pair, the
customizableverdict, thesys_pass-through close and the platform pair follow in later PRs)Clause-②: yes (widening)
Metadata-protocol: none
Ruling-ref: 6691022506
维护者速读
pairOf(base)(在@objectstack/metadata-core),它先问「这个包属于哪个 App」(appOf),再按 App 推出<id>.custom。原来散在 metadata-protocol、objectql、两个 service 里的 20 处直接推导和 3 处isCustomizationPairOf判定都改走它。app.objectstack.hotcrm.custom)。下一个 PR 只要改appOf一处,所有读写门、列表、卸载拒绝就一起按 App 找定制包,不会有漏改的地方各说各话。appOf现在原样返回包自己,所以每处答案、拒绝原因、定制包 id 都和今天一模一样;runtime的两处留给下一个 PR。回滚即 revert 本 PR。packages/metadata-core/src/customization-pair-lookup.ts(整个查找就 3 个函数);packages/metadata-protocol/src/protocol-packaged-base-refusal.ts(改动最多的一处,5 个调用点 + 1 处判定);packages/metadata-protocol/src/served-override.ts(被五个读路径共用的判定)。pnpm --filter @objectstack/metadata-core exec vitest run --maxWorkers=2 src/customization-pair-lookup.test.ts,预期 16 个用例全绿(App、模块、独立包、sys内核包、无命名空间的包都与旧推导相同)。What changes
The lookup (
packages/metadata-core/src/customization-pair-lookup.ts, exported from@objectstack/metadata-core).appOf(packageId): the App that carries a package for customization. Today the identity: every package is its own App, which is what every call site assumed.pairOf(base)=customizationPackageId(appOf(base)): the pair, or the same refusal the id rule names (base_id_missing,base_is_customization_package,base_id_invalid).isPairOf(pairId, base): whetherpairIdis THE pair ofbase, the predicateisCustomizationPairOfanswered.The id rule itself stays in
@objectstack/spec/kernel;packages/specis untouched. Why metadata-core and not metadata-protocol or objectql:service-datasource(a class (i) site) declares neither of those, onlymetadata-core, andmetadata-coreis the one package every class (i) caller already declares (metadata-protocol, objectql, service-automation, service-datasource, runtime). No new dependency edge, no lockfile change. That is the criterion already used for the write dispatch predicates and the code-artifact provenance test in that package (claim amendment 6691222368).The call sites. Each class (i) site now calls
pairOf/isPairOfwhere it calledcustomizationPackageId(base)/isCustomizationPairOf(pair, base). Nothing else in those functions moved: same branches, same sentences, same refusal codes.Census of
git grep -n customizationPackageId origin/main -- 'packages/**/*.ts' ':!*.test.ts'at69e133ea: 53 hits in 17 files. (i) = "the pair of this package" derivation, routed through the lookup. (ii) = the definition, a parse ("is this id a pair"), an import, a doc mention or a field name; stays.lint/src/validate-customization-namespace.tsbase_is_customization_packagereason on the manifest's own id, the same parse asisCustomizationPackage(manifest.id))metadata-protocol/src/protocol-customization-package.tsmetadata-protocol/src/protocol-discovery-and-bases.tsmetadata-protocol/src/protocol-environment-rows.tsbaseAddressServesMember)metadata-protocol/src/protocol-meta-layered-reads.tsmetadata-protocol/src/protocol-meta-list-reads.tsmetadata-protocol/src/protocol-overlay-merge.tsmetadata-protocol/src/protocol-packaged-base-refusal.tsmetadata-protocol/src/protocol-packageless-overlay-migration.tsmetadata-protocol/src/protocol-served-overrides.tsmetadata-protocol/src/protocol-write-gates.tspairOfAddedMember)objectql/src/registry.tsPackageUninstallExtendedError.customizationPackageIdfield declaration + assignment (a public property name, not a derivation)runtime/src/domains/packages.tscustomizeInPairRemedy, L3628 package customizations read)services/service-automation/src/flow-precedence.tsundeclaredRemedy)services/service-datasource/src/plugin.tsspec/src/kernel/customization-package.tsspec/src/api/package-lifecycle.zod.tsClass (i) is 22 (the ruling estimated about 20): 20 routed here, 2 in
packages/runtime/src/domains/packages.tsleft for PR 2 by the order, because #30756 (same seat) is editing that file.Plus 3
isCustomizationPairOf(pair, base)sites, the same derivation through a second spec helper, routed toisPairOf(claim amendment 6691222368):metadata-protocol/src/served-override.ts:88(isServedOverrideRowOf, the predicate five reads share),protocol-class-head.ts:190(restore intent),protocol-packaged-base-refusal.ts:1076(declared-override gate, which already readspairOftwo lines up). After this PR,git grep -n "customizationPackageId(\|isCustomizationPairOf(" -- 'packages/**/*.ts' ':!*.test.ts' ':!packages/spec/**'answers 4 lines: the lookup itself, lint's is-a-pair skip, and the 2 deferred runtime sites.Not in this PR: the ensure door resolving a member to its App and answering the App's pair (200), the
customizableverdict, thesys_pass-through invalidateObjectNamespacePrefix, the platform pair, the ADR-0146 D3 amendment (#31014, spec seat). Kernel objects stay not customizable: ③ is ruled (6691217245), the kernel whitelist (Users, Organizations) is a later version's work.Tests
New
packages/metadata-core/src/customization-pair-lookup.test.ts(16 cases): for an App package (app.objectstack.hotcrm), a module its artifact brings (app.objectstack.hotcrm.service, still its own pair today), a standalone package, asyskernel package (com.objectstack.plugin-auth, scopesystem) and a manifest with no namespace,appOfis the identity andpairOfequalscustomizationPackageIdand the literal pair; the five refusals match the id rule;isPairOfequalsisCustomizationPairOfover seven candidates per row.The existing suites of every touched package are the behaviour pin: they assert the served answers (pair ids in refusal sentences, served override rows, list and layered reads, uninstall refusal, flow precedence remedy, datasource import landing) and none of them changed.
Ablation (appOf moved off the identity, twice)
Both mutations through
node scripts/ablation-replace.mjson the linereturn packageId;ofcustomization-pair-lookup.ts, each restored and proved (blob == HEAD bd176c7702ab,git diff HEADempty, whole-treegit status --porcelainempty).return packageId === 'app.objectstack.hotcrm.service' ? 'app.objectstack.hotcrm' : packageId;(the HotCRM module resolved to its App).Tests 2 failed | 14 passed (16): the module row'sappOf(Expected: "app.objectstack.hotcrm.service",Received: "app.objectstack.hotcrm") and itsisPairOf(expected false to be true). The other rows stay green.dist/, the consumers:return packageId === 'com.acme.crm' ? 'com.acme.suite' : packageId;,pnpm --filter @objectstack/metadata-core build,ablation-dist-preflight.mjs @objectstack/metadata-core '"com.acme.suite"' --source-marker=…exit 0 (marker in 2 built files).metadata-protocolserved-override.test.ts,protocol.pair-translation-bundle.test.ts,protocol.named-base-overlay-write.test.ts,protocol.packaged-base-refusal.test.ts: 48 failed (23 + 22 + 1 + 2), for exampleExpected: "'com.acme.crm.custom' (?package=com.acme.crm.custom)",Received: "… save it into 'com.acme.suite.custom' (?package=com.acme.suite.custom) …". So the write gates, the served-override predicate and the pair translation read ask the lookup through the published build. Restore, rebuild, preflight--absentexit 0 (absent from all 12 built files), tree clean.Verification
Suites, typechecks, lint
Before the merge of
origin/main(head33e47fa35):pnpm --filter @objectstack/metadata-core test: Test Files 25 passed, Tests 944 passedpnpm --filter @objectstack/lint test: Test Files 196 passed, Tests 8275 passedpnpm --filter @objectstack/service-datasource test: Test Files 72 passed, Tests 1239 passedpnpm --filter @objectstack/service-automation test: Test Files 264 passed, Tests 3431 passedpnpm --filter @objectstack/metadata-protocol test: Test Files 320 passed, 4 skipped; Tests 22487 passed, 26 skippedpnpm --filter @objectstack/objectql test: Test Files 560 passed, Tests 11207 passedtypecheckon metadata-core, metadata-protocol, objectql, service-automation, service-datasource: exit 0 eachpnpm exec eslint --no-inline-configover the 18 changed.tsfiles: exit 0node scripts/check-platform-checklist.mjs --changed-from origin/main: exit 0 (22 anchors judged, 0 lost)After merging
origin/mainat33bc8ae5f(one conflict, the spec import line ofprotocol-write-gates.ts: main addedDEFAULT_METADATA_TYPE_REGISTRYbesidecustomizationPackageId; resolved toDEFAULT_METADATA_TYPE_REGISTRYalone, the body already readpairOf),pnpm install --frozen-lockfile+ closure rebuild, head979d839e4:pnpm --filter @objectstack/metadata-core test: Test Files 25 passed, Tests 944 passedpnpm --filter @objectstack/metadata-protocol test: Test Files 320 passed, 4 skipped; Tests 22489 passed, 26 skippedpnpm --filter @objectstack/objectql test: Test Files 560 passed, Tests 11207 passedpnpm --filter @objectstack/service-automation test: Test Files 264 passed, Tests 3431 passedpnpm --filter @objectstack/service-datasource test: Test Files 72 passed, Tests 1279 passedtypecheckon the same five: exit 0 eachcustomizationPackageId(/isCustomizationPairOf(call site arrived with mainDerived gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackagainst this diff, every line run with stdin closed, exit codes recorded before any pipe: record/mnt/project-files/server-seat-2/r2026-11-06/out/29594-gates-ran-33e47fa35.list.--ranreconciliation: 65 derived, 64 run, 1 NOT-MEASURED, 0 UNRUN; every run family exit 0.pnpm check:dual-build-cjs-loads, reason: PREREQUISITE NOT MET (it reads every package'sdist/; this worktree built the closure of the touched packages, not the whole workspace). CI'sLint & Repo Gatesruns it on a full build.node scripts/check-plugin-teardown-shape.mjs --self-testfirst answered exit 3 on the shallow clone (pinned fixture commit621a4876unreachable); aftergit fetch origin 621a4876…it is exit 0 (48 cases).Acceptance notes
Clause-②: yes (widening)(the contract review's reading: three new names on the published@objectstack/metadata-coreentry), patch on@objectstack/metadata-core(three new exports),metadata-protocol,objectql,service-automation,service-datasource(their dist now imports the lookup). No served answer changes, soMetadata-protocol: none.packages/runtime/src/domains/packages.tsL669 and L3628 (now L672 / L3737 after the merge). The parse direction (customizationBasePackageIdin clipackage/install.ts, cloud-connectionmarketplace-install-local-plugin.ts, runtime, metadata-protocol) is class (ii) and stays: it maps a pair to its base, which under the App unit is the App.packages/**only), for PR 2's rehearsal and thedomain:uihalf: six client-side derivations inui/packages/app-shellcallcustomizationPackageIdfrom the spec directly:views/metadata-admin/ResourceListPage.tsx:211,views/studio-design/AutomationsFlowRail.tsx:367,BuilderLanding.tsx:163,CustomizationMark.tsx:143,customizationMarks.ts:58,packages-io.ts:239. They answer the same as the server today; onceappOfresolves a module to its App they would still derive the module's own pair (for exampleapp.objectstack.hotcrm.service.custom) until the console reads the served pair (ruling 6691022506 routes that todomain:ui, after fix(studio): open system-scope packages read-only, with Customize #29591).app.objectstack.hotcrm.service→app.objectstack.hotcrm.service.custom) pins today's answer on purpose; it is the row the App resolver is meant to flip.PackageUninstallExtendedError.customizationPackageIdkeeps its name: it is a public error property, and renaming it is not part of a no-behaviour-change refactor.pr_create; the harness's assignee/reviewer step is skipped per the dispatch frame (the seat assigns at hand-over).Head after the review fixes:
678d6d6e(changeset declaration + one test docblock line).