Repository navigation
Commit 7abe151
domain:server#2 · os-project-manager · session_01FEkQ5otJRNvRGLG91cCJr7
<!-- claude-projects-attribution: {"github_login":"os-project-manager"}
-->
_Requested by **Leehom** · [project
thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1XMKwZYa8zJsAxCsvLkrMXaw)_
Part of #28117
Clause-②: yes (widening) — `PATCH /api/v1/packages/:id/disable` now
accepts a module of an installed app, which it refused `422
WRITABLE_PACKAGE_REQUIRED` before (measured red on the base tree, both
install paths). Nothing is narrowed.
Metadata-protocol: none
Skills-docs: updated here —
`content/docs/deployment/marketplace-handbook.mdx` (step 7 corrected,
new "Disabling a module" section, including what an app hook meets). No
`skills/**` page names the disable door or `navigationContributions`
(`git grep` in `skills/`: 0 hits). The ADR-0070 D2 addendum line (card
item 6) is the seat's follow-up #28285; this PR does not touch
`docs/adr/**`.
## 维护者速读
- **改了什么**:管理员(有 `manage_metadata`)现在可以停用一个已安装应用下的 `type: 'module'`
子包;停用后它的导航项从应用菜单里消失,绑在它对象上的记录触发流程也不再运行。其余只读包(应用本身、插件、平台包、没有所属应用的模块)照旧答
422,模块依旧不能被编辑、发布、删除。
- **为什么改**:维护者裁决「大包 + 设置里启用模块」(方案
A):客户自己的管理员按环境开关模块,「停用」只有一种含义:隐藏、数据面拒绝、数据保留。之前停用门对套件里的每个包都答 422,模块根本关不掉。
- **风险与回滚**:低。停用门只多放行一类包(`type: 'module'` 且直接依赖一个已安装的 `type: 'app'`
包,且不是 system/cloud 范围),其他门不变;导航折叠和触发器只在包已停用时才跳过。回滚即 revert。
-
**先看**:`packages/runtime/src/domains/packages.ts`(`isDisableableAppModule`
与停用门那一行);`packages/objectql/src/registry.ts`(`applyNavContributions`
里新增的一行);`packages/triggers/trigger-record-change/src/record-change-trigger.ts`(handler
里新增的判断)。
- **两分钟验证**:`pnpm --filter @objectstack/runtime exec vitest run
--maxWorkers=2 src/domains/packages-disable-app-module.test.ts`,预期 26/26
通过(两条安装路径各一套:停用 200、对象 404 `OBJECT_PACKAGE_DISABLED`、导航消失、重新启用后恢复、其余七类包
422、重启后保持停用)。
- **需要你做的**:无,席位落地。另有一个需要你拍板的问题在席位的报告里:应用自己的 hook 经 `ctx.api.object()`
读已停用模块的对象,实测**不会被拒**(拿到保留的数据),与补充说明 6523684730 的预期不同;本 PR
不改这个行为,只钉住并写进文档。
## What changes
### 1. The disable door (`@objectstack/runtime`)
`PATCH /packages/:id/disable` asked `requireWritablePackage` alone, and
`isWritablePackage` answers read-only for every package of a booted or
installed artifact (`engine.manifests`), so every suite module answered
`422 WRITABLE_PACKAGE_REQUIRED`. The door now reads one exemption first,
`isDisableableAppModule(engine, id)`, and only for this verb:
- the row's `manifest.type` is `'module'`;
- its `manifest.dependencies` DIRECTLY name a package that is installed
here and whose `manifest.type` is `'app'` (read with
`declaredDependencyIds` from `@objectstack/core`, the reader the
registry's co-owner gate uses);
- its own `manifest.scope` is not `system` / `cloud`
(`READ_ONLY_PACKAGE_SCOPES`): a module the platform delivers stays the
platform's.
`isWritablePackage` is unchanged, so `writable` still means "may author
into" and stays `false` for a module; edit, publish, revert, delete and
overwrite still refuse it (pinned below). Everything else answers the
disable door exactly as before.
**The tie, measured on both install paths.** A code boot (`AppPlugin` →
`manifest.register`, `source: 'code'`) and a marketplace install
(`manifest.register(…, { source: 'marketplace' })`) leave the same three
keys on the module's registry row: `type: 'module'`, `dependencies: {
APP: RANGE }`, `namespace` (the app's). Both put the module in
`engine.manifests` (read-only for authoring). The artifact's own package
list (`ArtifactInstallScope.packageIds`) is NOT on the row: it lives
only for the length of the `registerApp` call. So the declared
dependency on an installed app is the tie the registry actually carries;
the namespace is carried too but is not used, because sharing a
namespace is itself derived from that declaration (ADR-0141 D5). The
HotCRM read in addendum 6523684730 has every module declaring
`dependencies: { 'app.objectstack.hotcrm': '^6.0.0' }`;
`examples/app-multi-package`'s module declares its core app the same
way.
### 2. Navigation (`@objectstack/objectql`)
`SchemaRegistry.applyNavContributions` skips a contribution whose
`packageId` is disabled (`isPackageDisabled`), before the group lookup,
so a disabled package's contribution is neither folded, relocated to the
top level, nor diagnosed as `nav_contribution_group_missing`. That one
fold serves every reader: `getApp`, `getAllApps`, and the protocol's
`app` list and by-name reads (`protocol.ts` L10072 / L11175 call
`registry.applyNavContributions`). Nothing is cached past the fold
(`meta-overlay-cache.ts` caches only the `sys_metadata` row set), so a
re-enable restores the items with nothing to invalidate. The stored app
is never edited.
### 3. Record-change flows (`@objectstack/trigger-record-change`)
Measured first, as the card asks. Writes through the data plane to a
disabled package's object were already refused (`404
OBJECT_PACKAGE_DISABLED`), so no flow fired from them. But the engine
itself has no disabled-package check: a write made IN-PROCESS (an app
hook's `ctx.api.object(...)`, another flow's data node, a service) lands
and fires the object's lifecycle hooks. On the base tree, a
`record-after-create` flow on such an object stamped `done` (red below).
`RecordChangeTrigger`'s handler now returns before dispatch when
`engine.registry.isObjectPackageDisabled(object)` is true (the question
the data plane's refusal asks, `binding.object ?? ctx.object` as
`buildContext` reads it). The write itself is untouched, so the data is
kept; the check is per write, so an enable resumes the flow with nothing
to re-bind. `RecordChangeDataEngine` gains an optional structural
`registry` member; an engine without it dispatches as before.
### 4. Served verdict: a fork, not written here
The Console decides whether to offer Disable from `writable || !enabled`
(`ui/packages/app-shell/src/views/metadata-admin/PackagesPage.tsx`
L1054), and `writable` stays `false` for a module, so the Console cannot
offer this disable yet. The answer "may this package be disabled here?"
needs its own key beside `writable` in `SERVED_ROW_VERDICTS`
(`packages/spec/src/api/package-api-assembled.zod.ts` L155-166), which
is a spec-lane edit, so it is NOT written here and `writable` is not
overloaded.
`Seam: spec:SERVED_ROW_VERDICTS (a new served key beside writable) →
runtime:withWritableVerdict (packages/runtime/src/domains/packages.ts,
would read isDisableableAppModule) | renderer:PackagesPage.tsx L1054`
`isDisableableAppModule` is the one predicate both the door and that
future served key read, so the Console never re-derives the rule
(#8146).
### 5. Docs
`content/docs/deployment/marketplace-handbook.mdx`: step 7 said every
package "provided by code or by an installed app" answers the disable
door 422, which this PR makes false; it now names the module exemption.
A new "Disabling a module" section under "Extras: dependencies and
modules" gives the manifest shape, the one meaning of disabled (data
plane, navigation, flows, restart, authoring), and what an app's own
hook meets (addendum 6523684730 item 2).
## Tests
All three suites import the code under change from `src/` except where
noted; red-first readings are on the base tree `69ce01d7e`.
- `packages/runtime/src/domains/packages-disable-app-module.test.ts`
(new, 26 tests). Real `ObjectKernel` with `MetadataPlugin`,
`ObjectQLPlugin`, `AppPlugin` and a SQLite `:memory:` `SqlDriver`,
driven through the runtime dispatcher's `dispatch()`. A suite artifact
composed with `composeStacks(…, { manifest: 'preserve' })` (app, module
with a `navigationContributions` entry, a plugin, a module of the
plugin), plus four packages registered alone. Booted twice, once per
install path (`code`, `marketplace`):
- the tie on the row (type, dependencies, namespace, `source`, in
`engine.manifests`);
- enabled control: data 201/200, nav `nav_accounts, nav_orders` through
`GET /meta/app/qs_app` and `registry.getApp`;
- `PATCH …/disable` 200, `enabled: false`; `GET /data/qs_order` 404
`OBJECT_PACKAGE_DISABLED`; nav `nav_accounts` only, on both readers; the
app's own object still 200;
- edit (`PATCH /packages/:id`), publish, delete on the module: 422
`WRITABLE_PACKAGE_REQUIRED`, manifest unchanged;
- `PATCH …/enable` 200: data back with the kept record, nav back;
- still 422 and still enabled: the app, the plugin, a module of the
plugin, an orphan module (no dependency), a module whose app is not
installed, a `system`-scoped module of the installed app, a
`system`-scoped plugin.
- Restart: disable through the door, shut down, boot a second kernel on
the same `OS_HOME` and environment: the module boots disabled (404 on
the data plane, nav gone); enable, boot a third: enabled, nav back.
- Addendum measurement (see below).
- Base tree: 6 failed / 20 passed (the two disable cases answer 422, so
do the restart and measurement cases that disable first; the 14 refusal
cases and the tie readings were already green, as a control should be).
-
`packages/objectql/src/registry-nav-contribution-disabled-package.test.ts`
(new, 11 tests, real `SchemaRegistry`): for each of `getApp`,
`getAllApps`, `applyNavContributions`: enabled control, disabled drop
(in its group and its relocated top-level item, other packages' items
stay), re-enable restore; no missing-group diagnostic for a disabled
package; the stored app unchanged. Base tree: 4 failed / 7 passed.
-
`packages/triggers/trigger-record-change/src/record-change-integration.test.ts`,
new `[#28117]` block (2 tests, real kernel with ObjectQL, automation,
the trigger plugin and SQLite): enabled control stamps `done`; disabled,
an in-process user insert writes the record and the flow does not stamp;
another package's object in the same window still stamps; re-enabled, it
stamps again. Base tree: 1 failed / 1 passed (`expected 'done' to be
null`).
### What an app hook meets (addendum 6523684730 item 2)
Measured and pinned in `packages-disable-app-module.test.ts`: with the
module disabled through the door, an `afterInsert` hook registered for
the APP package on `qs_account` calls
`ctx.api.object('qs_order').find({})`; a `POST /data/qs_account` fires
it, and the hook gets the module's kept rows (`[{ rows: 1 }]`), no
error. In the same window `GET /data/qs_order` answers `404
OBJECT_PACKAGE_DISABLED`. The hook's `ctx.api` is a `ScopedContext` over
the engine (`buildHookApi`, `engine.ts`), and the disabled-package
refusal lives in the protocol's data plane (`assertObjectRegistered`),
never in the engine. This PR changes no answer for any caller (addendum
item 1); the handbook says there is no `OBJECT_PACKAGE_DISABLED` to
catch in-process.
<details><summary>Ablations (one-time, restored)</summary>
Each through `node scripts/ablation-replace.mjs` under the verify lock;
every suite imports the mutated file from `src/`, so no build sits
between mutation and run.
1. Door: `const readOnly = isDisableableAppModule(qlService, id)` →
`const readOnly = false && isDisableableAppModule(qlService, id)` (the
unconditional `requireWritablePackage`). Blob `53825564f8bb` →
`e14712be2677`. `packages-disable-app-module.test.ts`: 4 failed / 22
passed (both "disables the module (200)", the restart, the measurement).
Restored: blob = HEAD, `git diff HEAD` empty.
2. Nav: `--delete` the line `if (this.isPackageDisabled(c.packageId))
continue;`. Blob `0d8ee0e6b325` → `4629981b5d26`.
`registry-nav-contribution-disabled-package.test.ts`: 4 failed / 7
passed (the three disabled-drop cases and the diagnostic case).
Restored: blob = HEAD.
3. Trigger: `if (written &&
this.engine.registry?.isObjectPackageDisabled?.(written) === true) {` →
`if (written && false && …) {`. Blob `9ce4a4d903d3` → `a08bcd2662b8`.
The `[#28117]` block: 1 failed / 1 passed. Restored: blob = HEAD.
</details>
<details><summary>Package suites and typecheck</summary>
Under the verify lock, after building `@objectstack/runtime...` and
`@objectstack/trigger-record-change...`:
- `pnpm --filter @objectstack/objectql --filter
@objectstack/trigger-record-change --filter @objectstack/runtime
--no-bail test` (at `9808a8fd`): objectql `Test Files 501 passed (501)`,
`Tests 9761 passed (9761)`; trigger-record-change `11 passed (11)`, `116
passed (116)`; runtime `488 passed (488)`, `Tests 7932 passed | 19
skipped (7951)`; `VERDICT command-exit 0`.
- The trigger file again at `c403c6bd` after typing its slot lookups:
`Tests 11 passed (11)`.
- `pnpm … run typecheck` on the three packages: exit 0 (`tsc --noEmit`
plus each package's test program).
- `pnpm exec eslint --no-inline-config` on the six touched source and
test files: exit 0.
</details>
## Acceptance notes
- **Not in this PR (routed):** card item 6, the ADR-0070 D2 addendum
line, is #28285 (Tier H). The served-verdict key is the Seam above.
- **Read from source, not measured:** a disabled module's own CODE hooks
(`hooks` in its manifest) still run on an in-process write to its
objects, and a disabled module's record-change flows bound to an ENABLED
package's object (e.g. a module flow on an app object) still fire: the
engine and the automation engine read no package state, and this card's
item 3 names flows on the disabled package's objects only. Both sit with
the in-process question in the report.
- `Clause-②` measured: the door's 422 for these rows was the published
answer (the handbook's step 7 named it), and the request now accepted is
a new accept; no refusal is added anywhere.
## Verification
Gates from `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` on this diff (93 commands), each run on the
final head `c403c6bd` with its exit code captured before any pipe: all
93 exit 0. `dispatch-gates --ran`: "93 derived famil(ies) accounted for
— 93 run, 0 NOT-MEASURED". On the first head `9808a8fd` one was red,
`check:slot-lookup` (the new trigger test block added two `as any` slot
lookups, erasure count 11 → 13); `c403c6bd` types them and the ratchet
holds. The families CI runs are cited from this PR's check-runs in the
report on the card.
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(`packages/spec` untouched.)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes (scoped to the touched
files).
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: n/a.
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English (the 维护者速读 block is the
seat's required Chinese summary).
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01FEkQ5otJRNvRGLG91cCJr7)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 501c864 commit 7abe151
8 files changed
Lines changed: 774 additions & 7 deletions
File tree
- .changeset
- content/docs/deployment
- packages
- objectql/src
- runtime/src/domains
- triggers/trigger-record-change/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
214 | 214 | | |
215 | 215 | | |
216 | 216 | | |
217 | | - | |
218 | | - | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
219 | 223 | | |
220 | 224 | | |
221 | 225 | | |
| |||
255 | 259 | | |
256 | 260 | | |
257 | 261 | | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
258 | 301 | | |
259 | 302 | | |
260 | 303 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6795 | 6795 | | |
6796 | 6796 | | |
6797 | 6797 | | |
6798 | | - | |
6799 | | - | |
6800 | | - | |
| 6798 | + | |
| 6799 | + | |
| 6800 | + | |
| 6801 | + | |
6801 | 6802 | | |
6802 | 6803 | | |
6803 | 6804 | | |
| |||
6815 | 6816 | | |
6816 | 6817 | | |
6817 | 6818 | | |
| 6819 | + | |
| 6820 | + | |
| 6821 | + | |
| 6822 | + | |
| 6823 | + | |
| 6824 | + | |
6818 | 6825 | | |
6819 | 6826 | | |
6820 | 6827 | | |
| |||
0 commit comments