From 7ca6f94bd2e3355eee926ed2b5887617a5b3977c Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 25 Aug 2026 07:14:48 +0000 Subject: [PATCH 1/3] =?UTF-8?q?docs(adr):=20ADR-0126=20=E2=80=94=20the=20p?= =?UTF-8?q?latform=20customization=20model=20for=20packaged=20metadata?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three regimes assigned by what the artifact does (overlay / clone+takeover / extend), one generic data-plane activation ledger (sys_metadata_activation), install-level write authority with the org column reserved, the #6190 and sole-definition-ledger walls restated, and the packaged-flow worked example carrying the 2026-08-25 tentative directions (settled only by this ADR's human merge). Survey posted on the card as the evidence base. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01KWRU3s15AJz7PGW7a7wdCh --- ...6-packaged-metadata-customization-model.md | 363 ++++++++++++++++++ 1 file changed, 363 insertions(+) create mode 100644 docs/adr/0126-packaged-metadata-customization-model.md diff --git a/docs/adr/0126-packaged-metadata-customization-model.md b/docs/adr/0126-packaged-metadata-customization-model.md new file mode 100644 index 0000000000..c3393c6746 --- /dev/null +++ b/docs/adr/0126-packaged-metadata-customization-model.md @@ -0,0 +1,363 @@ +# ADR-0126: The platform customization model for packaged metadata — three regimes, one activation ledger + +**Status**: Proposed (2026-08-25) — awaiting the maintainer's hand-merge, which is itself the +acceptance act for a governed surface (Prime Directive #14) **and the ruling that settles the +tentative flow-instance directions in §7** (chartered on +[#12049](https://github.com/objectstack-ai/objectstack/issues/12049); ⛔ none of §7 is settled +until this merges). +**Deciders**: maintainer charter, 2026-08-25, live PM chat, verbatim and untranslated: +「要整体评估除了流程还有哪些需要自定义的也有类似的问题,开始写adr」, following +「很多所有的元数据都有类似的个性化需求,要统一考虑。」 +**Builds on**: [ADR-0005](./0005-metadata-customization-overlay.md) (org overlay + the two-tier +write model), [ADR-0029](./0029-kernel-object-ownership-and-platform-objects-decomposition.md) +D7/D9 (contributor kinds, navigation contributions), +[ADR-0049](./0049-no-unenforced-security-properties.md) (enforce-or-remove), the +[#11513](https://github.com/objectstack-ai/objectstack/issues/11513) +lock-and-clone ruling (2026-08-24, verbatim: 「同意 第一步(创业阶段,Salesforce 式)」), the +[#11665](https://github.com/objectstack-ai/objectstack/issues/11665) flow design +([comment 5404974967](https://github.com/objectstack-ai/objectstack/issues/11665#issuecomment-5404974967)) +and its 2026-08-25 maintainer-discussion record +([comment 5406672100](https://github.com/objectstack-ai/objectstack/issues/11665#issuecomment-5406672100)) +**Evidence**: the measured per-type survey on #12049 +([comment 5406807988](https://github.com/objectstack-ai/objectstack/issues/12049#issuecomment-5406807988)), +pinned to `origin/main` `0b048393faa98151600598bcebf2ad905ee7021f`. Load-bearing facts are +re-cited inline; the survey carries the instruments and positive controls. +**Consumers**: every seat that designs or reviews a post-install customization surface; the +#11665 implementation cards; the permission-set convergence card; `packages/platform-objects` +(the ledger object, when built); `content/docs` (the capability-page promises §1.3 names) + +--- + +## TL;DR + +The platform ships packaged apps and refuses nearly every post-install change to them, while two +shipped docs pages promise "install with one click, then customize in Studio". Three customization +mechanisms already exist — org **overlay** for exactly five presentation types, **clone-to- +customize** for permission sets, package-grain **extend** for objects and app navigation — and +each was invented per type, with its own ledger. This ADR writes the model down once: + +1. **Three regimes, assigned by what the artifact does** (§3): presentational → overlay; + behavioral → clone + takeover, ⛔ never silent override; structural → extend. Code-only types + sit outside customization entirely. +2. **One generic data-plane activation ledger** for the clone+takeover family (§4): + `sys_metadata_activation` — operational row state per packaged artifact. + `sys_metadata` remains the **sole definition ledger**; the activation ledger stores no + definitions, ever. +3. **Install-level scope now, operator-gated writes in multi-org postures, org column reserved** + (§5). +4. **The walls stand** (§6): the #6190 phantom-overlay wall (no `allowOrgOverride` flip on a + behavioral type, under any pressure), the sole-definition-ledger rule, and the + upgrade-vs-choice separation. +5. **Flows are the first consumer** (§7, the worked example); permission sets converge in a later + card; the five overlay types are untouched (§8). + +New per-type customization questions stop arriving at the decision inbox: a new mechanism is +admitted by naming its regime under §3's rule, or it is not admitted. + +--- + +## 1. Context + +### 1.1 What exists today — measured, not recalled + +The survey ([#12049 comment 5406807988](https://github.com/objectstack-ai/objectstack/issues/12049#issuecomment-5406807988)) +enumerated all **27 declared metadata types** (`DEFAULT_METADATA_TYPE_REGISTRY`, +`packages/spec/src/kernel/metadata-plugin.zod.ts:654` — the total universe per #8586, plus the +dynamic-kind channel for plugin-registered kinds such as `connector`). The write door partitions +them into three tiers by flags × item provenance (`packages/metadata-protocol/src/protocol.ts:13227-13260`): + +| Tier | Types | A packaged item can be… | +|:--|:--|:--| +| **A — org-overlayable** | view, dashboard, report, translation, email_template | overlaid env-wide and per-org (ADR-0005; pinned by identity in `protocol.org-scoped-write-refused.test.ts`) | +| **B — runtime-create only** | object, hook, seed, mapping, page, app, action, dataset, flow, datasource, external_catalog, doc, book, permission, position, tool, skill | **not customized at all** — writes against the packaged item answer 403 `NOT_OVERRIDABLE`; only brand-new sibling items can be authored | +| **C — code-only** | field, job, api, capability, agent | nothing — no runtime write channel exists | + +Three real mechanisms operate across those tiers, each invented separately: + +- **Org overlay** (ADR-0005): tier A only. Per-org rows for any other type are **phantom writes** + — the #6190 measured defect (`packages/metadata-core/src/meta-write-org-scope.ts:16-24`). +- **Clone-to-customize** (#11513, permission sets only): server-side lock on the packaged base + (`packaged-permission-set-lock.ts`), a clone action demanding a new name, and — the piece this + ADR generalizes — the ruling that **switching a packaged artifact off is row state, not a + definition write** (`permission-set-projection.ts:1128-1145`, #4669). +- **Extend** (`objectExtensions`, `packages/spec/src/data/object.zod.ts:2920`; navigation + contributions, ADR-0029 D7): a package adds fields/validations/indexes to another package's + object, or nav items to another package's app, merged at boot. Package-grain, additive, + upgrade-safe. + +And one **paper** mechanism: `packages/spec/src/kernel/metadata-customization.zod.ts` declares a +three-layer (system/platform/user) overlay protocol with field-level change tracking — exported, +published in the reference docs, and consumed by **zero** runtime packages +([#12057](https://github.com/objectstack-ai/objectstack/issues/12057)). §6.4 records its +disposition so it cannot be mistaken for this model. + +### 1.2 Why per-type invention had to stop + +#11665 (packaged flows) was about to mint the third regime instance with its own ledger shape. +The maintainer ruled the question platform-wide instead (the charter quotes above): most metadata +types have the same personalization need, so the model is decided **once**, and per-type cards +consume it instead of re-litigating it. + +### 1.3 The shipped promise + +Two published pages promise post-install customization generically — +`content/docs/capabilities/integrations.mdx:17` (*"install complete apps … objects, views, flows, +dashboards, and seed data included, **then customize in Studio**"*) and +`content/docs/build-without-code.mdx:37` (*"ready to customize in Studio"*). Measured against the +tier table, the promise is keepable today for views and dashboards, and for **no tier-B type**. +This is stronger pull than any wish-list: it is a shipped claim the platform cannot keep, and the +flows half of it is the #11665 breach specifically. + +--- + +## 2. Decision rule (D1): regime follows what the artifact DOES + +The regime of a metadata type is decided by one question — **what happens when two versions of +the artifact are live at once, or when a customization is wrong?** + +- **Presentational** (render-time; a wrong overlay renders wrong, side-effect-free) + → **Overlay** (Regime O). Per-org divergence is safe by construction; ADR-0005's admission + pair (overlay schema + written render-only rationale) is the gate. +- **Behavioral** (runs with side effects — fires triggers, writes records, grants access, sends + mail; two live versions double-fire, a silent replacement mis-fires invisibly) + → **Clone + takeover** (Regime C). The packaged base is locked and refused loudly at the write + door; customization is a NEW org/install-owned artifact plus an explicit, ledger-recorded + takeover. ⛔ Never silent override, never an overlay read path. +- **Structural** (schema/wiring the physical world depends on — tables, columns, connections; + per-org divergence diverges DDL) + → **Extend** (Regime E). Additive contributions from another package (`objectExtensions`, + navigation contributions), at package grain. The base is never edited; the extension is itself + a packaged artifact with package provenance. +- **Code-only** types (field, job, api, capability, agent — each closed by its own recorded + ruling) sit **outside** the model: customization = ship a new package version. This ADR does + not reopen any of them. + +A **new metadata type** (or newly admitted dynamic kind) must declare its regime at admission, +in its registry-entry comment, using this rule. A customization mechanism that fits none of the +three regimes is not a gap in this ADR — it is a signal the mechanism is wrong (Prime Directive +#5). Items of dynamic kinds default to Regime C posture (locked base, clone by re-authoring) +until their kind declares otherwise, because "behavioral" is the safe assumption for an artifact +the platform cannot classify. + +## 3. Per-type assignment (D1, continued) + +From the survey's per-type table; classes are the survey's measurements, not aspirations. + +| Regime | Types | Notes | +|:--|:--|:--| +| **O — overlay** | view, dashboard, report, translation, email_template | Unchanged, exactly these five. Promotion into this set is an ADR-0005 admission-pair revision, never a registry edit (the #6483 rollbacks are the precedent). | +| **C — clone + takeover** | **flow** (first consumer, §7) · **permission** (landed machinery, converges later — §8) · pre-charted as pull appears: hook, action, tool, skill, position | All behavioral tier-B types. Each type's implementation is its own card consuming this model; the ledger (§4) is shared. | +| **E — extend** | object (fields/validations/indexes via `objectExtensions`) · app (navigation via ADR-0029 D7 contributions) | Already live. No ledger involvement: the customization IS a package, and package identity supplies provenance and upgrade isolation. | +| **outside** | field, job, api, capability, agent (code-only) · seed, external_catalog (one-shot / derived — nothing durable to customize) · datasource (origin-gated: code-defined read-only, runtime-created free) | Recorded so their absence from the regimes reads as decided, not overlooked. | +| **unassigned, deliberately** | page, dataset, doc, book, mapping | Presentational/content tier-B types with no measured customization pull beyond re-authoring a sibling. Today's posture (locked base + free sibling authoring) stands. First real pull picks a regime **by the D1 rule** — likely O via the ADR-0005 admission pair (page/dataset/book) — and does so in an ADR-0005 revision, not ad hoc. | + +Two clarifications the survey forces: + +- **Managed extension fields** (`plugin-auth/src/managed-extension-fields.ts`) are **not** a + regime: a build-time ownership declaration between ObjectStack and the embedded auth library on + four sys objects. Its collision rule (an extension name must never collide with the base + surface, gated at build) IS adopted as Regime E prior art. +- Regime E is **install-grain, not per-org**. A tenant wanting per-org structural divergence is + asking for a different physical schema per org — refused by the same shared-DB invariant + ADR-0005 records. That refusal is part of the model. + +--- + +## 4. Decision (D2): one generic activation ledger — `sys_metadata_activation` + +The clone+takeover family shares **one data-plane platform object**. Proposed name: +**`sys_metadata_activation`** — it joins the existing data-plane siblings of the definition store +(`sys_metadata_history`, `sys_automation_run`, `sys_flow_dispatch`) and says what it is: the +activation/replacement record for metadata artifacts. It is declared in +`packages/platform-objects` like its siblings, so it needs **zero `packages/spec` surface** +(#11665 §6 item 1). + +| Column | Type | Meaning | +|:--|:--|:--| +| `metadata_type` | string | The artifact's registry type (`'flow'`, `'permission'`, …) | +| `name` | string | The packaged artifact's machine name | +| `package_id` | string | The package that ships the base artifact | +| `organization_id` | string, **nullable — reserved** | NULL today (install-level row, §5); the per-org dimension is an additive column later, never a redesign | +| `active` | boolean | Is the packaged artifact armed for this scope | +| `replaced_by` | string, nullable | Machine name of the org/install-authored replacement artifact, when a takeover designates one | +| `cloned_from` | `{ package, name, version }`, nullable | Provenance of the designated replacement: what it was cloned from and the base's version **at clone time**. Provenance, ⛔ not upgrade linkage — used for nothing at runtime; it is what lets a surface say *"clone based on v3, base now v5"* (§7.4) without diff machinery. | + +Uniqueness: one row per `(metadata_type, name, organization_id NULL-collapsed)`; absence of a row +means the packaged default — **active, not replaced**. The ledger is written by the takeover +actions and read at each runtime's own consult point; an empty ledger changes nothing anywhere. + +**What the ledger is NOT:** + +- ⛔ **Not a definition store.** `sys_metadata` remains the **sole definition ledger** — the + maintainer probed exactly this in the 2026-08-25 discussion, and the recorded grounds are the + #6190 phantom-overlay wall and the upgrade-vs-choice separation (§6). The activation ledger + holds booleans, names and provenance; never fields, nodes, grants, or any fragment of an + artifact body. +- ⛔ **Not a metadata type.** It is an ordinary platform object with ordinary rows: an ordinary + `organization_id` column and an ordinary read path — the construction #6190's measurement + leaves open, where an org-scoped `sys_metadata` row has neither (F3). The #11513 deactivation + carve-out (row state is not a customization of the definition, #4669) is the precedent + validating this plane split. +- **Not a central interceptor.** Consult points stay **per-runtime**: the automation engine + consults it in `execute()` beside the existing `FLOW_DISABLED` guard (#11665 §2.3 — the one + seam every entry path crosses); the permission projection keeps its own row-state door until + convergence (§8). Each consumer documents its consult point; the ledger imposes no global + dispatch layer. + +## 5. Decision (D3): scope and write authority + +- **Install-level rows now.** Every row is written with `organization_id NULL`. For the default + `single` posture — one logical tenant (`packages/spec/src/security/tenancy-posture.ts`) — + install-level and org-level are the same scope, so this is complete for the majority shape at a + fraction of the surface (#11665 Fork A, ①–④ analysis). +- **Operator-gated in multi-org postures.** In `group`/`isolated` postures the ledger write + requires the platform-operator capability: **a tenant org admin must never flip an + install-wide switch**. This is #10243 made durable in the correct direction — that incident + measured a tenant flipping a shipped flow off environment-wide through an unscoped in-process + map; a durable install-wide row writable by tenants would be the same leak with persistence. +- **The org column is reserved, and per-org semantics are pre-charted narrowly:** when a real + multi-org customer asks, per-org takeover is added for **record-change-triggered flows only** + (the one trigger type whose context carries an organization — F4: record-change 5 matches, + schedule/time-relative/api 0). A per-org takeover attempted on any other trigger type **refuses + loudly at the moment of the attempt, naming the trigger type** — never a silent fallback to + install scope. A3 (plumbing an org into the other trigger types) is **not opened**; it is a + product question ("which org does a nightly sweep belong to"), not a plumbing one. + +## 6. Decision (D4): the walls, restated so no implementation card re-derives them + +1. **The #6190 phantom-overlay wall.** Any design that needs `allowOrgOverride` flipped on a + behavioral type has drifted back into #6190 — that is a stop-and-report, not a pin to update. + The identity pin (`protocol.org-scoped-write-refused.test.ts`, exactly five overlay types) + turning red on such a flip is the pin working. Q2's rejected option (b) is this wall's + corollary: takeover-by-name-redirection is an overlay **read** path under another name, and is + rejected on the record (2026-08-25 discussion). +2. **Sole definition ledger.** Definitions live in `sys_metadata` (or the shipped artifact), + nowhere else. A customization that wants to store a definition fragment anywhere but a new + artifact row is out of contract. +3. **Upgrade-vs-choice separation.** Package upgrades rewrite the packaged BASE (definitions); + they never write the ledger — the ledger records the customer's **choices**, and no upgrade + un-makes a choice. The converse holds too: taking over never blocks or edits the base's + upgrade stream. The only place the two meet is the §7.4 factual notice, computed from + `cloned_from.version` at read time. +4. **The paper protocol is not the model.** The three-layer + `metadata-customization.zod.ts` surface (#12057 — exported, documented, zero consumers) is + **superseded by this ADR as a matter of record**: nothing may build against it, and its + retirement (ADR-0049 remove side) or re-scoping to what ADR-0005 actually implements is + chartered as an implementation card. Until that card lands, the reference page it generates + must not be cited as the customization architecture. + +--- + +## 7. The worked example: packaged flows (first consumer) + +Everything in this section is the 2026-08-25 maintainer discussion's tentative direction, +incorporated as required by #12049. **Provenance: live chat; the final ruling is the maintainer's +merge of this ADR — ⛔ none of the following is settled until then.** Implementation stays on +#11665's cards; this section is the contract they consume. + +### 7.1 Clone (Fork C: C1 + C3) + +- **New machine name, mandatory** (the #11513 shape exactly). ⛔ No same-name clone in a second + package: storage legitimately holds both — the uniqueness index keys on + `(type, name, organization_id, COALESCE(package_id, ''))` (ADR-0005 amendment, #6825; ADR-0048 + governs the cross-package coexistence) — and the engine's name-keyed flow map makes the winner + insertion-order: measured as a silent, non-deterministic replacement (#11665 §2.2; #11997 + tracks the shadow diagnostics independently of this model). +- **Whole-definition copy — ⛔ never param-list assembly.** The clone copies the parsed + definition and mutates only `name`/`label`/`status`. #11703 measured the alternative: a clone + assembled from an enumerated facet list silently dropped three of six facets; a flow has far + more facets than a permission set. (#11753 carries the objectui half: carried-over definition + blobs are not editable form fields.) +- **Provenance recorded**: the takeover row's `cloned_from` captures `{package, name, version}` + at clone time (§4). Provenance, not linkage — upgrades keep flowing to the base untouched + (#11513's ruled non-goal stands). + +### 7.2 Takeover (Fork A: A1; Q4: two steps) + +- Two **deliberate** steps: (1) clone; (2) take over — write the ledger row + (`active: false, replaced_by: `). An admin may clone for inspection without changing + runtime behaviour; the half-done state (cloned, not taken over, packaged original still + running) is a **displayed state, never silence** (§7.4). +- Enforcement seam: `execute()`-time refusal beside the existing `FLOW_DISABLED` guard, reusing + the `FLOW_DISABLED` code (no new ADR-0112 ledger entry; the distinction rides the message — + #11665 §6.3). The install-level row may **also** unbind the trigger, preserving today's + documented `toggleFlow` semantics; a future per-org row cannot unbind (the hook is registered + once env-wide) and is an entry-time refusal only — a stated cost, accepted. +- The durable ledger row **replaces** the process-local `flowEnabled` map as the sanctioned + off-switch for packaged flows, retiring the #10243 leak's mechanism rather than refining it. + +### 7.3 The subflow cascade (Q2: option (c)) + +A takeover of flow B is **refused while any packaged flow references B as a subflow, and the +refusal names the callers**. Rationale on the record: option (a) ships a silent late failure +inside the caller; option (b) — resolving `execute('B')` to the replacement — is name +redirection, rejected under wall §6.1. The refusal is honest, actionable (take over the callers +first, or don't), and preserves "packaged code calls what it names". + +### 7.4 The surface (Fork D: leaning D2; Q3) + +- **Leaning D2 — a Setup page for packaged automation** (final call rides this ADR's merge): + packaged flows with their activation state and the clone/takeover actions, contributed the way + `nav_permission_sets` is (`security-plugin.ts:974`). The maintainer corrected the record here: + automation UI is **Studio-only today** (`studio.app.ts:234-239`), so this page is new work, and + the Setup permission-set page is the precedent shape. Studio keeps the editing; Setup gets the + operational state. Minimum honest content per packaged flow: on/off for this scope · designated + replacement, if any · the §7.1 provenance. +- **Q3 — a one-line factual notice, no diff machinery**: *"clone based on v3, base now v5"*, + computed from `cloned_from.version` against the installed base version. ⛔ No diff-vs-base, no + `drift_status` columns, no "Needs Attention" view — that is the ServiceNow-style layer the + 2026-08-24 permission-set ruling recorded as deliberately unchartered, and it stays deferred + (D3 in #11665's fork D table). Saying "no drift information exists" plainly beats a + `customized` flag that the #11513 precedent measured going silently wrong. + +--- + +## 8. Decision (D5): convergence plan + +1. **Flows first** — #11665's implementation cards consume §7 once this ADR merges (that card is + `Blocked-by:` #12049 by its own record). +2. **Permission sets converge in a later card.** The landed #11513 machinery (lock, clone action, + row-state `active`, drift detection of the enforced copy) **stays valid meanwhile** — it is + the regime's first instance, not a violation of it. The convergence card decides how + `sys_permission_set.active` and the generic ledger relate (projection vs migration), and it + inherits §4's walls; nothing about it is urgent, because the semantics already match. +3. **The five overlay types are untouched.** No overlay-type work is chartered, implied, or + permitted by this ADR. +4. **Docs**: the §1.3 promise pages are corrected to promise what each regime actually delivers + (own card, docs lane) once the flow surface exists — not before, which would trade an + over-promise for a differently-shaped one. +5. **The paper protocol** (#12057) gets its enforce-or-remove card per §6.4. + +## 9. Explicitly not chartered (recorded so silence cannot be read as consent) + +- The ServiceNow-style overlay layer (badge / customization list / diff-vs-base / revert / + upgrade skip-report) — recorded as the mature direction **if** customer pull for in-place + customization appears; deferred again here. +- A3 (org-scoping the schedule / time-relative / api trigger contexts). +- Automatic re-pointing of references on clone (no reference index exists — #11665 §3.2; the + clone's references stay pointed at what the original pointed at, and the surface tells the + admin so). +- Reopening any code-only type (field, job, api, capability, agent). +- Any edit to `packages/spec` for the ledger itself (it is a platform object; §4). + +## 10. Consequences + +- A per-type customization request stops being a design question: classify by §2, apply the + regime, consume §4's ledger if Regime C. The decision inbox sees new customization questions + only when a type resists the D1 rule — which is the signal worth a human ruling anyway. +- The published "customize in Studio" promise becomes narrow-true (tier A), then broader-true as + Regime C surfaces land, instead of broadly false. +- Two silent-failure classes are structurally closed for Regime C types: the double-fire (clone + running beside the base) and the silent replacement (same-name shadow), both measured in + #11665. The half-done state remains possible by design (Q4: two steps) — but as a displayed + state, never a silent one. +- AI authors get one rule per regime instead of per-type folklore: a locked base that refuses + loudly with the sanctioned path in the refusal message is the shape that keeps AI-written + metadata from guessing (the axis-③ analysis in #11665's fork recommendations, adopted). + +## Refs + +#12049 (charter + survey) · #11665 (flow design + discussion record) · #11513 / #11702 / #11703 / +#11753 (permission-set precedent) · #6190 (phantom overlay) · #10243 (env-wide toggle leak) · +#11997 (same-name shadow diagnostics) · #12057 (paper protocol) · ADR-0005 · ADR-0029 · ADR-0048 · +ADR-0049 · ADR-0087 · ADR-0088 · ADR-0105 · ADR-0112 From 07f4e024d43eb4a13b617cfafcf4c35abda48f9e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 25 Aug 2026 08:45:04 +0000 Subject: [PATCH 2/3] =?UTF-8?q?docs(adr):=20ADR-0126=20amendments=20?= =?UTF-8?q?=E2=80=94=20hook=20reclassified=20as=20code-class;=20Regime=20C?= =?UTF-8?q?=20simplified=20to=20enable/disable=20+=20clone,=20no=20designa?= =?UTF-8?q?tion=20linkage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Records the two 2026-08-25 maintainer amendment rulings verbatim in the Deciders block. Ruling 1 moves hook out of the model (package code, no ledger row, no disable switch — the job posture; sibling authoring stays open). Ruling 2 drops replaced_by and cloned_from from the ledger: the schema is (metadata_type, name, package_id, organization_id reserved, active); clone and disable become independent primitives; the subflow guard attaches to disable; the base-moved notice is withdrawn and clone ancestry is recorded as deliberately untracked. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01KWRU3s15AJz7PGW7a7wdCh --- ...6-packaged-metadata-customization-model.md | 181 +++++++++++------- 1 file changed, 109 insertions(+), 72 deletions(-) diff --git a/docs/adr/0126-packaged-metadata-customization-model.md b/docs/adr/0126-packaged-metadata-customization-model.md index c3393c6746..dfc3d61a22 100644 --- a/docs/adr/0126-packaged-metadata-customization-model.md +++ b/docs/adr/0126-packaged-metadata-customization-model.md @@ -8,6 +8,14 @@ until this merges). **Deciders**: maintainer charter, 2026-08-25, live PM chat, verbatim and untranslated: 「要整体评估除了流程还有哪些需要自定义的也有类似的问题,开始写adr」, following 「很多所有的元数据都有类似的个性化需求,要统一考虑。」 +**Amendment rulings** (maintainer, 2026-08-25, live PM chat, after reading the first draft — +verbatim and untranslated, both incorporated throughout this revision): +1. 「hook 也算代码类吧,我觉得也不需要修改。」 — `hook` is reclassified out of the model + (§2, §3): packaged hooks are package code, no ledger rows, no disable switch. +2. 「行为类 能否搞一个启用停用的功能,我不想要可以停用,然后克隆一个。」 — Regime C is + simplified to two independent primitives, enable/disable + clone-as-sibling, with **no + designation linkage** (§2, §4, §7): `replaced_by` and `cloned_from` are dropped from the + ledger; the clone is an ordinary artifact with no recorded linkage to its base. **Builds on**: [ADR-0005](./0005-metadata-customization-overlay.md) (org overlay + the two-tier write model), [ADR-0029](./0029-kernel-object-ownership-and-platform-objects-decomposition.md) D7/D9 (contributor kinds, navigation contributions), @@ -37,12 +45,12 @@ customize** for permission sets, package-grain **extend** for objects and app na each was invented per type, with its own ledger. This ADR writes the model down once: 1. **Three regimes, assigned by what the artifact does** (§3): presentational → overlay; - behavioral → clone + takeover, ⛔ never silent override; structural → extend. Code-only types - sit outside customization entirely. -2. **One generic data-plane activation ledger** for the clone+takeover family (§4): - `sys_metadata_activation` — operational row state per packaged artifact. + behavioral → locked base + **enable/disable** + **clone-as-sibling**, ⛔ never silent + override; structural → extend. Code-only types sit outside customization entirely. +2. **One generic data-plane activation ledger** for the disable+clone family (§4): + `sys_metadata_activation` — operational row state per packaged artifact, nothing more. `sys_metadata` remains the **sole definition ledger**; the activation ledger stores no - definitions, ever. + definitions and records no clone linkage, ever. 3. **Install-level scope now, operator-gated writes in multi-org postures, org column reserved** (§5). 4. **The walls stand** (§6): the #6190 phantom-overlay wall (no `allowOrgOverride` flip on a @@ -120,9 +128,13 @@ the artifact are live at once, or when a customization is wrong?** pair (overlay schema + written render-only rationale) is the gate. - **Behavioral** (runs with side effects — fires triggers, writes records, grants access, sends mail; two live versions double-fire, a silent replacement mis-fires invisibly) - → **Clone + takeover** (Regime C). The packaged base is locked and refused loudly at the write - door; customization is a NEW org/install-owned artifact plus an explicit, ledger-recorded - takeover. ⛔ Never silent override, never an overlay read path. + → **Disable + clone** (Regime C — amendment ruling 2, the landed #11513 shape generalized). + Three independent parts, no ceremony connecting them: the packaged base is **locked** — + in-place edit refused loudly at the write door, the refusal naming the sanctioned path; each + packaged behavioral artifact carries an **enable/disable switch** — a ledger row flip (§4), + operator-gated per §5; customization is **authoring an ordinary sibling artifact** (clone or + from scratch; new machine name mandatory, whole-definition copy — §7.1). There is no + designated-replacement linkage. ⛔ Never silent override, never an overlay read path. - **Structural** (schema/wiring the physical world depends on — tables, columns, connections; per-org divergence diverges DDL) → **Extend** (Regime E). Additive contributions from another package (`objectExtensions`, @@ -130,7 +142,12 @@ the artifact are live at once, or when a customization is wrong?** a packaged artifact with package provenance. - **Code-only** types (field, job, api, capability, agent — each closed by its own recorded ruling) sit **outside** the model: customization = ship a new package version. This ADR does - not reopen any of them. + not reopen any of them. **`hook` joins them by amendment ruling 1** (「hook 也算代码类吧, + 我觉得也不需要修改。」): a hook body is imperative package code maintaining the package's own + data invariants — the managed-package trigger analogy (Salesforce locks managed Apex triggers; + customization = the vendor ships a new version). Packaged hooks get **no ledger rows and no + disable switch** — the same posture as `job`. The registry flags are untouched: authoring a + brand-new sibling hook stays open, exactly as today. A **new metadata type** (or newly admitted dynamic kind) must declare its regime at admission, in its registry-entry comment, using this rule. A customization mechanism that fits none of the @@ -146,9 +163,9 @@ From the survey's per-type table; classes are the survey's measurements, not asp | Regime | Types | Notes | |:--|:--|:--| | **O — overlay** | view, dashboard, report, translation, email_template | Unchanged, exactly these five. Promotion into this set is an ADR-0005 admission-pair revision, never a registry edit (the #6483 rollbacks are the precedent). | -| **C — clone + takeover** | **flow** (first consumer, §7) · **permission** (landed machinery, converges later — §8) · pre-charted as pull appears: hook, action, tool, skill, position | All behavioral tier-B types. Each type's implementation is its own card consuming this model; the ledger (§4) is shared. | +| **C — disable + clone** | **flow** (first consumer, §7) · **permission** (landed machinery, converges later — §8) · pre-charted as pull appears: action, tool, skill, position | Behavioral tier-B types (minus `hook`, reclassified out by amendment ruling 1). Each type's implementation is its own card consuming this model; the ledger (§4) is shared. | | **E — extend** | object (fields/validations/indexes via `objectExtensions`) · app (navigation via ADR-0029 D7 contributions) | Already live. No ledger involvement: the customization IS a package, and package identity supplies provenance and upgrade isolation. | -| **outside** | field, job, api, capability, agent (code-only) · seed, external_catalog (one-shot / derived — nothing durable to customize) · datasource (origin-gated: code-defined read-only, runtime-created free) | Recorded so their absence from the regimes reads as decided, not overlooked. | +| **outside** | field, job, api, capability, agent (code-only) · **hook** (amendment ruling 1 — package code maintaining the package's own invariants; no ledger row, no disable switch; sibling authoring open as today) · seed, external_catalog (one-shot / derived — nothing durable to customize) · datasource (origin-gated: code-defined read-only, runtime-created free) | Recorded so their absence from the regimes reads as decided, not overlooked. | | **unassigned, deliberately** | page, dataset, doc, book, mapping | Presentational/content tier-B types with no measured customization pull beyond re-authoring a sibling. Today's posture (locked base + free sibling authoring) stands. First real pull picks a regime **by the D1 rule** — likely O via the ADR-0005 admission pair (page/dataset/book) — and does so in an ADR-0005 revision, not ad hoc. | Two clarifications the survey forces: @@ -165,10 +182,10 @@ Two clarifications the survey forces: ## 4. Decision (D2): one generic activation ledger — `sys_metadata_activation` -The clone+takeover family shares **one data-plane platform object**. Proposed name: +The disable+clone family shares **one data-plane platform object**. Proposed name: **`sys_metadata_activation`** — it joins the existing data-plane siblings of the definition store (`sys_metadata_history`, `sys_automation_run`, `sys_flow_dispatch`) and says what it is: the -activation/replacement record for metadata artifacts. It is declared in +activation record for packaged metadata artifacts. It is declared in `packages/platform-objects` like its siblings, so it needs **zero `packages/spec` surface** (#11665 §6 item 1). @@ -179,20 +196,22 @@ activation/replacement record for metadata artifacts. It is declared in | `package_id` | string | The package that ships the base artifact | | `organization_id` | string, **nullable — reserved** | NULL today (install-level row, §5); the per-org dimension is an additive column later, never a redesign | | `active` | boolean | Is the packaged artifact armed for this scope | -| `replaced_by` | string, nullable | Machine name of the org/install-authored replacement artifact, when a takeover designates one | -| `cloned_from` | `{ package, name, version }`, nullable | Provenance of the designated replacement: what it was cloned from and the base's version **at clone time**. Provenance, ⛔ not upgrade linkage — used for nothing at runtime; it is what lets a surface say *"clone based on v3, base now v5"* (§7.4) without diff machinery. | +That is the whole schema — amendment ruling 2 (「行为类 能否搞一个启用停用的功能,我不想要 +可以停用,然后克隆一个。」) removed the designation columns an earlier draft carried +(`replaced_by`, `cloned_from`): there is **no recorded linkage** between a clone and its base, +matching the landed #11513 posture ("an ordinary org-owned set with no upgrade linkage"). Uniqueness: one row per `(metadata_type, name, organization_id NULL-collapsed)`; absence of a row -means the packaged default — **active, not replaced**. The ledger is written by the takeover -actions and read at each runtime's own consult point; an empty ledger changes nothing anywhere. +means the packaged default — **active**. The ledger is written by the enable/disable actions and +read at each runtime's own consult point; an empty ledger changes nothing anywhere. **What the ledger is NOT:** - ⛔ **Not a definition store.** `sys_metadata` remains the **sole definition ledger** — the maintainer probed exactly this in the 2026-08-25 discussion, and the recorded grounds are the #6190 phantom-overlay wall and the upgrade-vs-choice separation (§6). The activation ledger - holds booleans, names and provenance; never fields, nodes, grants, or any fragment of an - artifact body. + holds a boolean per artifact and nothing else; never fields, nodes, grants, or any fragment of + an artifact body. - ⛔ **Not a metadata type.** It is an ordinary platform object with ordinary rows: an ordinary `organization_id` column and an ordinary read path — the construction #6190's measurement leaves open, where an org-scoped `sys_metadata` row has neither (F3). The #11513 deactivation @@ -216,9 +235,9 @@ actions and read at each runtime's own consult point; an empty ledger changes no measured a tenant flipping a shipped flow off environment-wide through an unscoped in-process map; a durable install-wide row writable by tenants would be the same leak with persistence. - **The org column is reserved, and per-org semantics are pre-charted narrowly:** when a real - multi-org customer asks, per-org takeover is added for **record-change-triggered flows only** + multi-org customer asks, per-org disable is added for **record-change-triggered flows only** (the one trigger type whose context carries an organization — F4: record-change 5 matches, - schedule/time-relative/api 0). A per-org takeover attempted on any other trigger type **refuses + schedule/time-relative/api 0). A per-org disable attempted on any other trigger type **refuses loudly at the moment of the attempt, naming the trigger type** — never a silent fallback to install scope. A3 (plumbing an org into the other trigger types) is **not opened**; it is a product question ("which org does a nightly sweep belong to"), not a plumbing one. @@ -228,17 +247,18 @@ actions and read at each runtime's own consult point; an empty ledger changes no 1. **The #6190 phantom-overlay wall.** Any design that needs `allowOrgOverride` flipped on a behavioral type has drifted back into #6190 — that is a stop-and-report, not a pin to update. The identity pin (`protocol.org-scoped-write-refused.test.ts`, exactly five overlay types) - turning red on such a flip is the pin working. Q2's rejected option (b) is this wall's - corollary: takeover-by-name-redirection is an overlay **read** path under another name, and is - rejected on the record (2026-08-25 discussion). + turning red on such a flip is the pin working. This wall's corollary stands too: + name-redirection — resolving a packaged artifact's name to some other artifact at dispatch — + is an overlay **read** path under another name, rejected on the record (2026-08-25 + discussion); with no designation linkage in the model (§4) there is also nothing to redirect + to. 2. **Sole definition ledger.** Definitions live in `sys_metadata` (or the shipped artifact), nowhere else. A customization that wants to store a definition fragment anywhere but a new artifact row is out of contract. 3. **Upgrade-vs-choice separation.** Package upgrades rewrite the packaged BASE (definitions); they never write the ledger — the ledger records the customer's **choices**, and no upgrade - un-makes a choice. The converse holds too: taking over never blocks or edits the base's - upgrade stream. The only place the two meet is the §7.4 factual notice, computed from - `cloned_from.version` at read time. + un-makes a choice. The converse holds too: disabling never blocks or edits the base's upgrade + stream. The two never meet: the base's version history and the ledger share no columns. 4. **The paper protocol is not the model.** The three-layer `metadata-customization.zod.ts` surface (#12057 — exported, documented, zero consumers) is **superseded by this ADR as a matter of record**: nothing may build against it, and its @@ -250,12 +270,15 @@ actions and read at each runtime's own consult point; an empty ledger changes no ## 7. The worked example: packaged flows (first consumer) -Everything in this section is the 2026-08-25 maintainer discussion's tentative direction, -incorporated as required by #12049. **Provenance: live chat; the final ruling is the maintainer's -merge of this ADR — ⛔ none of the following is settled until then.** Implementation stays on -#11665's cards; this section is the contract they consume. +Everything in this section is the 2026-08-25 maintainer discussion's tentative direction, **as +amended by the same-day rulings quoted in the header** (which supersede the earlier discussion +record where they conflict: the C3 provenance pointer, the Q3 base-moved notice, and the Q4 +two-step takeover ceremony are all withdrawn; A1, C1 and the loud-refusal walls stand). +**Provenance: live chat; the final ruling is the maintainer's merge of this ADR — ⛔ none of the +following is settled until then.** Implementation stays on #11665's cards; this section is the +contract they consume. -### 7.1 Clone (Fork C: C1 + C3) +### 7.1 Clone (Fork C: C1, without C3) - **New machine name, mandatory** (the #11513 shape exactly). ⛔ No same-name clone in a second package: storage legitimately holds both — the uniqueness index keys on @@ -268,47 +291,53 @@ merge of this ADR — ⛔ none of the following is settled until then.** Impleme assembled from an enumerated facet list silently dropped three of six facets; a flow has far more facets than a permission set. (#11753 carries the objectui half: carried-over definition blobs are not editable form fields.) -- **Provenance recorded**: the takeover row's `cloned_from` captures `{package, name, version}` - at clone time (§4). Provenance, not linkage — upgrades keep flowing to the base untouched - (#11513's ruled non-goal stands). - -### 7.2 Takeover (Fork A: A1; Q4: two steps) - -- Two **deliberate** steps: (1) clone; (2) take over — write the ledger row - (`active: false, replaced_by: `). An admin may clone for inspection without changing - runtime behaviour; the half-done state (cloned, not taken over, packaged original still - running) is a **displayed state, never silence** (§7.4). +- **No linkage to the base** (amendment ruling 2): the clone is an ordinary org/install-owned + flow with no recorded relationship to what it was copied from — exactly #11513's "ordinary + org-owned set with no upgrade linkage". Upgrades keep flowing to the base untouched; nothing + records, tracks, or reports the copy's ancestry (§9). + +### 7.2 Enable / disable (Fork A: A1) + +- **A single deliberate act**: flipping a packaged flow off (or back on) writes the ledger row's + `active` bit — no clone required first, no designation to record, operator-gated per §5. Clone + and disable are **independent primitives**: cloned-without-disabled and disabled-without-clone + are both ordinary states the surface shows plainly (§7.4), not halves of an unfinished + ceremony. - Enforcement seam: `execute()`-time refusal beside the existing `FLOW_DISABLED` guard, reusing the `FLOW_DISABLED` code (no new ADR-0112 ledger entry; the distinction rides the message — #11665 §6.3). The install-level row may **also** unbind the trigger, preserving today's - documented `toggleFlow` semantics; a future per-org row cannot unbind (the hook is registered - once env-wide) and is an entry-time refusal only — a stated cost, accepted. + documented `toggleFlow` semantics; a future per-org row cannot unbind (the trigger's event + hook is registered once env-wide) and is an entry-time refusal only — a stated cost, accepted. - The durable ledger row **replaces** the process-local `flowEnabled` map as the sanctioned off-switch for packaged flows, retiring the #10243 leak's mechanism rather than refining it. -### 7.3 The subflow cascade (Q2: option (c)) +### 7.3 The subflow cascade (Q2: option (c), attached to disable) -A takeover of flow B is **refused while any packaged flow references B as a subflow, and the -refusal names the callers**. Rationale on the record: option (a) ships a silent late failure -inside the caller; option (b) — resolving `execute('B')` to the replacement — is name -redirection, rejected under wall §6.1. The refusal is honest, actionable (take over the callers -first, or don't), and preserves "packaged code calls what it names". +**Disabling flow B is refused while any packaged flow references B as a subflow, and the refusal +names the callers** — otherwise a vendor flow breaks mid-run at its subflow node with an +inexplicable failure (`subflow 'B' failed: Flow 'B' is disabled`, composed from `execute()`'s +own refusal). Rationale on the record: silently letting the caller fail late (the Q2(a) shape) +ships exactly the class of invisible breakage this regime exists to close, and name-redirection +(the Q2(b) shape) is rejected under wall §6.1. The refusal is honest, actionable (disable the +callers first, or don't), and preserves "packaged code calls what it names". -### 7.4 The surface (Fork D: leaning D2; Q3) +### 7.4 The surface (Fork D: leaning D2) - **Leaning D2 — a Setup page for packaged automation** (final call rides this ADR's merge): - packaged flows with their activation state and the clone/takeover actions, contributed the way - `nav_permission_sets` is (`security-plugin.ts:974`). The maintainer corrected the record here: - automation UI is **Studio-only today** (`studio.app.ts:234-239`), so this page is new work, and - the Setup permission-set page is the precedent shape. Studio keeps the editing; Setup gets the - operational state. Minimum honest content per packaged flow: on/off for this scope · designated - replacement, if any · the §7.1 provenance. -- **Q3 — a one-line factual notice, no diff machinery**: *"clone based on v3, base now v5"*, - computed from `cloned_from.version` against the installed base version. ⛔ No diff-vs-base, no - `drift_status` columns, no "Needs Attention" view — that is the ServiceNow-style layer the - 2026-08-24 permission-set ruling recorded as deliberately unchartered, and it stays deferred - (D3 in #11665's fork D table). Saying "no drift information exists" plainly beats a - `customized` flag that the #11513 precedent measured going silently wrong. + packaged flows with their activation state and the enable/disable and clone actions, + contributed the way `nav_permission_sets` is (`security-plugin.ts:974`). The maintainer + corrected the record here: automation UI is **Studio-only today** (`studio.app.ts:234-239`), + so this page is new work, and the Setup permission-set page is the precedent shape. Studio + keeps the editing; Setup gets the operational state. Minimum honest content per packaged flow: + **on/off for this scope** — that is all the ledger knows (§4), and the page claims nothing + more. +- ⛔ **No drift or ancestry surface**: no diff-vs-base, no `drift_status` columns, no "Needs + Attention" view, and — with `cloned_from` withdrawn (amendment ruling 2) — no base-moved + notice either; the earlier draft's *"clone based on v3, base now v5"* line has no data source + and is removed (§9). That is the ServiceNow-style layer the 2026-08-24 permission-set ruling + recorded as deliberately unchartered, and it stays deferred (D3 in #11665's fork D table). + Saying "no drift information exists" plainly beats a `customized` flag that the #11513 + precedent measured going silently wrong. --- @@ -316,11 +345,13 @@ first, or don't), and preserves "packaged code calls what it names". 1. **Flows first** — #11665's implementation cards consume §7 once this ADR merges (that card is `Blocked-by:` #12049 by its own record). -2. **Permission sets converge in a later card.** The landed #11513 machinery (lock, clone action, - row-state `active`, drift detection of the enforced copy) **stays valid meanwhile** — it is - the regime's first instance, not a violation of it. The convergence card decides how - `sys_permission_set.active` and the generic ledger relate (projection vs migration), and it - inherits §4's walls; nothing about it is urgent, because the semantics already match. +2. **Permission sets converge in a later card — and the amendment made it simpler.** The landed + #11513 machinery (lock, clone action, row-state `active`, drift detection of the enforced + copy) **stays valid meanwhile** — it is the regime's first instance, not a violation of it, + and with ruling 2 the regime's semantics are now **identical** to what already shipped + (active row state + free clone, no linkage). The convergence card decides one thing only: how + `sys_permission_set.active` and the generic ledger relate (projection vs migration). Nothing + about it is urgent. 3. **The five overlay types are untouched.** No overlay-type work is chartered, implied, or permitted by this ADR. 4. **Docs**: the §1.3 promise pages are corrected to promise what each regime actually delivers @@ -333,6 +364,12 @@ first, or don't), and preserves "packaged code calls what it names". - The ServiceNow-style overlay layer (badge / customization list / diff-vs-base / revert / upgrade skip-report) — recorded as the mature direction **if** customer pull for in-place customization appears; deferred again here. +- **Clone provenance and any base-moved notice** (amendment ruling 2): a clone's ancestry is + deliberately not tracked — no `cloned_from` column, no *"clone based on v3, base now v5"* + line — consistent with the deferred ServiceNow-style layer above and with the landed + permission-set posture, where clone-vs-base drift is deliberately absent + (`permission-set-drift.ts` covers only the enforced copy of the base against its own + artifact). - A3 (org-scoping the schedule / time-relative / api trigger contexts). - Automatic re-pointing of references on clone (no reference index exists — #11665 §3.2; the clone's references stay pointed at what the original pointed at, and the surface tells the @@ -347,10 +384,10 @@ first, or don't), and preserves "packaged code calls what it names". only when a type resists the D1 rule — which is the signal worth a human ruling anyway. - The published "customize in Studio" promise becomes narrow-true (tier A), then broader-true as Regime C surfaces land, instead of broadly false. -- Two silent-failure classes are structurally closed for Regime C types: the double-fire (clone - running beside the base) and the silent replacement (same-name shadow), both measured in - #11665. The half-done state remains possible by design (Q4: two steps) — but as a displayed - state, never a silent one. +- Two silent-failure classes are structurally closed for Regime C types, both measured in + #11665: the double-fire is closed by **disable** (the packaged original can actually be + switched off, durably), and the silent replacement is closed by **name uniqueness plus the + no-redirection wall** (a clone can never shadow or capture its base's name). - AI authors get one rule per regime instead of per-type folklore: a locked base that refuses loudly with the sanctioned path in the refusal message is the shape that keeps AI-written metadata from guessing (the axis-③ analysis in #11665's fork recommendations, adopted). From a09d035a8adeece8958d0452d1c57bda20f89faa Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 25 Aug 2026 09:39:56 +0000 Subject: [PATCH 3/3] =?UTF-8?q?docs(adr):=20ADR-0126=20amendment=20ruling?= =?UTF-8?q?=203=20=E2=80=94=20action=20stays=20Regime=20C;=20its=20disable?= =?UTF-8?q?=20leg=20follows=20the=20flow=20legs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Records the third 2026-08-25 maintainer ruling verbatim in the Deciders block. action does not follow hook out of the model: internal package plumbing is code-only, customer-facing business behavior is disable + clone. The quote is first-party pull for the disable half, so the action-disable leg (same sys_metadata_activation ledger, same write authority, a consult point at action dispatch, a surface row) is chartered to follow the flow legs; the clone half stays pre-charted. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01KWRU3s15AJz7PGW7a7wdCh --- ...6-packaged-metadata-customization-model.md | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/docs/adr/0126-packaged-metadata-customization-model.md b/docs/adr/0126-packaged-metadata-customization-model.md index dfc3d61a22..6ca6dd1d25 100644 --- a/docs/adr/0126-packaged-metadata-customization-model.md +++ b/docs/adr/0126-packaged-metadata-customization-model.md @@ -9,13 +9,17 @@ until this merges). 「要整体评估除了流程还有哪些需要自定义的也有类似的问题,开始写adr」, following 「很多所有的元数据都有类似的个性化需求,要统一考虑。」 **Amendment rulings** (maintainer, 2026-08-25, live PM chat, after reading the first draft — -verbatim and untranslated, both incorporated throughout this revision): +verbatim and untranslated, all incorporated throughout this revision): 1. 「hook 也算代码类吧,我觉得也不需要修改。」 — `hook` is reclassified out of the model (§2, §3): packaged hooks are package code, no ledger rows, no disable switch. 2. 「行为类 能否搞一个启用停用的功能,我不想要可以停用,然后克隆一个。」 — Regime C is simplified to two independent primitives, enable/disable + clone-as-sibling, with **no designation linkage** (§2, §4, §7): `replaced_by` and `cloned_from` are dropped from the ledger; the clone is an ordinary artifact with no recorded linkage to its base. +3. 「动作 可能是需要开关的,因为有的 action 我不想启用。」 — `action` stays in Regime C + (§3, §8): its **disable** half has first-party maintainer pull on record and follows the + flow legs when the post-merge implementation cards are cut; its **clone** half stays + pre-charted, waiting for real pull. **Builds on**: [ADR-0005](./0005-metadata-customization-overlay.md) (org overlay + the two-tier write model), [ADR-0029](./0029-kernel-object-ownership-and-platform-objects-decomposition.md) D7/D9 (contributor kinds, navigation contributions), @@ -163,7 +167,7 @@ From the survey's per-type table; classes are the survey's measurements, not asp | Regime | Types | Notes | |:--|:--|:--| | **O — overlay** | view, dashboard, report, translation, email_template | Unchanged, exactly these five. Promotion into this set is an ADR-0005 admission-pair revision, never a registry edit (the #6483 rollbacks are the precedent). | -| **C — disable + clone** | **flow** (first consumer, §7) · **permission** (landed machinery, converges later — §8) · pre-charted as pull appears: action, tool, skill, position | Behavioral tier-B types (minus `hook`, reclassified out by amendment ruling 1). Each type's implementation is its own card consuming this model; the ledger (§4) is shared. | +| **C — disable + clone** | **flow** (first consumer, §7) · **permission** (landed machinery, converges later — §8) · **action** (disable half: maintainer pull on record 2026-08-25, amendment ruling 3 — follows the flow legs, §8; clone half pre-charted) · pre-charted as pull appears: tool, skill, position | Behavioral tier-B types (minus `hook`, reclassified out by amendment ruling 1; ruling 3 settles the boundary the pair leaves: internal package plumbing → code-only, customer-facing business behavior → disable + clone). Each type's implementation is its own card consuming this model; the ledger (§4) is shared. | | **E — extend** | object (fields/validations/indexes via `objectExtensions`) · app (navigation via ADR-0029 D7 contributions) | Already live. No ledger involvement: the customization IS a package, and package identity supplies provenance and upgrade isolation. | | **outside** | field, job, api, capability, agent (code-only) · **hook** (amendment ruling 1 — package code maintaining the package's own invariants; no ledger row, no disable switch; sibling authoring open as today) · seed, external_catalog (one-shot / derived — nothing durable to customize) · datasource (origin-gated: code-defined read-only, runtime-created free) | Recorded so their absence from the regimes reads as decided, not overlooked. | | **unassigned, deliberately** | page, dataset, doc, book, mapping | Presentational/content tier-B types with no measured customization pull beyond re-authoring a sibling. Today's posture (locked base + free sibling authoring) stands. First real pull picks a regime **by the D1 rule** — likely O via the ADR-0005 admission pair (page/dataset/book) — and does so in an ADR-0005 revision, not ad hoc. | @@ -345,19 +349,24 @@ callers first, or don't), and preserves "packaged code calls what it names". 1. **Flows first** — #11665's implementation cards consume §7 once this ADR merges (that card is `Blocked-by:` #12049 by its own record). -2. **Permission sets converge in a later card — and the amendment made it simpler.** The landed +2. **The action-disable leg follows the flow legs** (amendment ruling 3 — 「动作 可能是需要开关 + 的,因为有的 action 我不想启用。」, the first-party pull on record): same + `sys_metadata_activation` ledger, same §5 write authority, a consult point at action dispatch, + a surface row. ⛔ The action-**clone** half is not chartered — it stays on §3's pre-chart + discipline until real pull appears. +3. **Permission sets converge in a later card — and the amendment made it simpler.** The landed #11513 machinery (lock, clone action, row-state `active`, drift detection of the enforced copy) **stays valid meanwhile** — it is the regime's first instance, not a violation of it, and with ruling 2 the regime's semantics are now **identical** to what already shipped (active row state + free clone, no linkage). The convergence card decides one thing only: how `sys_permission_set.active` and the generic ledger relate (projection vs migration). Nothing about it is urgent. -3. **The five overlay types are untouched.** No overlay-type work is chartered, implied, or +4. **The five overlay types are untouched.** No overlay-type work is chartered, implied, or permitted by this ADR. -4. **Docs**: the §1.3 promise pages are corrected to promise what each regime actually delivers +5. **Docs**: the §1.3 promise pages are corrected to promise what each regime actually delivers (own card, docs lane) once the flow surface exists — not before, which would trade an over-promise for a differently-shaped one. -5. **The paper protocol** (#12057) gets its enforce-or-remove card per §6.4. +6. **The paper protocol** (#12057) gets its enforce-or-remove card per §6.4. ## 9. Explicitly not chartered (recorded so silence cannot be read as consent)