Skip to content

fix(runtime)!: the package disable door refuses while an enabled package's validation rule reads a field the package adds - #30825

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-mwjmre
Nov 6, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-mwjmre

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

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) and content/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 for packages/: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 at c15ba57084

维护者速读

  • 改了什么:设置里停用一个模块时,如果停用后对象上仍然生效的某条校验规则读了这个模块加到别的对象上的字段,停用会被拒绝(409,错误码 PACKAGE_DISABLE_RESTRICTED_BY_VALIDATION_RULE),提示里逐条列出「包 · 对象 · 规则」,什么都不改。规则按停用后对象实际生效的样子判断:同名规则只判胜出的那一份,客户在自己那份对象上关掉的规则算关闭,规则归到它生效文本所在的那一层(客户覆写层写作 tenant overlay)。用 enableOnInstall: false 覆盖一个当前启用的包时同样判断:装上,但保持启用,答同一个 409;全新安装、删除后重装、覆盖已停用的包照旧装成停用。公式读这些字段不拦(停用后答空值)。
  • 为什么改:以前能停用,但停用后那条规则读不到字段,该对象的每一次写入都被拒(400 VALIDATION_FAILED),直到重新启用。这是 [Decision] A package can be disabled while an enabled package's validation rule reads one of its extension fields, and then every write to that object is refused #30379 裁决 A(你 2026-11-05 21:56Z 的「同意」);本版按合同复审 6677642547 和增量复审 6679292152 改过。
  • 风险与回滚:小。只多了一种拒绝,只在会把对象写死的停用上出现;读写接口、元数据和导出都不变。回滚即撤 PR。
  • 先看: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 before registry.disablePackage and setPackageDisabled, the door asks readDisableDependentRules(registry, id). If that is non-empty, it answers 409 with error.code PACKAGE_DISABLE_RESTRICTED_BY_VALIDATION_RULE (ledgered under @objectstack/runtime by 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 as package · object · rule (reads FIELD), one or several. Nothing changes on a refusal. The refusal carries no error.details beyond 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 extend contributions 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.dependencies entry is read), an app's BASE.custom pair included. They are read off the registry's own fold, getAllObjects(undefined, { withoutExtensionsOf: id }), never re-assembled in the door. So:

    • one rule per name, the copy the fold serves: a later layer's rule replaces an earlier one of its name (mergeValidationsByName), and only the winning copy is judged;
    • the tenant overlay's switch is honoured: its message and active sit on the packaged rule it names (tenantRuleSettingsOf / withPackagedOwnerRules), so a rule the tenant switched off through the overlay is off;
    • less the rules the disable takes off a base that carries them (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 extend contribution that stays) that declares the rule's name, which is the layer its served text comes from. A tenant overlay is named tenant overlay of PKG (or tenant overlay when it carries no package); the (no package) placeholder is gone (an unpackaged layer is unpackaged object / unpackaged extension).

  • What "reads" means:

    • A predicate is read with analyzeRelationshipTraversals (@objectstack/formula) over record and previous. That covers script / cross_field condition and a conditional's when, at any depth. It is the reading the rule validator itself asks before it refuses a write (readsRecordColumn).
    • A record.REF.FIELD read is also a hop into REF's target, judged with referenceTargetOf on the served field.
    • A state_machine / format / json_schema rule reads the field it names.
    • No second parser.
  • Not counted:

  • The code is held as its wire pair { code, status: 409 }, the shape check:error-status-conformance reads as a producer.

@objectstack/runtime: POST /api/v1/packages with enableOnInstall: false, on an overwrite of an enabled package (contract review should-fix, delta review must-fix 3)

  • Before either install limb writes, the door reads whether the id's row exists and is enabled (registry.getPackage(pkgId)?.enabled === true). Only then can the install's enableOnInstall: false arm withhold a field a rule is served against, so only then does it ask the same readDisableDependentRules(registry, pkgId) before it disables.
  • When a rule would read a field the disable withholds, the overwrite still lands, the package stays enabled on the row, in the registry and in the state file (all three follow the row the door returns), and the door answers the same 409 and code, with the message lead Package 'ID' was installed and stays enabled: enableOnInstall: false cannot disable it, instead of a 201 that claims a disable.
  • A fresh install (or a reinstall after DELETE /packages/:id) and an overwrite of a disabled package withhold nothing served, so they honour enableOnInstall: false exactly 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)

  • The scan follows a rule's record.REF.FIELD hop with referenceTargetOf, which makes packages/runtime/src/domains/packages.ts a new caller of the reference-target arbiter. The census names it not-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 on d315efca97.)

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.ts pins it against the wire face). A ### PACKAGE_DISABLE_RESTRICTED_BY_VALIDATION_RULE entry 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 and no_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 only 422 WRITABLE_PACKAGE_REQUIRED.
  • content/docs/api/metadata-api.mdx: the PATCH /packages/:id/disable entry names the refusal, the served-fold judgment, its remedy and the install arm on an overwrite of an enabled package.

Changeset: @objectstack/runtime patch, 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: none and the ADR-0087 disposition not-required (no-migration-prescription).

Tests

packages/runtime/src/domains/packages-disable-module-extension-fields.test.ts is the PR #30362 harness: real HttpDispatcher doors over a file-backed SQLite kernel. It has 20 cases.

  • [#30379] the disable door refuses … (3 cases). The fixture has:

    • a rules module with an error script, a warning conditional, a switched-off rule, and an info rule on the app's visit that reads record.account.qx_credit;
    • a formula module;
    • a tenant rule saved and published into the app's .custom pair, through POST /packages/:app/ensure-customization and the protocol's save and publish.

    The cases:

    • Disable: 409 with PACKAGE_DISABLE_RESTRICTED_BY_VALIDATION_RULE. The message reads 4 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, and error.details is absent. The row stays enabled, the fields stay served, a write still lands and the state file does not list the package.
    • Reading module disabled first: the refusal names only the pair's rule (an active validation rule reads a field …). Once the pair is disabled, the disable proceeds (200), the fields are withheld, the formula answers null and writes land.
    • Measured, not ruled: re-enabling the reading module while the field stays withheld refuses writes with 400 VALIDATION_FAILED naming 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.

    • Replaced by name (pin b): 409 naming 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.
    • Tenant overlay (pin a): a tenant overlay of the account (registered as 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 as tenant overlay of com.qa.extsuite, and no (no package). Once overlays switch every served reader off (active: false), the disable answers 200, 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 real SchemaRegistry and HttpDispatcher over a stub kernel):

    • an overwrite with enableOnInstall: false of an enabled extender whose field another package's rule reads answers 409 with the code and the exact message; the package stays enabled, the state file does not list it and the field is still served;
    • fresh install (delta review must-fix 3): with no row before the install and the same reading rule registered, enableOnInstall: false answers 201 and the package is disabled, in the registry and in the state file;
    • control: with no rule reading the field, the overwrite answers 201 and 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 base origin/main c17719dd6c), under scripts/pm/os-verify-lock.sh, after a whole-tree pnpm turbo run build --concurrency=2 at that head (119/119 tasks, VERDICT command-exit 0).

pnpm --filter @objectstack/runtime exec vitest run src/domains/packages-disable-module-extension-fields.test.ts
 Test Files  1 passed (1)
      Tests  20 passed (20)
os-verify-lock: VERDICT command-exit 0 · held the lock 19s

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 blob b1cec1748292 (git diff HEAD empty).

Ablation Anchor → replacement Red
A0 the disable door's scan readDisableDependentRules(registry, id); → the same call with an always-false .filter 4 failed / 20
A1 active ignored drop rule.active === false || from the served-rule filter 3 failed / 20
A2 named after the base ruleLayerLabel(layerOf.get(rule.name) ?? base) → ruleLayerLabel(base) 6 failed / 20
A3 the install arm's scan overwritesEnabledRow ? readDisableDependentRules(registry, pkgId) : []; → [] 1 failed / 20 (the overwrite-of-enabled pin)
A4 every contributor's copy judged declaredRulesOf(served) → every contributor's declaredRulesOf(c.definition) 2 failed / 20
A5 the scan on every install overwritesEnabledRow ? readDisableDependentRules(registry, pkgId) : []; → readDisableDependentRules(registry, pkgId); 1 failed / 20 (the fresh-install pin)

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:

pnpm --filter @objectstack/core run test:repo      (at d315efca97, without the row)
     × every caller of the reference-target arbiter is classified, and every classified file still calls it
       - packages/runtime/src/domains/packages.ts
 Test Files  1 failed | 5 passed (6)
      Tests  1 failed | 93 passed (94)

pnpm --filter @objectstack/core run test:repo      (at c15ba57084)
 Test Files  6 passed (6)
      Tests  94 passed (94)
os-verify-lock: VERDICT command-exit 0 · held the lock 6s
pnpm --filter @objectstack/core test
 Test Files  136 passed (136)
      Tests  3131 passed (3131)
os-verify-lock: VERDICT command-exit 0 · held the lock 36s

pnpm --filter @objectstack/spec run test:repo
 Test Files  90 passed (90)
      Tests  1539 passed (1539)
os-verify-lock: VERDICT command-exit 0 · held the lock 425s (7m05s)

pnpm --filter @objectstack/runtime test
 Test Files  547 passed (547)
      Tests  8768 passed | 19 skipped (8787)
os-verify-lock: VERDICT command-exit 0 · held the lock 675s (11m15s)

pnpm --filter @objectstack/runtime typecheck      (tsc --noEmit && check:test-typecheck)
os-verify-lock: VERDICT command-exit 0 · held the lock 31s

pnpm --filter @objectstack/core typecheck         (tsc --noEmit, the examples project, check:test-typecheck)
os-verify-lock: VERDICT command-exit 0 · held the lock 21s

No @objectstack/objectql file is touched (the install pins import its SchemaRegistry); its suite is CI's.

Premise, re-measured on origin/main 6ef7f583 before the change: the disable answered 200 and the write after it 400 VALIDATION_FAILED (12/12 of the then harness).

Acceptance notes

Out of scope

Verification

Local gates (dispatch-gates --commands on this diff)

At head c15ba57084: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 92 commands; each ran with its exit code captured before any pipe.

node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
node scripts/check-adr-links.mjs :: exit 0
node scripts/check-adr-links.mjs --self-test :: exit 0
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-adoption.mjs :: exit 0
node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-dev-prereqs.mjs --self-test :: exit 0
node scripts/check-doc-route-spelling.mjs --advisory :: exit 0
node scripts/check-doc-route-spelling.mjs --self-test :: exit 0
node scripts/check-docs-section-name.mjs :: exit 0
node scripts/check-docs-section-name.mjs --self-test :: exit 0
node scripts/check-dts-emitted.mjs --self-test :: exit 0
node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
node scripts/check-empty-changeset.mjs --self-test :: exit 0
node scripts/check-issue-citations.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0
node scripts/check-section-landing-index.mjs :: exit 0
node scripts/check-section-landing-index.mjs --self-test :: exit 0
node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0
node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0
node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0
node scripts/docs-audit/check-affected-docs.mjs :: exit 0
node scripts/docs-audit/check-drift-comment.mjs :: exit 0
node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0
node scripts/release-pending-publish.mjs --self-test :: exit 0
pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0
pnpm --filter @objectstack/lint run check:doc-security-posture :: exit 0
pnpm --filter @objectstack/spec run check:docs :: exit 0
pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
pnpm --filter @objectstack/spec run check:empty-state :: exit 0
pnpm --filter @objectstack/spec run check:liveness :: exit 0
pnpm --filter @objectstack/spec run check:skill-examples :: exit 0
pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0
pnpm --filter @objectstack/spec run check:variant-docs :: exit 0
pnpm --filter @objectstack/spec run check:yaml-examples :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:ci-filter-parity :: exit 0
pnpm check:closed-ceremony-text :: exit 0
pnpm check:corpus-claim-drift :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:dispatcher-error-vocabulary :: exit 0
pnpm check:doc-anchors :: exit 0
pnpm check:doc-authoring :: exit 0
pnpm check:doc-frontmatter :: exit 0
pnpm check:docs-audit-scope :: exit 0
pnpm check:docs-redirects :: exit 0
pnpm check:docs-single-h1 :: exit 0
pnpm check:docs-spec-enumerations :: exit 0
pnpm check:docs-transcript-drift :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:dts-closure :: exit 0
pnpm check:dual-build-cjs-loads :: exit 0
pnpm check:durability-log-level :: exit 0
pnpm check:engine-double-contract :: exit 0
pnpm check:error-status-conformance :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:issue-citations :: exit 0
pnpm check:kernel-hook-pairs :: exit 0
pnpm check:keyed-text-bounds :: exit 0
pnpm check:lean-entry-closure :: exit 0
pnpm check:logger-receiver-detach :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:objectql-double-limit :: exit 0
pnpm check:org-identifier :: exit 0
pnpm check:page-declaration-shape :: exit 0
pnpm check:platform-object-tenancy-census :: exit 0
pnpm check:published-files :: exit 0
pnpm check:published-readme-links :: exit 0
pnpm check:query-options-erasure :: exit 0
pnpm check:react-page-adapter-contract :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:registry-log-declared :: exit 0
pnpm check:rest-log-spy-declared :: exit 0
pnpm check:role-word :: exit 0
pnpm check:route-envelope :: exit 0
pnpm check:skill-identifier-liveness :: exit 0
pnpm check:slot-lookup :: exit 0
pnpm check:sourcemap-no-sources-content :: exit 0
pnpm check:system-context-census :: exit 0
pnpm check:test-source-alias :: exit 0
pnpm check:tier-file-adoption :: exit 0
pnpm check:type-check-coverage :: exit 0
pnpm check:type-check-debt :: exit 0
pnpm check:undeclared-dep-imports :: exit 0
pnpm check:vendor-version-stamps :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0

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)
pnpm --filter @objectstack/dogfood test      (whole-tree build first; no OS_TEST_SHARD, no path)
 Test Files  423 passed | 3 skipped (426)
      Tests  3533 passed | 41 skipped (3574)
os-verify-lock: VERDICT command-exit 0 · held the lock 1558s (25m58s)

Head c15ba57084. No failure.

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.
  • 3. No formatter ran; pnpm lint passes. (CI's repo-wide run)
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed. (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.

Generated by Claude Code

…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>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 6, 2026
@github-actions

github-actions Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

25 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 555eb1ba18c9738e5831b7844822b08fda7c70ac.

⛔ 7 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 cross-cutting symbol(s) contributed no route anchor: packageId (4 routes)
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 63 of 222 client-bound route-ledger rows — the other 159 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 159: 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; 104 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 — 28 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 555eb1ba18c9738e5831b7844822b08fda7c70ac → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 08d974c5d8317b2edb53f3f78623fd48efcf2d0f — the merge of head c15ba57084196643bacd7b42344bb95563a09dcf into base 555eb1ba18c9738e5831b7844822b08fda7c70ac, 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 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

⚠️ 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 555eb1ba18c9738e5831b7844822b08fda7c70ac → 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

domain:server#4 · os-elon-musk · session_016WV4czvZykUqhYxYXSSDt4

Contract review for card #30379 · narrowing face (Clause-②: no (narrowing)) · read-only reviewer at the contract-review tier, brief per record-recognisers --brief-template --face narrowing · 2026-11-06T05:21Z

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: a154e1ba1b6ce5f9552f567b62088bf0e4360e78
Local-runs: none
Scope: packages/runtime/src/domains/packages.ts, packages/runtime/src/domains/packages-disable-module-extension-fields.test.ts + packages/objectql/src/registry.ts (withoutWithheldMembers, withholdDisabledExtensions, resolveObject, mergeValidationsByName, tenantRuleSettingsOf, withPackagedOwnerRules, tenantRuleCopyRefusal), packages/objectql/src/validation/rule-validator.ts, packages/formula/src/relationship-traversal.ts, packages/spec/src/data/field-value.zod.ts (referenceTargetOf), packages/spec/src/data/validation.zod.ts, packages/spec/src/api/error-code-ledger.zod.ts, packages/spec/src/api/error-catalog-docs.test.ts, packages/runtime/src/http-dispatcher.ts, docs/adr/0141-inter-app-dependency-declared-checked-before-install.md (prose read, not reviewed: the changeset, content/docs/api/error-catalog.mdx, content/docs/api/metadata-api.mdx, content/docs/deployment/marketplace-handbook.mdx)

① Derived judgments

  • Placement — right. The scan runs after the permission check, the catalog wall, the query refusal and the read-only check, and before registry.disablePackage and setPackageDisabled. It is reads only. The segment from the last await through setPackageDisabled is synchronous, so no state moves before the refusal.

  • Code and envelope — right. 409 with the ledgered PACKAGE_DISABLE_RESTRICTED_BY_VALIDATION_RULE (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). There is no error.details, per the seat's message-only ruling.

  • Who is scanned — right on the axes ruling A names.

    • Every severity counts; only active is read.
    • Formulas are not refused (the control).
    • Every enabled package counts, declared dependent or not.
    • An app's .custom pair is pinned.
    • The read mirrors the validator's readsRecordColumn (record and previous, bare fields, traversals, multi-hop). A hop is judged with referenceTargetOf, and the validator fails closed on an undeclared related column.
  • Correctly not refused:

    • the disabled package's own rule on its own object;
    • a rule of a disabled package, or on an object whose owner is disabled;
    • a rule that reads the field through a formula;
    • a package that is already disabled.
  • Over-refused — WRONG (must-fix 1). The scan judges every contributor's copy of a rule, but the served fold serves one rule per name.

    • A later layer's copy replaces an earlier one (mergeValidationsByName).
    • A tenant overlay sets the packaged rule's message and active (tenantRuleSettingsOf / withPackagedOwnerRules). That is a declared capability (ADR-0029 D9.2a), and tenantRuleCopyRefusal admits exactly those two keys.

    Three consequences:

    • (a) A packaged extension rule that the tenant switched off through the overlay is still counted. That switch is the only one a tenant admin has on a read-only module. The door then refuses a disable after which no served rule reads the field, and its remedy names a switch that is already thrown.
    • (b) A rule that a later layer replaced by name is still judged from the earlier copy.
    • (c) An overlay seeded from a served read lists the same rule twice.

    None of this is bound by A, and the PR's own contract excludes it.

  • Under-refused — should-fix. POST /packages with enableOnInstall: false on an overwrite reaches registry.disablePackage with no scan: the same end state by another verb.

② Semver level

  • @objectstack/runtime: patch with the BREAKING banner. Clause-②: no (narrowing) and Metadata-protocol: none in both carriers. One ADR-0087 marker (not-required (no-migration-prescription)). Title fix(runtime)!:. Right.
  • The docs rows match the code, except that their remedy sentence promises the tenant switch that must-fix 1 does not honour.
  • Must-fix 2. content/docs/api/error-catalog.mdx L7 still says "57 error codes reachable on the wire". The PR adds the 58th wire-face heading. packages/spec/src/api/error-catalog-docs.test.ts ("the advertised code count matches the wire face") therefore reds Test Core (2/8) on this head (expected 57 to be 58).

③ Boundary flags

④ Design judgment

The narrowing makes ruling A real at the door:

  • the refusal sits before any state moves;
  • the reader is the platform's one member reader and one reference arbiter;
  • the withheld-field reading is the registry's own;
  • the message names package · object · rule.

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 active honoured, attributed to the last contributing layer), with pins for an overlay active: false ⇒ 200 and a by-name replacement judged once. Must-fix 2 is the red required check. Nits: the message says (no package) for an overlay rule; getAllObjects() is used only to list names.

Check-runs at review time: 32 success, 12 skipped, 1 failure (Test Core 2/8, the catalog count), 4 in progress.

Implemented-by: claude/project-thread-mwjmre
Reviewed-by: session_016WV4czvZykUqhYxYXSSDt4

VERDICT: FAIL

…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>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#4 · os-elon-musk · session_016WV4czvZykUqhYxYXSSDt4

Contract review for card #30379 · narrowing face (Clause-②: no (narrowing)) · delta review over record 6677642547 (FAIL on a154e1ba1b) · read-only reviewer at the contract-review tier · 2026-11-06T07:06Z

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: d315efca9756b74d7593a1657229c260b3207e0a
Local-runs: none
Scope: packages/runtime/src/domains/packages.ts, packages/runtime/src/domains/packages-disable-module-extension-fields.test.ts + packages/objectql/src/registry.ts (withoutWithheldMembers, withholdDisabledExtensions, resolveObject, resolveObjectLeavingExtensionsOut, foldExtendersOntoDefinition, mergeValidationsByName, tenantRuleSettingsOf, withPackagedOwnerRules, tenantRuleCopyRefusal, getAllObjects, registerObject's priority sort), packages/objectql/src/validation/rule-validator.ts (readsRecordColumn), packages/spec/src/api/error-catalog-docs.test.ts (prose read, not reviewed: the changeset, content/docs/api/error-catalog.mdx, content/docs/api/metadata-api.mdx, content/docs/deployment/marketplace-handbook.mdx). Judged: the fix commit d315efca and the net diff against main (six files); the merge of main c17719dd (356e54ef) is not reviewed. The head's registry.ts is main's at c17719dd; every cited function reads the same there as on the local tree.

① Derived judgments

  • Must-fix 1 (served-fold judging) — CLOSED. readDisableDependentRules now takes each object from getAllObjects(undefined, { withoutExtensionsOf: id }), the registry's own fold with this package's extend contributions left out, and judges declaredRulesOf(served). Read against the cited code:
    • resolveObjectLeavingExtensionsOut keeps the base overlay ?? own, folds the kept extenders in the registered (priority-sorted) order and withholds the already-disabled packages' members. So the served list is one rule per name (mergeValidationsByName), and a tenant base's copy carries the tenant's message and active on the packaged rule (withPackagedOwnerRules for the owner's rules, withTenantRuleSettings inside the merge for an extension's). The scan reads active === false off that copy. Honoured.
    • The one step a left-out fold does not take is the disable's own withoutWithheldMembers over this package's members. The scan mirrors it: withheldRules is every leaving extension's rule name not declared by the owner or a kept extension, exactly withholdDisabledExtensions' set once the disable lands, and those names are skipped. withheldFields mirrors it the same way (the owner's own fields plus the kept extensions' count as kept; the overlay's copies do not, as in the registry).
    • Attribution: layerOf walks the base then the kept extend contributions in getObjectContributors order, which registerObject sorts by priority on every registration, the order foldExtenders folds in. Last declaring layer wins, matching the copy the merge serves. ruleLayerLabel names an overlay tenant overlay of PKG / tenant overlay; the (no package) placeholder is gone.
    • Break attempts, each read through the fold: an overlay active: true over a packaged active: false serves the rule on, the scan refuses — right, the write path would run it. An overlay carrying only message leaves active the package's — judged as served. A later layer's copy that stops reading the field while the earlier copy reads it is not judged (pinned, the override's qx_credit_not_negative); the reverse is judged once and named after the later layer (pinned, qx_credit_review). A rule of the disabled package itself, carried as a copy on the overlay, is in withheldRules and skipped, as the fold will strip it; one that a kept extension also declares is kept by name and judged from the kept copy. A packaged rule the tenant switched off through the overlay no longer refuses, and once every served reader is off the disable answers 200 with the overlay's copies of the fields withheld too (pinned). Nothing found that the served fold would serve and the scan judges differently.
    • Residual nits, not findings: an OWNER rule the tenant copied into the overlay is labelled tenant overlay of PKG though its condition is the owner's (the switch lives on the overlay, so the remedy still points right); declaredFieldNamesOf filters field values to plain records where the registry's fieldNamesOf takes every key, a divergence only on a degenerate body.
  • Must-fix 2 (catalog count) — CLOSED. content/docs/api/error-catalog.mdx L7 reads 58 error codes reachable on the wire. The head page carries 59 ### code headings of which two are the one INVALID_REQUEST spelling, 58 distinct, one more than before (the new 409 entry). The pin compares the claim with face.wireCodes.length; the dev reports pnpm --filter @objectstack/spec run test:repo green at this head (1539/1539). CI's Test Core is still in progress (below).
  • Should-fix (install arm) — ADDRESSED, with a new finding. POST /packages with enableOnInstall: false asks the same scan before disablePackage; with a reader the row, the registry and the state file all follow pkg.enabled (true), and the door answers the same 409 and code with the lead Package 'ID' was installed and stays enabled. One pin and one control. Read on ④ below.
  • Placement and envelope — unchanged and right. The disable door asks the scan after isDisableablePackage / requireWritablePackage and before disablePackage and setPackageDisabled; 409 with the ledgered code, no error.details; one wire pair for both doors (check:error-status-conformance reads it).
  • Over-reach (must-fix 3). The install arm's scan fires on EVERY enableOnInstall: false install, a fresh install included (the arm reads only request.enableOnInstall; overwrite and the pre-install row are not consulted). On a fresh install of a package whose field an enabled rule already names (a module reinstalled after DELETE /packages/:id while a .custom rule or another package's rule still reads its field — writes already fail closed there, since Script validation rules are silently skipped when their predicate fails to evaluate — fail-open is the wrong direction for a validation #4649), the package lands ENABLED against the caller's explicit false, its hooks, flows and navigation live, and the door says it "stays enabled" — it was never enabled. Nothing served is withheld in that case, so ruling A does not reach it, and the ruled install contract ([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 「有旗 = 设置」: false ⇒ disablePackage) is narrowed on a path the PR body and changeset describe only as "an overwrite of an enabled package that asks to come back disabled". Fix: gate the arm on the pre-install row being enabled (the registry.getPackage(pkgId) read before the install is at hand), so a fresh install and an overwrite of a disabled package honour false as before; and pin a fresh install with a reader answering 201 disabled. Alternatively, if the seat wants the fresh case refused too, the lead, the body, the changeset and the three docs must say so, and the pin must hold it — but that is a narrowing of [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 beyond A and wants its own ruling.

② Semver level

  • @objectstack/runtime: patch with the BREAKING banner, Clause-②: no (narrowing) and Metadata-protocol: none in the body and the changeset, one ADR-0087 marker (not-required (no-migration-prescription)), title fix(runtime)!:, model-free trailer pair on the fix commit. Right.
  • Body, changeset and docs agree with the code on the served-fold judgment, the layer naming, the overlay's switch as a remedy, and the overwrite case of the install arm. They disagree with the code on the install arm's reach (must-fix 3): the changeset's "The install door too" bullet and the body's install section describe the overwrite case; the catalog, metadata-api and handbook say "the package stays enabled", which is false on the fresh-install path the code also takes.

③ Boundary flags

④ Design judgment

  • The served-fold scan makes A real exactly where the prior record found it broken: the door now refuses only a disable after which a rule the fold would run reads a withheld field, the tenant's one switch on a read-only module is honoured, and the message names the layer the admin must act on. The reader stays the platform's one member reader and reference arbiter, the fold is the registry's own, not a second copy. Five ablations each red, including the per-copy reading the prior record measured.
  • The install arm's 409 after a landed write, judged against the door's contract and the docs: on an overwrite of an enabled package it is coherent. The end state is what a plain overwrite leaves, nothing goes live that was not live, only the disable half is refused, the lead sentence says what landed, the catalog's entry names both doors, and a pin holds the row, the state file and the served field. The remaining tension — one code meaning "nothing changed" on one door and "the install landed" on the other — is carried by the lead and the catalog text, and no_retry fits. Undoing the install would be a second write the request did not ask for; refusing before the install would need the scan asked of an artifact not yet registered. Acceptable as documented. On a fresh install it is a half-state the docs do not describe and the ruling does not cover: must-fix 3.

Check-runs at review time on d315efca: 19 success, 14 skipped, 18 in progress (Test Core 1–8, Build Core, Build Docs, the Lint and Type Check lanes, Check Changeset), 0 failure. The gate verdicts are not yet in.

Implemented-by: claude/project-thread-mwjmre
Reviewed-by: session_016WV4czvZykUqhYxYXSSDt4

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>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#4 · os-elon-musk · session_016WV4czvZykUqhYxYXSSDt4

Flake judgment · 2026-11-06T08:42Z · run 42186037268, Test Core (6/8) on head c15ba570. The single failure is the @objectstack/rest case for #30675 record-currency, in meta-write-door-capability-enumeration.test.ts. It hit a 5000 ms timeout while that shard's imports took 1379 s. The case comes from main, @objectstack/rest does not depend on @objectstack/runtime, and this diff touches no rest file. Every other shard is green. The seat judges this a load timeout, not this PR, and re-runs the failed jobs once through rerun-failed.mjs. A second failure is real.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#4 · os-elon-musk · session_016WV4czvZykUqhYxYXSSDt4

Contract review for card #30379 · narrowing face (Clause-②: no (narrowing)) · delta review over record 6679292152 (FAIL on d315efca97, must-fix 3) · read-only reviewer at the contract-review tier · 2026-11-06T08:52Z

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: c15ba57084196643bacd7b42344bb95563a09dcf
Local-runs: none
Scope: packages/runtime/src/domains/packages.ts, packages/runtime/src/domains/packages-disable-module-extension-fields.test.ts, packages/core/src/security/second-object-read-exposure.pin.test.ts + packages/objectql/src/registry.ts (getPackage, installPackage's lifecycle carry-over, enablePackage, disablePackage, isPackageDisabled, getAllObjects with withoutExtensionsOf), packages/metadata-protocol/src/protocol-package-install.ts (the protocol limb stores the row and registers no contribution), packages/spec referenceTargetOf through the census (prose read, not reviewed: the changeset, content/docs/api/error-catalog.mdx, content/docs/api/metadata-api.mdx, content/docs/deployment/marketplace-handbook.mdx). Judged: the one delta commit c15ba57084 (7 files, +53/-16) over d315efca97 and the net diff against main (seven files). The merge with main the CI run tests (555eb1ba) is not reviewed. The head's registry.ts is main's; every cited function reads the same there as on the local tree.

① Derived judgments

  • Must-fix 3 (install arm over-reach) — CLOSED. overwritesEnabledRow = registry.getPackage(pkgId)?.enabled === true is read at L2071, after the 409 for an existing id without overwrite, the read-only gate and the pair refusal, and BEFORE await resolveProtocol and either install limb. The enableOnInstall: false arm asks readDisableDependentRules only when it is true (L2203); otherwise disablePackage runs 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. Read against the registry:
    • Missing row (fresh install, reinstall after DELETE /packages/:id): getPackage answers undefined, the guard is false, the scan is not asked, installPackage stamps enabled: !seeded, disablePackage flips it: 201, disabled, the state file lists it. Disabled row (enabled === false, and a row isPackageDisabled reads off status === 'disabled'): === true fails on both spellings, the scan is skipped, installPackage carries the row's lifecycle, disablePackage keeps it off. Enabled row with overwrite: scanned, and on a reader the row, registry and state file stay enabled, 409 with the code and the lead. An enabled row without overwrite never reaches the guard (the 409 at L2019 comes first).
    • What the scan sees. It runs after the limb, over the registry as it stands then: the row installPackage just wrote with the pre-install lifecycle carried, and the contributions the registry holds (neither limb registers a manifest's objectExtensions; the loader does). That is the state PATCH /packages/:id/disable asked next would judge, so the two doors cannot disagree on the same registry. A new artifact that drops a field a rule reads is already unserved by the overwrite itself, the same as a plain overwrite, and the scan rightly does not name it.
    • Concurrent install. The guard is a pre-read across the limb's awaits. Interleaving that enables the row between the read and the write (an enable door call, a second install with true) leaves this request's scan skipped and its disable landing; the window is the one the door's !overwrite 409 and read-only gate already carry on the same in-memory registry, and the end state it can reach is the one the enable door accepts today (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). Residual, not a finding; a tighter spelling exists if the seat wants it (existedBefore && pkg.enabled === true read off the row the limb returned).
    • Pins. The fresh-install pin (no row, a reader registered, enableOnInstall: false ⇒ 201, enabled: false, listed in the state file) and the overwrite-of-enabled control (409, the exact message, the row enabled, the state file clear, the field served) both exist, with the no-reader control (201, disabled). Ablation A5 (guard removed) reds the fresh pin only; A3 (scan removed) reds the overwrite pin only: the two directions are held apart. Nit: the fresh fixture registers the extend contribution before the row exists, and a rowless contribution is served, so the comment "nothing served is withheld" overstates the fixture; the contract it pins is the right one and the production path has no contribution ahead of the row.
  • Must-fix 1 (served-fold judging) — stays CLOSED. The delta touches no line of readDisableDependentRules or its helpers; the fold judgment, withheldRules mirror, layerOf attribution and ruleLayerLabel read as record 6679292152 judged them.
  • Must-fix 2 (catalog count) — stays CLOSED. L7 reads 58 error codes reachable on the wire; the head page carries 58 distinct code headings.
  • Docs, changeset, body — CONSISTENT. The changeset's Clause-② line, BREAKING banner, "The install door too, on an overwrite of an enabled package" bullet and "Who moves" bullet; the catalog's 409 entry; the metadata-api entry; the handbook's Disabling-a-module list; and the v3 body (header line, 维护者速读, the install section, the Docs section, the acceptance notes, Out of scope) all say the refusal is an overwrite of an enabled package with enableOnInstall: false, that the overwrite lands and the package stays enabled, and that a fresh install (the changeset and body add: a reinstall after a delete, an overwrite of a disabled package) lands disabled as before. No sentence left says the install arm fires on every enableOnInstall: false. The ADR-0087 marker, Metadata-protocol: none and the trailer pair are unchanged.
  • Census row — HONEST. The file's own classes: decided (a read that asks the target's exposure), executes, open (a read that does not ask yet), and not-a-served-read ("something that serves no field of the target, with the reason"). The scan's one arbiter call, referenceTargetOf(servedFields[ref]), turns a reference field of the rule's own object into a target name, then reads withheldFields.get(target), a set of DECLARED field names built from getObjectContributors; no record, no driver, no engine read, and the only thing that leaves is a 409 naming REF.FIELD to a manage_metadata caller. The sibling rows it sits beside are the same shape: no-operator-object-door.ts (names the related object in a refusal's wording; reads no record) and credential-evaluation-guard.ts / internal-field-evaluation-guard.ts (read the target's declared fields from the registry, refuse, read no record). The reason text matches the code; its "also asked by the install door's enableOnInstall: false arm" is still true, now on an overwrite of an enabled row only.
  • Placement and envelope — unchanged and right. Disable door: scan after the read-only check, before disablePackage and setPackageDisabled, synchronous between the two; 409 with the ledgered code, no error.details; one wire pair for both doors.

② Semver level

  • @objectstack/runtime: patch with the BREAKING banner, Clause-②: no (narrowing) and Metadata-protocol: none in the body and the changeset, one ADR-0087 marker (not-required (no-migration-prescription)), title fix(runtime)!:, model-free trailer pair on the delta commit. Right.
  • Body, changeset and docs now agree with the code on every arm: the served-fold judgment, the layer naming, the overlay's switch as a remedy, the install arm's reach (an overwrite of an enabled package only) and its end state (the overwrite lands, the row stays enabled), and the fresh-install, reinstall and disabled-overwrite paths that honour false.

③ Boundary flags

④ Design judgment

  • The served-fold scan makes A real where it was broken, as the prior record found; the delta changes none of it.
  • The install arm now narrows exactly the case the docs describe: an overwrite of an enabled package that asks to come back disabled, where the only thing the disable half would do is withhold a field a served rule reads. The pre-install read is the one fact that tells an overwrite of an enabled row from a fresh install, since installPackage carries the row's lifecycle on an overwrite and stamps it on a fresh row; reading it after the limb could not tell the two apart. The 409 after a landed overwrite stands as record 6679292152 judged it: coherent on this one path, the lead says what landed, no_retry fits, and undoing the overwrite would be a second write. [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's 「有旗 = 设置」 holds unchanged on every other path.
  • Six ablations each red, the two install-arm ablations reddening disjoint pins.

Check-runs at review time on c15ba57084: 57 success, 10 skipped, 0 failure, 1 in progress: Test Core (6/8), run attempt 2 (started 2026-11-06). Attempt 1 of that job completed failure, and the Test Core rollup with it, on @objectstack/rest src/meta-write-door-capability-enumeration.test.ts (the #30675 control case, timed out at 5000ms); @objectstack/rest is not in this diff and does not depend on @objectstack/runtime; the seat judged it a flake and re-ran it once. The gate verdict for that shard is the re-run's, not yet in; every other check on the head is green. Reported as read, not adopted.

Implemented-by: claude/project-thread-mwjmre
Reviewed-by: session_016WV4czvZykUqhYxYXSSDt4

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#4 · os-elon-musk · session_016WV4czvZykUqhYxYXSSDt4

维护者速读

  1. 改了什么:停用一个包时,如果某个仍启用的包有一条正在生效的校验规则读到这个包加的字段,停用会被拒绝,返回 409 PACKAGE_DISABLE_RESTRICTED_BY_VALIDATION_RULE,提示里写明是哪个包、哪个对象、哪条规则。判断按停用后实际生效的规则来算:同名规则只看胜出的那份,租户在覆盖层里关掉的规则不算。用 POST /packages 覆盖安装一个已启用的包、并要求 enableOnInstall: false 时,也做同样的检查,结果是安装落地、包保持启用、返回 409。新装和重装照旧按 false 处理。
  2. 为什么改:[Decision] A package can be disabled while an enabled package's validation rule reads one of its extension fields, and then every write to that object is refused #30379 裁定 A(6670643454)。以前停用后,那条规则会让该对象的所有写入失败。
  3. 风险:中。这是破坏性改动:以前能停用的包,在上面这种情况下现在会被拒绝。处理办法是先关掉或改掉那条规则,租户可以直接在覆盖层里关。
  4. 先看:packages/runtime/src/domains/packages.ts 的 readDisableDependentRules 和安装入口的 overwritesEnabledRow。
  5. 两分钟验证:pnpm --filter @objectstack/runtime exec vitest run src/domains/packages-disable-module-extension-fields.test.ts,预期 20 条全过。
  6. 需要你做的:在席位线程的决策卡上点头(破坏性)。点头后由席位入队落地。不属 Tier H,869 行 ≤ 3000。

席位意见:ACCEPT(#30379,裁定 A)。

席位核对

Head-sha: c15ba57084196643bacd7b42344bb95563a09dcf · read 2026-11-06T08:55Z

Item Reading
Shape Draft → main, Fixes #30379, title fix(runtime)!:. 7 files, +843/−26. Branch claude/project-thread-mwjmre; the whole claim was re-posted with the measured surface (core census row and three docs pages).
Seat diff read The disable door asks readDisableDependentRules after the read-only check and before disablePackage. The scan reads getAllObjects(undefined, { withoutExtensionsOf: id }), the registry's own post-disable fold, and mirrors the withheld-member strip. It judges one rule per name, honours the overlay's active, and names the last declaring layer. The install door reads registry.getPackage(pkgId)?.enabled === true before either limb writes, and scans only on an overwrite of an enabled row. The core census gains one not-a-served-read row for the scan's referenceTargetOf hop.
Evidence os-dev-report 6680811958: harness 20/20, six ablations each red, core test:repo 94/94, spec test:repo 1539, runtime 8768, typecheck 0, gates 92/92, dogfood 423 files. Three contract-review rounds: FAIL 6677642547 (served fold, catalog count), FAIL 6679292152 (fresh-install over-reach), and PASS 6681034355 on this head with no must-fix.
Gates (check-runs on this head) 59 success, 10 skipped, 0 failed (latest attempts). Test Core (6/8) failed once on an @objectstack/rest timeout outside this diff; the seat recorded it as a flake (6680871126) and re-ran it once, and attempt 2 passed. mergeable_state clean.
Accepted deviations The core census row (gate-forced; referenceTargetOf callers must be classified).
Out of scope The enable door (#30756, unblocked after this lands). The cloud disable route, the Settings page disableable verdict (domain:ui card) and the uninstall door (DELETE /packages/:id withdraws fields without a scan) each get a follow-up card from the seat after landing.
Semver @objectstack/runtime: patch. Clause-②: no (narrowing), Metadata-protocol: none, BREAKING banner, ADR-0087 not-required (no-migration-prescription).
Landing Not governed (check-governed-merges --pr 30825: NOT governed), 869 lines ≤ 3000, BREAKING. ⛔ Not queued until the maintainer gives the per-PR word on the decision card. After that, the seat lands it with pr_ready + automerge_enable on this head.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

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 c15ba570 (card #30379: the disable door refuses while another package's active validation rule reads a field it would withdraw). CI is green on that head and the seat ACCEPT stands, so the seat lands it now with pr_ready + automerge_enable.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 6, 2026 15:03
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 6, 2026
@github-actions

github-actions Bot commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 42239876496 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

分类:failure —— 按下面的日志分诊。

失败的 job(日志抽取,best effort):

  • Test Core (2/8) — 失败步骤: Run this shard's tests

    @objectstack/platform-objects:test:  FAIL  src/apps/translations/app-nav-translation-parity.test.ts > every nav id an app presents is translated in every locale > account — zh-Hant
      ↳ 失败原因: @objectstack/platform-objects:test: AssertionError: untranslated nav ids in apps.account.navigation: expected [ 'grp_account_ai_assistant' ] to deeply equal []
    @objectstack/spec:test:      ✓ inverts the card's measured control: the three bogus keys now FAIL parse instead of adding zero warnings  303ms
      ↳ 失败原因: (这条 FAIL 之后 12 行内没有可识别的原因行 —— 点进 job 看)
    

↳ 失败原因 是判读的关键:超时(Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言(AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError。 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 21 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

2 participants