Skip to content

Commit 9cde14a

Browse files
os-zhuangclaude
andauthored
docs(adr): ADR-0069 package-first authoring (Airtable base model) (#2277)
Every runtime-authored metadata item must live in a writable package (base); no orphans. Code/installed packages are read-only; the package is the delete/export/duplicate unit. Unifies the contract across the kernel (saveMetaItem), Studio UI, and ../cloud service-ai-studio AI authoring. Retires the #2252 (coerce-to-null) and #1946 (Local scope) stopgaps. Drafted from a business-user Studio dogfood pass. Status: Proposed. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 84784fc commit 9cde14a

1 file changed

Lines changed: 104 additions & 0 deletions

File tree

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# ADR-0069: Package-first authoring — every runtime-authored item lives in a writable package (base); no orphans
2+
3+
**Status**: Proposed (2026-06-24)
4+
**Deciders**: ObjectStack Protocol Architects
5+
**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)
6+
**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`)
7+
8+
**Premise**: pre-launch — specify the target end-state. The platform already has the **package** primitive (ADR-0048) and a runtime overlay store (ADR-0005); what is missing is a *contract* that ties every runtime-authored item to a writable package and forbids package-less ("orphan") metadata. The **Airtable model** is the north star: you create a **base** (= a writable software package) first, then author objects/fields/views/flows **into it**; the base is the unit you organise, version, export, and **delete**. Items below are tagged **[existing]** / **[new]**.
9+
10+
> **Trigger**: a business-user dogfood pass of Studio surfaced a family of related defects whose common root is the *absence of a package-first contract*:
11+
> 1. **Read-only-after-publish** ([framework#2252](https://github.com/objectstack-ai/framework/pull/2252)): authoring a new object while a *code* package was selected stamped the new row with that code package's `package_id`, so it read back as "code-provided" and locked itself read-only the moment it was published — the user could not edit what they had just created.
12+
> 2. **The orphan pile / no delete unit** ([objectui#1946](https://github.com/objectstack-ai/objectui/pull/1946)): the band-aid for (1) coerces such writes to `package_id = null`. That correctly unblocks editing, but it scatters runtime metadata into a package-less bucket that the package-scoped Studio lists filter *out* — items become hard to find and, critically, **there is no container to delete**: "how do I clean up a pile of loose metadata?"
13+
> 3. **AI authoring can still orphan**: `../cloud`'s `service-ai-studio` resolves a target package with smart fallbacks (active package -> single-app package -> **auto-created `com.workspace`**) and rejects read-only code packages, but it never *requires* the user/agent to choose a base first — so it can silently default into a catch-all dumping ground.
14+
>
15+
> The fixes shipped so far (#2252, #1946) are deliberate **stopgaps**. This ADR replaces them with the proper model.
16+
17+
## TL;DR
18+
19+
1. **[new] No orphan authored metadata.** Every runtime-*created* metadata item is bound to a **writable package** (`package_id` is a real, writable package id — never `null`, never a code/installed package). The "package-less / `sys_metadata`-scope" state stops being an authoring destination; it remains only as a read-side rehydration detail and a migration source.
20+
2. **[existing->ruled] Two package kinds.** *Code/installed* packages (manifest `source: 'filesystem'` / installed via marketplace) are **read-only** — you may org-overlay an item where `allowOrgOverride` (ADR-0005), but you may **not** author *new* items into them. *Writable* packages (DB-backed, user/AI-created "bases") are the only authoring targets.
21+
3. **[new] Create-flow contract — "pick or create a base first".** Before any create, all three surfaces (Studio UI, the AI authoring tools, the kernel write path) resolve to a writable package or **prompt to create one** — no silent default. This is enforced in code, not just prompt guidance.
22+
4. **[new] The package is the lifecycle unit.** Delete (cascade all its items + data), export, snapshot/version, and duplicate operate on a whole package. This is the answer to "how do I delete the mess."
23+
5. **[ruled] Retire the stopgaps.** #2252's null-coercion evolves into *redirect-to-a-writable-package*; #1946's "Local / Custom" scope becomes a **migration affordance** ("move these loose items into a base") and is then removed.
24+
25+
---
26+
27+
## Context: what each surface does today
28+
29+
| Surface | Where new metadata's package comes from | Orphan possible? | Read-only code pkg? | Status |
30+
|---|---|---|---|---|
31+
| **Kernel** `protocol.saveMetaItem` (`@objectstack/objectql`) | `?package=` passed by caller; for a `runtime-only` write whose target is a *loaded code package*, the binding is **coerced to `null`** (`isLoadedPackage` guard, #2252) | **Yes** — by design today (the coercion *creates* nulls) | guarded (won't stamp into a loaded code pkg) | **[existing]** |
32+
| **Studio UI** (`../objectui`) | the package **selector** (`StudioHomePage`/`DirectoryPage`/`ResourceListPage` read `?package=`); a "Local / Custom" scope (`sys_metadata`) surfaces the null bucket (#1946); `PackagesPage` *can* create a writable package, but the create flow doesn't **require** one | **Yes** — default scope is a code pkg or the null bucket | partially (object becomes read-only — fixed for editability, not for *where it lives*) | **[existing]** |
33+
| **AI authoring** (`../cloud` `service-ai-studio`) | `resolvePackageId` (`metadata-tools.ts:268-328`): explicit `packageId` -> target item's pkg -> conversation active pkg -> single-app pkg -> **auto-create `com.workspace`**; read-only pkgs **rejected** (`:319`); `create_package` / `set_active_package` / `apply_blueprint` (`ensureAppPackage`) exist | **Soft** — auto-defaults into a catch-all instead of requiring a choice | **rejected** (`metadata-tools.ts:319`) | **[existing]** — most advanced of the three |
34+
35+
The primitives already exist on every surface (a package concept; a way to create one; a read-only/writable distinction in cloud). **What is missing is a single, enforced contract** so the three surfaces agree, and so authored metadata always has a managed home.
36+
37+
---
38+
39+
## Decision
40+
41+
### D1 — Authored metadata is package-bound; orphans are disallowed [new]
42+
43+
A runtime **create** (`intent = runtime-only`, i.e. not an overlay of an artifact item per ADR-0005) MUST resolve to a **writable** `package_id`. The kernel write path is the enforcement point of last resort:
44+
45+
- `protocol.saveMetaItem` gains a notion of *writable-package required for runtime creates*. When the resolved target is missing or is a **code/installed** package, the write is **rejected** with a structured, actionable error (`code: 'writable_package_required'`) rather than silently coerced to `null` (today's #2252 behaviour).
46+
- `null` / `sys_metadata`-scope ceases to be a *write* destination for new items. It survives only as (a) the read-side rehydration tag for legacy rows and (b) the source set for the D5 migration.
47+
- Overlays of artifact items (`intent = override-artifact`, `allowOrgOverride`) are **unchanged** — they keep their binding to the packaged item they customise.
48+
49+
### D2 — Two package kinds; only writable packages are authoring targets [existing->ruled]
50+
51+
- **Code / installed packages** — manifest `source: 'filesystem'`, or installed via the marketplace; scope `system` / `cloud` (already filtered out of the Studio selector). **Read-only.** New items may not be authored into them; only `allowOrgOverride` overlays are permitted (ADR-0005). This mirrors Airtable's installed apps/templates.
52+
- **Writable packages (bases)** — DB-backed, created by a user or the AI (`PackagesPage` create dialog; cloud `create_package`). The only valid authoring destination. Scope `project`.
53+
54+
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).
55+
56+
### D3 — Create-flow contract: select-or-create a base first, enforced everywhere [new]
57+
58+
Every authoring surface follows the same contract; none silently defaults into a catch-all:
59+
60+
- **Studio UI** [new]: the scope selector defaults to a *writable* package (the user's most-recent base) or, when none exists, the create entry points (`新建`) **prompt to create a base** (reuse the existing `PackagesPage` create dialog) before opening the designer. New items are bound to the active writable package. The "Local / Custom" (`sys_metadata`) scope is removed as a create destination (see D5).
61+
- **AI authoring** [new] (`../cloud` `service-ai-studio`): `resolvePackageId` **drops the auto-`com.workspace` fallback** (`metadata-tools.ts:304`) and the `applyDraft` re-bind (`:568-573`); when no writable package is resolvable it returns an actionable error directing the agent to `create_package` or `set_active_package`. `apply_blueprint` requires a package context (its `ensureAppPackage` is invoked only after an explicit create/confirm). Skill prompts (`metadata-authoring-skill.ts`, `solution-design-skill.ts`) are updated to make "establish the base first" a hard step, not a hint.
62+
- **Kernel** [new]: D1 is the backstop — even if a surface regresses, the write is rejected, not orphaned.
63+
64+
### D4 — The package is the lifecycle unit (delete / export / snapshot / duplicate) [new]
65+
66+
Operating on a *base* operates on all of its contents — this is the direct answer to "a pile of loose metadata, how do I delete it?":
67+
68+
- **Delete**: deleting a writable package **cascades** to all its metadata items (and, per the env's data-lifecycle contract, optionally its seeded/created data). Builds on `registry.uninstallPackage` (`@objectstack/objectql`), extended to a *writable-base delete* that removes the DB-backed items it owns.
69+
- **Export / snapshot / version**: a base can be exported (the "app = package" north star) and snapshotted (ADR-0067 commit history is per-package).
70+
- **Duplicate**: clone a base into a new writable package (the Airtable "duplicate base" gesture).
71+
72+
### D5 — Retire the stopgaps via a migration affordance [ruled]
73+
74+
- The framework #2252 *coerce-to-null* behaviour is replaced by D1 *reject/redirect-to-writable-package*. Existing `package_id = null` rows are not broken.
75+
- The objectui #1946 *"Local / Custom"* scope is repurposed as a **one-time migration surface**: it lists the legacy package-less items and offers "move into a base" (assign them a writable `package_id`). Once an environment has no orphans, the scope is removed from the selector.
76+
- A small migration (or admin action) can bulk-assign legacy orphans to a default base named for the environment.
77+
78+
### D6 — Single-tenant framing; no "org" dumping ground [ruled]
79+
80+
A metadata-customizable deployment is **single-tenant** (an *environment*; `project -> environment`); there is no per-org overlay dimension here. The relevant axis is **code package vs writable base**, not "org". The earlier instinct to make a package-less "org/local" scope the *default* is explicitly rejected: it reproduces the orphan pile. (Multi-tenant hosting keeps `package_id = null` overlays as a *tenant-overlay* mechanism; that is orthogonal and out of scope for this ADR.)
81+
82+
---
83+
84+
## Consequences
85+
86+
- **Positive**: every authored item has a managed home; bulk delete/export/duplicate become trivial (whole base); the three surfaces (kernel / Studio / AI) share one contract; AI output is never orphaned; the "read-only after publish" and "where did my object go" classes of bug are designed out, not patched.
87+
- **Cost / migration**: a create now has a (cheap, one-time per environment) "create your first base" step; existing null-scope rows need the D5 migration; #2252/#1946 code is removed after migration. The cloud `resolvePackageId` fallback removal must be paired with good "no package selected" UX so the AI does not dead-end.
88+
- **Risk**: over-strict enforcement could block legitimate flows (e.g. seed/import). Mitigation: the writable-package requirement applies to *interactive authoring*; bulk import paths may pre-create a base. Validate against the dogfood gate.
89+
90+
## Open questions
91+
92+
1. **Default base auto-provisioning**: do we auto-create a single "My workspace" base on first authoring (one-click, then required thereafter), or always force an explicit create? (Cloud currently auto-creates `com.workspace`; D3 removes the *silent* version but a *prompted* one-click is acceptable.)
93+
2. **`allowOrgOverride` types in single-tenant**: in a single-tenant env, is org-overlay of a code item still meaningful, or should customising a code item also fork it into a writable base? (Leaning: keep overlay for surgical tweaks; "fork to base" for substantial changes.)
94+
3. **Delete-cascade & data**: does deleting a base also delete the *records* of its objects, or only the schema? Ties to the data-lifecycle ADR (retention).
95+
4. **Naming/id ergonomics**: bases need friendly names but stable ids (`app.<slug>` today). Surface a name; keep the id stable.
96+
97+
## Rollout (tracked separately — see the package-first epic)
98+
99+
1. Kernel D1/D2 predicate + `writable_package_required` rejection (`@objectstack/objectql`).
100+
2. Studio D3 create-flow + selector default; PackagesPage delete/duplicate (`../objectui`).
101+
3. AI D3 enforcement in `service-ai-studio` (drop fallback, require package, update skills).
102+
4. D4 package-as-unit (delete-cascade, export/duplicate).
103+
5. D5 migration affordance; remove #2252/#1946 stopgaps.
104+
6. Dogfood-gate coverage for each.

0 commit comments

Comments
 (0)