Repository navigation
fix(runtime)!: the package disable door refuses while an enabled package's validation rule reads a field the package adds - #30825
Conversation
…age's validation rule reads a field the package adds PATCH /packages/:id/disable now answers 409 PACKAGE_DISABLE_RESTRICTED_BY_VALIDATION_RULE while a validation rule of any enabled package (declared dependent or not, any severity, an app's .custom pair included) reads a field the package adds through objectExtensions, on the rule's own object or one hop through a reference field. The refusal is stamped after the read-only check and before disablePackage, names every such rule as package · object · rule in its message, carries no error.details beyond the code, and changes nothing: the row, the served objects and the environment's state file stay as they were. A formula over the field does not refuse the disable (it answers null), and a rule switched off reads nothing. The rule's reads come from analyzeRelationshipTraversals, the member reader the rule validator asks; hops are judged with referenceTargetOf. The code is held as its wire pair, which check:error-status-conformance reads as the producer. Claude-Session: https://claude.ai/code/session_016WV4cwCWp9qZ5SgMof1qLa Co-authored-by: Claude <noreply@anthropic.com>
…ield the package adds The marketplace handbook's Enable-and-disable step and Disabling-a-module list, the metadata API's disable entry and the error catalog's Conflict section name PACKAGE_DISABLE_RESTRICTED_BY_VALIDATION_RULE, what it names and the remedy. Claude-Session: https://claude.ai/code/session_016WV4cwCWp9qZ5SgMof1qLa Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cwCWp9qZ5SgMof1qLa Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 25 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 7 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 28 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 08d974c5d8317b2edb53f3f78623fd48efcf2d0f && git checkout 08d974c5d8317b2edb53f3f78623fd48efcf2d0f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 555eb1ba18c9738e5831b7844822b08fda7c70ac c15ba57084196643bacd7b42344bb95563a09dcf && git checkout -B drift-repro 555eb1ba18c9738e5831b7844822b08fda7c70ac && git merge --no-ff c15ba57084196643bacd7b42344bb95563a09dcf
node scripts/docs-audit/affected-docs.mjs --json 555eb1ba18c9738e5831b7844822b08fda7c70ac
|
|
domain:server#4 · os-elon-musk · session_016WV4czvZykUqhYxYXSSDt4 Contract review for card #30379 · narrowing face ( Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgmentThe narrowing makes ruling A real at the door:
The one exception: in the tenant-overlay state, the door takes away a disable that A permits, and the documented remedy becomes unreachable for the handbook's own audience. That is must-fix 1: judge the rules as the fold serves them (one rule per name, the tenant's Check-runs at review time: 32 success, 12 skipped, 1 failure (Test Core 2/8, the catalog count), 4 in progress. Implemented-by: VERDICT: FAIL |
…-mwjmre Claude-Session: https://claude.ai/code/session_016WV4cwCWp9qZ5SgMof1qLa Co-authored-by: Claude <noreply@anthropic.com>
…s them, and ask it on the install door's enableOnInstall false arm The dependent-rule scan now reads each object as the registry's fold serves it once the disable lands (getAllObjects with the package's extensions left out, less the rules the disable takes off a base that carries them): one rule per name, the copy a later layer declares winning, with a tenant overlay's message and active on it, so a packaged rule the tenant switched off through the overlay no longer refuses the disable. Each rule is named after the last layer in fold order that declares it, a tenant overlay by that name, never a placeholder. POST /packages with enableOnInstall false asks the same scan before it disables: such an install still lands, the package stays enabled on the row, the registry and the state file, and the door answers the same 409 instead of a 201 that claims a disable. The error catalog's advertised wire-code count moves to 58 for the new entry, and the docs name the fold reading and the install arm. Claude-Session: https://claude.ai/code/session_016WV4cwCWp9qZ5SgMof1qLa Co-authored-by: Claude <noreply@anthropic.com>
|
domain:server#4 · os-elon-musk · session_016WV4czvZykUqhYxYXSSDt4 Contract review for card #30379 · narrowing face ( Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
Check-runs at review time on Implemented-by: VERDICT: FAIL |
…verwrite of an enabled package POST /packages with enableOnInstall false asked the scan on every install, so a fresh install (or a reinstall after a delete) of a package whose field an enabled rule already names landed ENABLED against the caller's explicit false, though nothing served is withheld there. The door now reads, before either install limb writes, whether the id's row exists and is enabled, and asks the scan only then; a fresh install and an overwrite of a disabled row honour false exactly as #18877 rules. The docs and the changeset say "an overwrite of an enabled package". The scan follows a rule's record.REF.FIELD hop with referenceTargetOf, which the second-object read census in @objectstack/core holds to a classification: it is named there as not-a-served-read (it reads no record of the target and serves none of its fields). Claude-Session: https://claude.ai/code/session_016WV4cwCWp9qZ5SgMof1qLa Co-authored-by: Claude <noreply@anthropic.com>
|
domain:server#4 · os-elon-musk · session_016WV4czvZykUqhYxYXSSDt4 Flake judgment · 2026-11-06T08:42Z · run 42186037268, Test Core (6/8) on head |
|
domain:server#4 · os-elon-musk · session_016WV4czvZykUqhYxYXSSDt4 Contract review for card #30379 · narrowing face ( Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
Check-runs at review time on Implemented-by: VERDICT: PASS |
|
domain:server#4 · os-elon-musk · session_016WV4czvZykUqhYxYXSSDt4 维护者速读
席位意见:ACCEPT(#30379,裁定 A)。 席位核对Head-sha:
|
|
domain:server#4 · os-elon-musk · session_016WV4czvZykUqhYxYXSSDt4 Maintainer's per-PR word (BREAKING) · 2026-11-06T14:59Z Elon chose 「同意落地」 on the seat's decision card in the server seat 4 thread, 2026-11-06T14:59Z, for this PR at head |
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 42239876496 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 分类: 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
Fixes #30379
Requested by Elon · project thread
domain:server#4 · os-elon-musk · session_016WV4cwCWp9qZ5SgMof1qLa
Clause-②: no (narrowing)
Metadata-protocol: none
Skills-docs: updated here —
content/docs/api/metadata-api.mdx(the disable door's entry),content/docs/deployment/marketplace-handbook.mdx(the Enable-and-disable step and the Disabling-a-module list) andcontent/docs/api/error-catalog.mdx(the code's 409 entry and the wire-code count) name the refusal;skills/**teaches no package disable door (grep forpackages/:id/disable,disableable,WRITABLE_PACKAGE_REQUIRED: none), so no skill or eval moves.Claim: 6672353180 (amended 6672467575) · Branch:
claude/project-thread-mwjmre· Ruling-ref: 6670643454 (letter A, items 1–3) · ADR line: #30729 (merged) · Error code: #30724 / PR #30775 (merged) · Carry note: 6675092387 · Contract review: 6677642547 (FAIL) and delta review 6679292152 (FAIL), both addressed atc15ba57084维护者速读
PACKAGE_DISABLE_RESTRICTED_BY_VALIDATION_RULE),提示里逐条列出「包 · 对象 · 规则」,什么都不改。规则按停用后对象实际生效的样子判断:同名规则只判胜出的那一份,客户在自己那份对象上关掉的规则算关闭,规则归到它生效文本所在的那一层(客户覆写层写作tenant overlay)。用enableOnInstall: false覆盖一个当前启用的包时同样判断:装上,但保持启用,答同一个 409;全新安装、删除后重装、覆盖已停用的包照旧装成停用。公式读这些字段不拦(停用后答空值)。packages/runtime/src/domains/packages.ts的readDisableDependentRules(第二遍改为读注册表自己的折叠结果)、ruleLayerLabel、停用门和安装门的 409 分支;测试文件最后三个 describe。pnpm --filter @objectstack/runtime exec vitest run src/domains/packages-disable-module-extension-fields.test.ts,预期 20 条全绿。What changes
@objectstack/runtime:PATCH /api/v1/packages/:id/disable(domains/packages.ts)After the read-only check (
isDisableablePackage/requireWritablePackage), and beforeregistry.disablePackageandsetPackageDisabled, the door asksreadDisableDependentRules(registry, id). If that is non-empty, it answers409witherror.codePACKAGE_DISABLE_RESTRICTED_BY_VALIDATION_RULE(ledgered under@objectstack/runtimeby PR feat(spec): register PACKAGE_DISABLE_RESTRICTED_BY_VALIDATION_RULE, the package disable door's 409 when an enabled package's validation rule reads a field the package adds #30775). The message names every rule aspackage · object · rule (reads FIELD), one or several. Nothing changes on a refusal. The refusal carries noerror.detailsbeyond the code (the seat's ruling on carry-note item 2, see Out of scope).What the disable withholds, per object the package extends: the fields its
extendcontributions add that neither the owner's own declaration nor an extension that stays served also declares. This is the registry's own reading of a withheld member (withholdDisabledExtensions), asked of the state the disable would leave.Which rules count (ruling items 1–2, contract review must-fix 1): the rules the served object will carry once the disable lands, of every severity, from any enabled package, declared dependent or not (no
manifest.dependenciesentry is read), an app'sBASE.custompair included. They are read off the registry's own fold,getAllObjects(undefined, { withoutExtensionsOf: id }), never re-assembled in the door. So:mergeValidationsByName), and only the winning copy is judged;messageandactivesit on the packaged rule it names (tenantRuleSettingsOf/withPackagedOwnerRules), so a rule the tenant switched off through the overlay is off;withoutWithheldMembers), the one step a left-out fold does not take, which the scan mirrors.On an object whose owner is disabled or is the package itself, no write reaches any rule, so none counts. A package already disabled withholds nothing more, so the scan answers empty.
Who a rule is named after (must-fix 1): the LAST layer in fold order (the base, then each
extendcontribution that stays) that declares the rule's name, which is the layer its served text comes from. A tenant overlay is namedtenant overlay of PKG(ortenant overlaywhen it carries no package); the(no package)placeholder is gone (an unpackaged layer isunpackaged object/unpackaged extension).What "reads" means:
analyzeRelationshipTraversals(@objectstack/formula) overrecordandprevious. That coversscript/cross_fieldconditionand aconditional'swhen, at any depth. It is the reading the rule validator itself asks before it refuses a write (readsRecordColumn).record.REF.FIELDread is also a hop into REF's target, judged withreferenceTargetOfon the served field.state_machine/format/json_schemarule reads thefieldit names.Not counted:
null, PR fix(objectql)!: withhold a disabled package's objectExtensions fields from the object they extend, and keep their columns #30362, ruling item 1);active: false, on its own copy or through the tenant overlay; the remedy the refusal names);The code is held as its wire pair
{ code, status: 409 }, the shapecheck:error-status-conformancereads as a producer.@objectstack/runtime:POST /api/v1/packageswithenableOnInstall: false, on an overwrite of an enabled package (contract review should-fix, delta review must-fix 3)registry.getPackage(pkgId)?.enabled === true). Only then can the install'senableOnInstall: falsearm withhold a field a rule is served against, so only then does it ask the samereadDisableDependentRules(registry, pkgId)before it disables.409and code, with the message leadPackage 'ID' was installed and stays enabled: enableOnInstall: false cannot disable it, instead of a201that claims a disable.DELETE /packages/:id) and an overwrite of a disabled package withhold nothing served, so they honourenableOnInstall: falseexactly as [Decision] the package boot seed set is never updated by enable/disable, so a flag-absent re-install durably reverts an operator later enable once #18752 lands — and the obvious producer fix re-opens #18058 F1 pins #18877 rules (「有旗 = 设置」):201, disabled. With no reading rule, an overwrite of an enabled package disables as before.@objectstack/core: the second-object read census (src/security/second-object-read-exposure.pin.test.ts)record.REF.FIELDhop withreferenceTargetOf, which makespackages/runtime/src/domains/packages.tsa new caller of the reference-target arbiter. The census names itnot-a-served-read: it reads no record of the target and serves none of its fields, it only asks whether FIELD is one the disable would withhold, from declared metadata. (Unclassified, this pin turned Test Core 7/8 red ond315efca97.)Docs (carry-note item 3, must-fix 2)
content/docs/api/error-catalog.mdx: the advertised count moves from 57 to 58 error codes reachable on the wire (L7;packages/spec/src/api/error-catalog-docs.test.tspins it against the wire face). A### PACKAGE_DISABLE_RESTRICTED_BY_VALIDATION_RULEentry under Conflict Errors (409), with cause (the served-fold judgment, the layer naming and the install arm on an overwrite of an enabled package), fix andno_retry. The status-conformance gate reconciles it against the producer above (packages.ts: { code, status }, 409).content/docs/deployment/marketplace-handbook.mdx: the Enable-and-disable step and the Disabling-a-module list gain the 409 row, a customer's switched-off copy counting as off, and the install arm on an overwrite of an enabled module (a fresh install still lands disabled). Before, they named only422 WRITABLE_PACKAGE_REQUIRED.content/docs/api/metadata-api.mdx: thePATCH /packages/:id/disableentry names the refusal, the served-fold judgment, its remedy and the install arm on an overwrite of an enabled package.Changeset:
@objectstack/runtimepatch, with the BREAKING banner (accept-set narrowing on the disable door, and on the install door for an overwrite of an enabled package),Clause-②: no (narrowing),Metadata-protocol: noneand the ADR-0087 dispositionnot-required (no-migration-prescription).Tests
packages/runtime/src/domains/packages-disable-module-extension-fields.test.tsis the PR #30362 harness: realHttpDispatcherdoors over a file-backed SQLite kernel. It has 20 cases.[#30379] the disable door refuses …(3 cases). The fixture has:script, a warningconditional, a switched-off rule, and an info rule on the app's visit that readsrecord.account.qx_credit;.custompair, throughPOST /packages/:app/ensure-customizationand the protocol's save and publish.The cases:
409withPACKAGE_DISABLE_RESTRICTED_BY_VALIDATION_RULE. The message reads4 active validation rules read fields …and lists exactly the four reading rules, in order (pair, two on the account, the hop on the visit), each with what it reads. The switched-off rule and the formula module are not named, anderror.detailsis absent. The row stays enabled, the fields stay served, a write still lands and the state file does not list the package.an active validation rule reads a field …). Once the pair is disabled, the disable proceeds (200), the fields are withheld, the formula answersnulland writes land.400 VALIDATION_FAILEDnaming the rule. That is the enable door, runtime: enabling a package whose validation rule reads a field of a disabled package is accepted, and every write to that object then answers 400 #30756; it flips this pin.[#30379 contract review] the scan judges the rules the served fold serves(2 cases). A third module extends the account at a later priority and redeclares two of the rules module's rules by name: one so it no longer reads the field, one so it does.409naming exactly 2 rules: the visit's hop, and the redeclared reader named after the override module. The rule whose winning copy no longer reads the field is not named, though the rules module's own copy still reads it.overlay) switches the override's reader off and declares its own reader. The refusal names the visit's hop and the overlay's own rule astenant overlay of com.qa.extsuite, and no(no package). Once overlays switch every served reader off (active: false), the disable answers200, the module's fields are withheld (the overlay's copies of them included) and a write lands.[#30379 contract review] POST /packages with enableOnInstall: false asks the same scan(3 cases, a realSchemaRegistryandHttpDispatcherover a stub kernel):enableOnInstall: falseof an enabled extender whose field another package's rule reads answers409with the code and the exact message; the package stays enabled, the state file does not list it and the field is still served;enableOnInstall: falseanswers201and the package is disabled, in the registry and in the state file;201and the package is disabled.The formula describe stays the CONTROL: a formula alone over the field ⇒ the disable proceeds (200) and writes land.
Run and ablation
All at head
c15ba57084(merge baseorigin/mainc17719dd6c), underscripts/pm/os-verify-lock.sh, after a whole-treepnpm turbo run build --concurrency=2at that head (119/119 tasks, VERDICT command-exit 0).Six ablations, each
node scripts/ablation-replace.mjs --file packages/runtime/src/domains/packages.ts --anchor A --replacement R -- (the same vitest run), all on the committed head under one lock (VERDICT command-exit 0, 116s). Each mutation landed (anchor 1 → 0), every one went red, and every restore left the file at HEAD's blobb1cec1748292(git diff HEADempty).readDisableDependentRules(registry, id);→ the same call with an always-false.filteractiveignoredrule.active === false ||from the served-rule filterruleLayerLabel(layerOf.get(rule.name) ?? base)→ruleLayerLabel(base)overwritesEnabledRow ? readDisableDependentRules(registry, pkgId) : [];→[]declaredRulesOf(served)→ every contributor'sdeclaredRulesOf(c.definition)overwritesEnabledRow ? readDisableDependentRules(registry, pkgId) : [];→readDisableDependentRules(registry, pkgId);A4 is the reading the contract review measured: judged per copy, the replaced rule and the overlay-switched rule both refuse again. A5 is the delta review's: scanned on a fresh install, the package lands enabled against
enableOnInstall: false.The second-object census row, before and after, with the tree built:
No
@objectstack/objectqlfile is touched (the install pins import itsSchemaRegistry); its suite is CI's.Premise, re-measured on
origin/main6ef7f583before the change: the disable answered 200 and the write after it 400VALIDATION_FAILED(12/12 of the then harness).Acceptance notes
400 VALIDATION_FAILEDnaming the rule. A boot replaying such a state file ends in the same state. Pinned as measured. The mirror is runtime: enabling a package whose validation rule reads a field of a disabled package is accepted, and every write to that object then answers 400 #30756 (pm:blocked behind this card).disableableis unchanged. The package row'sdisableable(the door's PACKAGE verdict, ADR-0070 / packages: the served package row carries no "may be disabled" verdict, so the Console cannot offer Disable on a suite module the server now accepts #28318) still answerstruefor such a module. The dependent rule is an environment-state conflict, answered by the409. Showing it on the Settings page is thedomain:uicard the seat files after this lands.enableOnInstall: false(only ever on an overwrite of an enabled package) does not roll the overwrite back: the artifact the request carried is installed and stays enabled, and the409says so. Undoing the install would be a second write the request did not ask for.cross_field.fieldsis advisory (per the spec, only its first entry labels the violation), so across_fieldrule is judged by itscondition./api/v1/cloud/environments/:env/packages/:pkg/disable(carrier: cloud) was not run here.Out of scope
error.detailsshape for the package · object · rule triples. The seat ruled (A) on carry-note item 2: no structured details, and nopackages/spec. If the Settings page ever needs the triples machine-readable, that is adomain:speccontract card first: acontracts/schema plus the ledger row comment, theACTION_CONFIRMATION_REQUIREDpattern. The runtime would stamp it after that card lands. The seat records this on the UI card.DELETE /packages/:idwithdraws the same extension fields with no scan. The seat files it as its own card.Verification
Local gates (dispatch-gates --commands on this diff)
At head
c15ba57084:node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 92 commands; each ran with its exit code captured before any pipe.node scripts/pm/dispatch-gates.mjs --ran: 92 derived, 92 run, 0 NOT-MEASURED, 0 UNRUN (exit 0).CI families outside the derivation (the artifact rosters, the wide-population families, the path-scheduled jobs and the type-check lanes) belong to CI; this PR's check-runs are their record.
Full dogfood battery (Clause-② carries the (narrowing) arm)
Head
c15ba57084. No failure.Human Minimum
packages/specholds no runtime logic. (not touched)pnpm lintpasses. (CI's repo-wide run)main, one pull request.packages/spectouched: its generated artifacts are regenerated and committed. (not touched)content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.Generated by Claude Code