Repository navigation
feat(metadata-protocol)!: thread the installed-manifest lookup through every pair site; the ensure door creates no pair for a kernel package - #31060
Conversation
…site; kernel verdict on the ensure door Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibzCo6pa7SSUFmosQD8
…loses reopen; changeset Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibzCo6pa7SSUFmosQD8
…plugin's other optional reads' way Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibzCo6pa7SSUFmosQD8
…nt-wide, the kernel having no pair Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibzCo6pa7SSUFmosQD8
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibzCo6pa7SSUFmosQD8
…dited test files (check:test-source-alias) Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibzCo6pa7SSUFmosQD8
📓 Docs Drift CheckThis PR changes 9 package(s): 21 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 10 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 70 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 c96d3211551fd414721a4ca4f81119036a95fe47 && git checkout c96d3211551fd414721a4ca4f81119036a95fe47
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 23cf11a4b9f3fa8c7291b5d7a308c5d14793b006 b94d1ac68d151d99ad908992ad25f65bf2413a6d && git checkout -B drift-repro 23cf11a4b9f3fa8c7291b5d7a308c5d14793b006 && git merge --no-ff b94d1ac68d151d99ad908992ad25f65bf2413a6d
node scripts/docs-audit/affected-docs.mjs --json 23cf11a4b9f3fa8c7291b5d7a308c5d14793b006
|
…t's copy lands environment-wide Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibzCo6pa7SSUFmosQD8
|
domain:server#2 · os-tesla · session_01L9qibyvxbrLP4jm4PargoH Contract reviewServed-tier: Inputs read: rulings 6691022506 (2026-11-06T17:24Z, unit = App; (b) readable verdict; ② "always installed whole" recorded as goal, not fact) and 6691217245 (2026-11-06T17:33Z, every kernel object not customizable in step 1; the ensure door refuses a kernel base with the readable verdict, not 422; whitelist next version); the seat's claim amendments 6694004188 (2026-11-06T19:50Z) and 6694069747 (2026-11-06T19:53Z, the re-split: PR 2 = lookup threaded with ① Lookup threading — behaviour unchanged
② The kernel verdict
③ The clone collision (option A)
④ Tests, ablations, dogfood, gates (the dev's readings;
|
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibzCo6pa7SSUFmosQD8
…ming no pair Ruling record 6691217245 serves every kernel object as not customizable in this version, so the package-less close and the close of a write naming the base stay shut on a kernel item exactly where they were shut before the ensure door's kernel verdict: 403 NOT_OVERRIDABLE, with a sentence that names no pair. The ensure door's other judgements are read with its kernel verdict left out (judgeKernel: false), and the fix carries kernelScope. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibzCo6pa7SSUFmosQD8
…nswers the ensure door's 409 The flow clone door lands the copy in the source's pair through the ensure door; a kernel package has none in this version, so the door relays the 409 and the registration made before the landing is undone: no copy in the engine, no row, no pair. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibzCo6pa7SSUFmosQD8
…set, and say where a kernel copy lands The changeset's Clause-② line names both arms: the flow clone's 200 to 409 for a kernel-shipped flow beside the ensure door, and the five new exports and the new permission-set clone notice code. The closes bullet now says they stay shut on a kernel item. Who moves names a vendor package that declares a platform scope. metadata-lifecycle.mdx says a kernel item's package-less write names no pair, a kernel flow's copy answers 409 until package is named, and a kernel set's copy lands environment-wide. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibzCo6pa7SSUFmosQD8
Requested by Tesla · project thread
Clause-②: yes (narrowing) —
POST /api/v1/packages/:id/ensure-customizationanswers a kernel base (scopesystem/cloud) with409 RESOURCE_CONFLICT, where it created<id>.custom; a copy of a kernel-shipped permission set with nopackagenamed is saved environment-wide (new notice codePERMISSION_SET_CLONE_ENVIRONMENT_WIDE_KERNEL_SOURCE) where it landed in<kernel id>.custom; the exported pair helpers take the installed-manifest lookup as a required argument.Metadata-protocol: none
Refs #29594
Ruling-ref: 6691022506
Ruling-ref: 6691217245
维护者速读
pairOf/isPairOf/isServedOverrideRowOf/ 写闸 / flow 优先级 等 23 处、14 个文件,连同 cli 与 cloud-connection 的调用方)都改为携带「已安装清单查询」,appOf仍是恒等,行为不变;②ensure 门对内核包(scopesystem/cloud)返回 409RESOURCE_CONFLICT,不再创建<id>.custom。appOf一处把模块解析到它的 App。packages/metadata-protocol/src/protocol-customization-package.ts(内核判定)、packages/metadata-core/src/customization-pair-lookup.ts(查询入口)、packages/objectql/src/protocol-custom-override-effective-read.test.ts末尾两条钉子。pnpm --filter @objectstack/runtime exec vitest run --project local src/domains/packages-ensure-customization.test.ts(内核 409、旧配对包保留);pnpm --filter @objectstack/objectql exec vitest run --project local src/protocol-custom-override-effective-read.test.ts(旧内核配对包的覆盖继续生效、两道关闭恢复可写)。appOf翻转、旧配对包容忍、ensure 门回答 App 配对包、HotCRM 演练)在本 PR 合并且 ADR-0146 修订 docs(adr): ADR-0146 D3 — one.custompair per App, and one platform pair for the kernel suite #31027 合并后开工。What changes
1. The installed-manifest lookup, threaded through every pair site (behaviour unchanged)
pairOf(base) = customizationPackageId(appOf(base))landed in #31026 withappOfthe identity. Resolving a module to its App (ADR-0146 amendment, merge question 11, #31027) needs the registry's installed manifests (type/dependencies/namespace/scope) at every site that asks "which pair customizes this package?". If only the ensure door resolved the App, the override gate and the served-override read (still identity) would refuse or hide the very override written into the App pair. So this PR threads the lookup everywhere first, withappOfstill the identity, and PR 3 becomes a one-place flip.@objectstack/metadata-core:InstalledManifestLookup,installedManifestsOf(registry);appOf,pairOf,isPairOftake the lookup (required, so the compiler names every site that has not passed it).@objectstack/metadata-protocol:installedManifestsOfEngine(engine);isServedOverrideRowOf(row, base, installed),OverrideWriteClosure.installed, the overlay merge, the lock state, the layered/list reads, the per-package layer, the stored-flow slot, the served-overrides census, the package-less migration census, the packaged-base refusal and the write gates. Hosts that read the engine gain theenginegetter (the codebase'sreadEnginepattern).objectql(plugin, registry uninstall refusal),runtime(customizeInPairRemedy, the per-package customizations read),service-automation(flow precedence, with the objectql registry),service-datasource,cli(offline override declarations: the walked manifests),cloud-connection(install-local: the delivered manifests over the registry's).customization-pair-lookup.test.tsasks every row twice, with nothing installed and with every row installed (the module declaring its App the way HotCRM's modules do), and gets the old derivation; every existing suite of the touched packages is green with the lookup passed.os validate's customization-namespace advisory skips a pair throughisCustomizationPackage. Edge: an id ending in.customthat no base derives (a.custom.custom, bare.custom) is now judged like any package.2. The kernel verdict on the ensure door (narrowing)
system/cloud), no pair yet201, creates<id>.custom409 RESOURCE_CONFLICT, nothing created or written200,created: false409 RESOURCE_CONFLICT; the pair stays installed and readable403 NOT_OVERRIDABLE, naming the pair403 NOT_OVERRIDABLE, naming the pairpackage200, copy in<kernel id>.custom(created)200, copy environment-wide, codePERMISSION_SET_CLONE_ENVIRONMENT_WIDE_KERNEL_SOURCEpackage200, copy in<kernel id>.custom409 RESOURCE_CONFLICT, the ensure door's sentenceThe code.
409 RESOURCE_CONFLICTis the door's existing family for "the base's state rules the pair out" (a writable base, no namespace, a namespace with no pair namespace, a non-SemVer version, a malformed existing pair), every one of which answersRESOURCE_CONFLICTinpackages-ensure-customization.test.ts.403would misread as an authority problem (even the platform operator is refused), and422 NAMESPACE_CONFLICTnames another condition. No ledger code and nopackages/specedit.Where. First in
customizationPairEnsureRefusal, the one reading of the door's registry-decidable refusals, so the two overlay closes andoverlayPairFixOfsee the same verdict the door throws.The "new similar" doors (found by the dogfood battery). The permission-set clone door landed a copy of a kernel-shipped set (
member_default, shipped bycom.objectstack.plugin-security, scopesystem) in the source package's pair through the ensure door, so the verdict refused the commonest lock-and-clone path. A clone is a new item, not a customization of the kernel item, so with nopackagenamed the copy is now saved environment-wide, exactly as a copy of an environment-authored set is, with its own notice and code (PERMISSION_SET_CLONE_ENVIRONMENT_WIDE_KERNEL_SOURCE; a Console that does not know the code shows the English sentence, as it does for any unknown code). A namedpackagestill lands there. The kernel predicate is one spelling,kernelPackageScope(@objectstack/metadata-protocol), read by the door and the clone door. The flow clone door has no environment-wide landing (an unpackaged flow already needspackage), so a kernel-shipped flow gets the ensure door's 409 sentence. This landing is option A of a decision card put to the maintainer; B (refuse, namingpackage) and C (a kernel pair for clones only) are the alternatives.Test fixtures.
packages-ensure-customization.test.tsandautomation-flow-clone-custom-package.test.tsmodelled "an installed app" asscope: 'system', which is the kernel's scope (manifest schema:system= cross-project shared identity,cloud= control plane). They now hold the base as code, the way an installed app's packages are held (HotCRM 6961ab1 declares no scope).Blast radius (measured on
origin/main3fe5f2a)23
pairOf/isPairOfsites in 14 files;isServedOverrideRowOf17 non-test callers;judgeOverrideWrite4 (incl.clioffline andcloud-connectioninstall-local). This PR: 31 source files (+471 / −156), 16 test files (+342 / −123), one changeset; 48 files, +842 / −279 in all.PR 2 / PR 3 split
appOfthe identity; the kernel verdict..custompair per App, and one platform pair for the kernel suite #31027 merges):appOfresolves App membership from the lookup (Q11); legacy per-package pairs stay served (Q12:isPairOfaccepts the App pair or an installed legacy<base>.custom); the ensure door answers the App pair withbasePackageId= the App id (the spec describe "packageId:<basePackageId>.custom" stays true); the HotCRM rehearsal.Findings kept for PR 3: the package-less migration's incomplete-declarations census parses pair→base (
customizationBasePackageId) and misses module overrides under the App unit; its census still lists a kernel item's package-less row asmovableand the apply step fails it with the kernel sentence (a disposition for it is a contract question); the dogfood helpercustomization-pair.tsand sixui/packages/app-shellcall sites derive<base>.customclient-side; acustomizableflag on the package list row would need a spec sub-card.Verification
Head
1340933c2, baseorigin/main3fe5f2a (merges cleanly with today's main, which is 17 commits past that base). Exit codes landed on disk before reading.pnpm typecheckin metadata-core, metadata-protocol, objectql, runtime, cli, cloud-connection, lint, service-automation, service-datasourcepnpm testmetadata-core / metadata-protocol / lint / service-datasource / service-automation / cloud-connection / cli / objectql / runtimepnpm turbo run test --filter=@objectstack/dogfoodon head 021a8a3 (the later commit adds only test-file warm imports)--no-inline-configon every changed.tsfiledispatch-gates --commands(75 families), each run with</dev/null--ran: 75 derived, 75 run, 0 NOT-MEASURED, 0 UNRUNGate notes:
check-plugin-teardown-shape --self-testfirst answered 3 (the pinned fixture commit 621a487 was not in the shallow clone; fetched, then exit 0).check:dual-build-cjs-loadsfirst answered 3 (no full build); after a full package build, exit 0.check:test-source-aliasreported four test files this PR edits that reach a lazy import during collection; each gained the module-top warm import it names, then exit 0.Ablations (
scripts/ablation-replace.mjs, restore proven by blob hash == HEAD and an emptygit status --porcelain):customizationPairEnsureRefusaldisabled (ablation-dist-preflightconfirmed the mutation inmetadata-protocol/dist): red — the three runtime door pins (system, cloud, the legacy kernel pair) and the two objectql pins (legacy pair served after a refused ensure; the closes leave a kernel item's save open);resolveDefaultCloneLandingdisabled: red —[#29594] a set a kernel package ships copies environment-wide.The lookup threading needs no ablation: it changes no answer, which every existing suite of the nine packages confirms, and
customization-pair-lookup.test.tspins that both lookups (nothing installed, every row installed) give the old derivation.Acceptance notes
packages/spec,ui/orskills/edit; no ADR edit.🤖 Generated with Claude Code
https://claude.ai/code/session_01L9qibzCo6pa7SSUFmosQD8