Skip to content

Commit 8e6acf4

Browse files
docs(adr): ADR-0070 D2 amended — a type: module sub-package of an installed app may be disabled, not authored into, by an admin (#28285) (#28430)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"} --> _Requested by **Leehom** · [project thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1X1MLz3h8qcUsNrPdNUHERHj)_ domain:server#2 · os-project-manager · session_01FEkQ5nCt4UPJjChW4GyQxQ Fixes #28285 Clause-②: no — governed prose only; no accept set, refusal or served shape moves here. The behaviour the note records shipped in PR #28308 (`7abe151b`), which declared its own `yes (widening)`. Metadata-protocol: none ## 维护者速读 - **改了什么**:在 ADR-0070 的 D2 下加一条带日期的修订说明,并在 Status 行加同日期的指针:已安装应用下的 `type: module` 子包可以被管理员停用(不是写入)。说明按代码实际行为写:停用门读取的归属判据(模块包的 `manifest.dependencies` 直接声明一个已安装的 `type: app` 包,自身 scope 不是 system/cloud)、写入仍拒绝(`isWritablePackage` 不变)、导航折叠丢弃已停用包的贡献、其记录触发流程在进程内写入时也不派发。 - **为什么改**:#28117 第 6 项,裁决「大包 + 设置里启用模块」/「同意」(2026-10-30);代码已随 PR #28308 合入 main,决策记录要跟上代码。 - **风险与回滚**:零运行时风险,只改一份决策记录;回滚即 revert。ADR-0146 覆盖的是 D3/D5,D2 不在其列,本修订与之不冲突。 - **先看**:`docs/adr/0070-package-first-authoring.md` 的 D2 修订块(一个引用段);Status 行末尾的指针。 - **两分钟验证**:`pnpm check:adr-symbol-anchors`,预期全部锚点解析(本次新增 5 个符号锚);再把 D2 修订块与 `packages/runtime/src/domains/packages.ts` 里 `isDisableableAppModule` 的注释对照。 - **需要你做的**:Tier H(`docs/adr/**`):你的 approve 或亲手合并。另有一个未决问题不在本 PR 内:应用自己的 hook 在进程内读写已停用模块的对象要不要拒绝(决策函 6538936519),修订块只把它记为未决。 ## What changes One file, `docs/adr/0070-package-first-authoring.md`: 1. **An amendment note under D2**, in the registry's dated `> **Amended …**` form (the form ADR-0087 and ADR-0005 use), recording the maintainer's ruling of 2026-10-30 verbatim and untranslated (「大包 + 设置里启用模块」, then 「同意」) and what shipped in PR #28308 (`7abe151b`): - *read-only* in D2 means *may not be authored into*, not *may not be switched off*; - the tie the disable door reads, as `isDisableableAppModule` measured it on both install paths: `manifest.type` `'module'`, `manifest.dependencies` directly naming an installed `type: 'app'` package, own `manifest.scope` not `system` / `cloud`; - what does not move: `isWritablePackage` unchanged, `writable` still means "may author into" and stays `false` for a module, edit / publish / revert / delete / overwrite still answer `422 WRITABLE_PACKAGE_REQUIRED`; the door's verdict is served beside it as `disableable` (#28318, on `main`), computed by the same predicate; - what disabled means for the module: `404 OBJECT_PACKAGE_DISABLED` on the data plane and the by-name reads, its nav contributions dropped by the fold, its record-change flows withheld for in-process writes, enable restores all three, data kept; - the in-process question (an app's own hook reading a disabled module's objects through `ctx.api`) is recorded as **not decided here**, with the maintainer as decision letter `6538936519` on #28117. 2. **A dated pointer on the Status line**, the form ADR-0146's "Status-line pointers" section fixes for an amended record (`· **Amended** (date, …) — clause; see the amendment note under D2`). Nothing else on the Status line moves; D3–D6 are untouched. **ADR-0146 check.** Its supersession of ADR-0070 covers D3's prompt and D5's destination for the paired case, and its pointer on ADR-0070's Status line says "D1, D2, D4 and D6 stand". D2 is not the superseded part, so the amendment stands beside ADR-0146 and this PR touches no other record. Five symbol anchors are new, all resolving (`check:adr-symbol-anchors` below): `packages/runtime/src/domains/packages.ts#isDisableableAppModule`, `packages/metadata-protocol/src/package-writability.ts#isWritablePackage`, `packages/spec/src/api/package-api-assembled.zod.ts#SERVED_ROW_VERDICTS`, `packages/objectql/src/registry.ts#applyNavContributions`, `packages/triggers/trigger-record-change/src/record-change-trigger.ts#RecordChangeTrigger`. ## Tests None: a decision record, no tested behaviour changes, so no ablation. The gates that read `docs/adr/**` are the verification. ## Acceptance notes - **Governed surface.** `node scripts/pm/check-governed-merges.mjs --test docs/adr/0070-package-first-authoring.md` answers exit 3: "⛔ GOVERNED — a human merge is the review record for this PR (#9495 regime) … ⚖️ landing tier: H(人合) — the maintainer's hand, or an authorized APPROVED review (GOVERNED_APPROVERS) and then the owning seat lands it. docs/adr/** ×1". This PR stays draft; no seat readies, queues or arms it. - **Model tier.** `dispatch-gates.mjs --tier` on the path: "Model tier — MANDATORY: claude-fable-5-1 … the same order fixes the effort floor at xhigh". This session ran at `claude-fable-5-1`, effort `xhigh` (the session record). - **`skip-changeset`** rides the create: `docs/adr/**` publishes nothing (fast track). - **The generated ADR index** `docs/adr/README.md` is current after the edit (`node scripts/adr-index.mjs --check`: "OK — docs/adr/README.md is current and every record parsed"), so it is not touched. - **Not here, by the order:** the in-process access decision (letter `6538936519`), the Console half (#28118), D4–D6. - **Lint.** `pnpm exec eslint --no-inline-config docs/adr/0070-package-first-authoring.md` exits 0 with "File ignored because no matching configuration was supplied": Markdown is outside the eslint config, and the doc gates below are the style authority for this path. - **The PR body's first two lines** are the harness attribution block the project requires; the seat signature follows it. ## Verification Gates from `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` on this diff (21 commands), each run on head `c2b0e3ad` with its exit code captured before any pipe: | # | Gate | Exit | |:--|:--|:--| | 1 | `node scripts/check-adr-links.mjs` | 0 | | 2 | `node scripts/check-adr-links.mjs --self-test` | 0 | | 3 | `node scripts/check-closing-keyword-parity.mjs` | 0 | | 4 | `node scripts/check-closing-keyword-parity.mjs --self-test` | 0 | | 5 | `node scripts/check-comment-mask-corpus.mjs` | 0 | | 6 | `node scripts/ci/plan-test-core-shards.mjs --self-test` | 0 | | 7 | `pnpm --filter @objectstack/lint run check:doc-formula-expressions` | 0 | | 8 | `pnpm check:adr-anchors` | 0 | | 9 | `pnpm check:adr-index` | 0 | | 10 | `pnpm check:adr-symbol-anchors` | 0 | | 11 | `pnpm check:ci-filter-parity` | 0 | | 12 | `pnpm check:closed-ceremony-text` | 0 | | 13 | `pnpm check:cross-package-test-inputs` | 0 | | 14 | `pnpm check:doc-authoring` | 0 | | 15 | `pnpm check:driver-memory-census` | 0 | | 16 | `pnpm check:gitlink-declared` | 0 | | 17 | `pnpm check:nul-bytes` | 0 | | 18 | `pnpm check:pm-governed-merges` | 0 | | 19 | `pnpm check:pm-prior-rulings` | 0 | | 20 | `pnpm check:refd-timer-probe` | 0 | | 21 | `pnpm check:watch-hint-literal` | 0 | `dispatch-gates --ran`: "21 derived famil(ies) accounted for — 21 run, 0 NOT-MEASURED (a DERIVED zero — all 21 recorded an exit code and none of them is 3)". The first pass recorded exit 3 (PREREQUISITE NOT MET: `@objectstack/formula` and `@objectstack/lint` not built) on `check:doc-formula-expressions`; after `pnpm exec turbo run build --filter=@objectstack/formula --filter=@objectstack/lint` under the verify lock (VERDICT command-exit 0, 1m36s) the gate ran and exited 0, and the record above carries that run. <details><summary>Readings quoted from the gate logs</summary> - `node scripts/check-adr-links.mjs` → ✅ check-adr-links: 962 relative link destination(s) under docs/adr/ resolve - `node scripts/check-adr-links.mjs --self-test` → ✅ check-adr-links --self-test: discrimination, census, ADR-0046 pin and baseline staleness all verified - `node scripts/check-closing-keyword-parity.mjs` → check-closing-keyword-parity: OK (3 parsers agree on all 9 keywords and both measured separators; sweep found 5 file(s) carrying the grammar across 23186 tracked file(s), all registered). - `node scripts/check-closing-keyword-parity.mjs --self-test` → ✓ check-closing-keyword-parity --self-test: 40 assertions, 5 mutations of the shipped parsers each driven to red. - `node scripts/check-comment-mask-corpus.mjs` → ✓ comment-mask corpus sweep [scripts/js-comment-mask.mjs]: 19531 files, 0 disagree, 0 unparseable, 186.4s (comparator self-test: 26 cases pass). - `node scripts/ci/plan-test-core-shards.mjs --self-test` → ✓ plan-test-core-shards self-test: 23 case(s) across 5 batteries. - `pnpm --filter @objectstack/lint run check:doc-formula-expressions (rerun after the build)` → ✓ check:doc-formula-expressions (field-level `*When`, #11407): 15 predicate(s) on a statically determinable field layer judged clean; 12 skipped as undeterminable. - `pnpm check:adr-anchors` → check-adr-anchors: OK (95 anchored file(s), every governing ADR still referenced; 142 decision number(s), each naming one decision or an allowlisted pair; 62056 citation(s) across 8887 file(s) resolve, 37 of them stem-qualified to one record; 2025 decision-let - `pnpm check:adr-index` → adr-index --report: OK — docs/adr/README.md is current, every record parsed, self-test 46 assertions green. Report-only: this line never reds the step. - `pnpm check:adr-symbol-anchors` → ✅ check-adr-symbol-anchors: 2952 anchors across 148 records resolve — 691 symbol (663 declaration, 28 literal), 2214 file-level, 47 cross-repo, 6 exempt, 31 continuation. 0 line anchors survive in the path-anchored, backticked continuation, fenced comment head - `pnpm check:ci-filter-parity` → OK: all 8 root input(s) of `test` / `test:repo` in turbo.json are covered by `core` or `crosspkg`, which the `test` job's `if:` reads. - `pnpm check:closed-ceremony-text` → ✓ check-closed-ceremony-text: 5537 shipped file(s) read; none says the ownership ceremony is closed. - `pnpm check:cross-package-test-inputs` → OK: 31 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob (6 of them on a split "test:repo" task); 13 walked root(s) judged, 3 on ACCEPTED_WALK_RADII; 4061 test file(s) import a workspace sibling by bare specifier over - `pnpm check:doc-authoring` → ✓ doc authoring guard: sibling-package prose ids hold the baseline — 0 pinned site(s) across 0 file(s), 116712 string(s) read in 1627 parsed source(s) (seen floor 40000 strings / 600 sources), no growth, no burn-down unrecorded. - `pnpm check:driver-memory-census` → check-driver-memory-census: OK — every declaration is ledgered, every ledger entry is live, and every ruled file states "#6664 census: 2 ruled consumers". This gate polices the census, never investment. - `pnpm check:gitlink-declared` → check-gitlink-declared: OK (23186 index entries -- 0 gitlink(s) at mode 160000; no .gitmodules in the index, so nothing is declared; nothing to declare). - `pnpm check:nul-bytes` → check-nul-bytes: OK (scanned 23087 text file(s) -- 23087 tracked, 0 untracked-not-ignored; skipped 99 binary; no raw ASCII control bytes). - `pnpm check:pm-governed-merges` → ✓ check-governed-merges --self-test: 537 assertions (the unified governed predicate + near misses, subject→PR spellings, window parsing, the #12633 landing window — the QS-7 regression pin in both directions, the topological close beyond the budget, the unprov - `pnpm check:pm-prior-rulings` → ✓ check-prior-rulings self-test: 155 cases pass (term derivation with its length floor, stopwords and code-span splitting; the governing-text carrier in both measured shapes plus the absent case that is 23 of 27 on the live board; the backticked-CLAUSE and lea - `pnpm check:refd-timer-probe` → OK check-refd-timer-probe: 19525 source file(s) swept; the process-global timer probe is read in packages/qa/refd-timer-testkit/src/index.ts and nowhere else. - `pnpm check:watch-hint-literal` → ✓ check-watch-hint-literal: 75 declaration(s) across 4 rostered name(s) -- ROOT_DIR_WATCH_HINTS 49, ROOT_FILE_WATCH_HINTS 14, ROOT_WATCH_HINTS 3, DECLARED_WATCH_HINTS 9 -- every one an array of quoted literals inside its own statement, all 188 of those literal </details> ## 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. (No names added.) - [x] 3. No formatter ran; `pnpm lint` scoped to the file is ignored by config (exit 0, see Acceptance notes). - [x] 4. One issue, one branch off an up-to-date `main` (`5da3a8ce`), one pull request. - [x] 5. A change that publishes carries a changeset: nothing publishes (`skip-changeset`); both declaration lines are filled in above. - [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 required Chinese summary; the ruling is quoted verbatim). - [x] The *Before Submitting* list in CONTRIBUTING.md is done. --- _Generated by [Claude Code](https://claude.ai/code/session_01FEkQ5nCt4UPJjChW4GyQxQ)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 96d12ec commit 8e6acf4

1 file changed

Lines changed: 3 additions & 1 deletion

File tree

‎docs/adr/0070-package-first-authoring.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# ADR-0070: Package-first authoring — every runtime-authored item lives in a writable package (base); no orphans
22

3-
**Status**: Accepted (2026-06-24) — P1–P3 implemented, merged & live-verified (see *Implementation status*); D4–D6 remaining · **Superseded in part by [ADR-0146](./0146-studio-designs-setup-administers-one-editor-per-item-paired-customize-package.md)** (2026-10-31) — D3's prompt to create a base, for the paired case only: a customer's first customization of an installed base lands in the auto-created `<base>.custom` package without a prompt (ADR-0146 D3); D5's destination for a package-less overlay row of a packaged item: the base's `<base>.custom`, as a declared override, after which the package-less write closes (ADR-0146 D11). D1, D2, D4 and D6 stand; D3's prompt stands everywhere else.
3+
**Status**: Accepted (2026-06-24) — P1–P3 implemented, merged & live-verified (see *Implementation status*); D4–D6 remaining · **Superseded in part by [ADR-0146](./0146-studio-designs-setup-administers-one-editor-per-item-paired-customize-package.md)** (2026-10-31) — D3's prompt to create a base, for the paired case only: a customer's first customization of an installed base lands in the auto-created `<base>.custom` package without a prompt (ADR-0146 D3); D5's destination for a package-less overlay row of a packaged item: the base's `<base>.custom`, as a declared override, after which the package-less write closes (ADR-0146 D11). D1, D2, D4 and D6 stand; D3's prompt stands everywhere else. · **Amended** (2026-10-31, [#28117](https://github.com/objectstack-ai/objectstack/issues/28117) — the maintainer's ruling of 2026-10-30, 「大包 + 设置里启用模块」, then 「同意」: a `type: module` sub-package of an installed app may be **disabled**, not authored into, by an admin, on the tie PR [#28308](https://github.com/objectstack-ai/objectstack/pull/28308) shipped; see the amendment note under D2. D2's two kinds and its writability predicate stand.)
44
**Deciders**: ObjectStack Protocol Architects
55
**Builds on**: [ADR-0005](./0005-metadata-customization-overlay.md) (`allowOrgOverride` / runtime overlay model; one Zod source per type), [ADR-0048](./0048-cross-package-metadata-collision.md) (one app per package; package id is the addressing unit), [ADR-0033](./0033-ai-assisted-metadata-authoring.md) (AI authors metadata as drafts), [ADR-0067](./0067-commit-history-and-rollback-for-ai-authoring.md) (commit history & rollback for AI authoring)
66
**Consumers**: `@objectstack/objectql` (`protocol.saveMetaItem` write path), `@objectstack/spec` (metadata-type registry), `../objectui` (Studio create flow, package scope selector, `PackagesPage`), `../cloud` (`service-ai-studio` — `create_metadata` / `resolvePackageId` / `apply_blueprint`)
@@ -63,6 +63,8 @@ A runtime **create** (`intent = runtime-only`, i.e. not an overlay of an artifac
6363

6464
The kernel exposes a single predicate — *is this package writable?* — reused by all surfaces (cloud's `resolvePackageId:319` read-only check is the seed of this).
6565

66+
> **Amended 2026-10-31 ([#28117](https://github.com/objectstack-ai/objectstack/issues/28117), the maintainer's ruling of 2026-10-30; shipped in PR [#28308](https://github.com/objectstack-ai/objectstack/pull/28308), `7abe151b`) — a `type: module` sub-package of an installed app may be disabled (not authored into) by an admin.** The maintainer, verbatim: 「大包 + 设置里启用模块」, then 「同意」 — a suite ships as one artifact, an `app` package with `module` packages beside it; the customer's own admin switches its modules off and on, per environment; and "disabled" has exactly one meaning: hidden, refused on the data plane, data kept. *Read-only* above means *may not be authored into*; it does not mean *may not be switched off*. So `PATCH /api/v1/packages/:id/disable` reads one exemption before the writability check, `packages/runtime/src/domains/packages.ts#isDisableableAppModule`, and the tie it reads is the one both install paths leave on the registry row — a code boot and a marketplace install alike, since neither keeps the artifact's own package list past registration: the row's `manifest.type` is `'module'`, its `manifest.dependencies` directly names a package installed here whose `manifest.type` is `'app'` (ADR-0141 D1), and its own `manifest.scope` is not `system` / `cloud`. Everything else answers the door as before. The predicate above does not move: `packages/metadata-protocol/src/package-writability.ts#isWritablePackage` is unchanged, `writable` keeps meaning "may author into" and stays `false` for a module, so edit, publish, revert, delete and overwrite still answer `422 WRITABLE_PACKAGE_REQUIRED`; the door's own verdict is served beside it as `disableable` (`packages/spec/src/api/package-api-assembled.zod.ts#SERVED_ROW_VERDICTS`), computed by the predicate the door asks, so no client re-derives the rule. While the module is disabled, its objects answer `404 OBJECT_PACKAGE_DISABLED` on the data plane and the by-name reads, as any disabled package's do; the navigation fold drops its contributions (`packages/objectql/src/registry.ts#applyNavContributions`); and its record-change flows are withheld for in-process writes too (`packages/triggers/trigger-record-change/src/record-change-trigger.ts#RecordChangeTrigger`). Enable restores all three; the data was never touched. Not decided here: whether an app's own in-process access to a disabled module's objects (a hook's `ctx.api`) should meet the data plane's refusal — today it is served, pinned and documented, and the question is with the maintainer as decision letter `6538936519` on #28117.
67+
6668
### D3 — Create-flow contract: select-or-create a base first, enforced everywhere [new]
6769

6870
Every authoring surface follows the same contract; none silently defaults into a catch-all:

0 commit comments

Comments
 (0)