Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .changeset/28509-custom-override-read-teaching.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'@objectstack/skills': patch
---

docs(skills): objectstack-platform teaches the one effective read of a `.custom` declared override (ADR-0146 D8)

Clause-②: no
Metadata-protocol: none

- **Before.** The `overrides: { package, basedOn }` declaration takes effect on every read of a name an installed package ships (ADR-0146 D8), but no published skill mentioned `.custom`, `overrides` or `basedOn`. An agent customizing an installed package (HotCRM) was still taught the Setup overlay row as the only path, which the read now treats as a package-unbound row awaiting migration, or wrote an undeclared same-name item that the layered read reports as an orphan.
- **Now.** `objectstack-platform`'s Manifest Reference says where a customization lives (the paired `<base>.custom` package), what an author writes (the same-named item with `overrides: { package, basedOn }`, the base's `manifest.id` / `manifest.version`), where the declaration is honoured (only from the pair; today only for a view, dashboard, report, email_template or translation, the other overridable types waiting for their doors; `basedOn` never judged; any other package's declaration ignored) and what the read serves (every read agrees; one item per name in the environment-wide list; orphans in the layered read's `overlay`; a withdrawn public form never re-opened; the pair's export carries the declaration). One eval asserts the declared shape and refuses the Setup-write alternative.
- The file stays under its token ceiling: the new text is paid for by deleting restatement in the same file (the CLI cheat sheet `references/operations.md` already holds, the seed `object`-is-derived sentence `objectstack-data`'s `seeds.md` holds, a table restating its own paragraph, two doubled rules). The `os migrate` row of `references/operations.md` takes back the `apply` rule the cheat sheet carried (loosens only; `--allow-destructive` for drops/tightenings). The SKILL.md ceiling lowers to its new measurement; no ceiling rises, no frontmatter, schema or behaviour change. The docs site gains the same facts on the metadata-lifecycle page and one sentence on the forms page.
32 changes: 32 additions & 0 deletions content/docs/concepts/metadata-lifecycle.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,38 @@ later ships. See [ADR-0070](https://github.com/objectstack-ai/objectstack/blob/m
> container of metadata), distinct from the npm `@objectstack/*` packages listed in
> the repo README.

### Customizing an installed package: the paired `.custom` override (ADR-0146)

A customer's change to an installed base package — HotCRM, id `hotcrm` — is an item of
the base's paired writable package **`hotcrm.custom`**, which depends on the base; the
base's own items stay read-only. An item of the pair that bears a base item's name and
declares in its body `overrides: { package: 'hotcrm', basedOn: '3.2.0' }` (the base's
`manifest.id` and `manifest.version`; `basedOn` is recorded and never judged; schema:
[Override Declaration](/docs/references/kernel/override-declaration)) replaces the base's
document **whole** for every read that serves the base's name
([ADR-0146 D8](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0146-studio-designs-setup-administers-one-editor-per-item-paired-customize-package.md)):

- **The reads agree.** `GET /meta/:type/:name` with no `?package`, the same read with
`?package=hotcrm`, the layered read (`code` = the base's document, `overlay` = the
override, `effective` = the override), the list scoped to the base and the
environment-wide list all serve the override, stamped `_packageId: 'hotcrm'`. The
declaration counts only from the pair — the stored row's own `package_id` must be
`hotcrm.custom` — and today only for a `view`, `dashboard`, `report`, `email_template`
or `translation`: the rest of `OVERRIDABLE_METADATA_TYPES` (`@objectstack/spec/kernel`)
— `page`, `app`, `dataset`, `doc`, `book`, `mapping`, `flow` and `action` — waits for
its doors and is not served yet. A declaration from any other package is ignored.
- **The environment-wide list** serves one item per name that exactly one code package
ships. A dependent package's same-named item is not listed beside it and stays under
its own `?package=` address. A package-unbound row — the overlay row of the table
below — still wins over the base's document until ADR-0146 D11 migrates it into the
pair.
- **Orphans are reported, not dropped.** A row of the base's name stored in the pair
without the declaration, or in a package that depends on the base, is reported by the
layered read in `overlay` with `effective` = the base's document.
- **Export carries the declaration.** `GET /packages/hotcrm.custom/export` carries the
overriding item with its `overrides` block; `GET /packages/hotcrm/export` carries the
base unchanged.

## Overlay whitelist (shared-DB tenancy invariant)

**Most metadata types must not be overlaid by an environment** — overlaying a packaged item would break the physical schema or the behaviour its package ships. The whitelist lives in **one** place: `MetadataTypeRegistryEntry.allowEnvironmentOverlay` in `packages/spec/src/kernel/metadata-plugin.zod.ts` (spelled `allowOrgOverride` before protocol 18; `GET /api/v1/meta/types` serves it per entry as `allowEnvironmentOverlay`).
Expand Down
2 changes: 1 addition & 1 deletion content/docs/ui/forms.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Both modes:
- Honor `?prefill_<field>=<value>` URL params
- Honor `submitBehavior` (thank-you / redirect / continue / next-record) — with **mode-aware defaults** when it is omitted (see [§8](#8-submitbehavior--what-happens-after-submit))

A **public form** is the Salesforce *Web-to-Lead* style embeddable form — declare a `FormView` with `sharing.enabled: true` and `sharing.allowAnonymous: true`, give it a `publicLink`, and the framework wires the anonymous REST endpoints automatically. Clearing either switch withdraws the form from every anonymous endpoint.
A **public form** is the Salesforce *Web-to-Lead* style embeddable form — declare a `FormView` with `sharing.enabled: true` and `sharing.allowAnonymous: true`, give it a `publicLink`, and the framework wires the anonymous REST endpoints automatically. Clearing either switch withdraws the form from every anonymous endpoint. An override of the form view in the base package's paired `.custom` package ([ADR-0146](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0146-studio-designs-setup-administers-one-editor-per-item-paired-customize-package.md)) is the form these endpoints serve, so it can narrow or withdraw anonymous intake — but it never re-opens a form that the base's own document or any other body the endpoints honoured withdrew: the anonymous doors only narrow.

## Architecture at a glance

Expand Down
4 changes: 2 additions & 2 deletions scripts/check-skills-token-ratchet.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -581,7 +581,7 @@ export const CEILINGS = new Map([
// absorbed and the ceiling moves 5997 -> 6014 (+17), the measured
// remainder only. Pinned AT the measurement.
// ceil(($(wc -c < skills/objectstack-platform/SKILL.md) - 20) / 4) == ceil(24055 / 4)
['skills/objectstack-platform/SKILL.md', 6014],
['skills/objectstack-platform/SKILL.md', 6012],
// `skills/objectstack-pm-dispatch/SKILL.md` (9708) and its
// `rules/dev-template.md` (1838) left this table on 2026-09-10 with the files:
// the maintainer deleted the published PM skill whole (verbatim:
Expand Down Expand Up @@ -993,7 +993,7 @@ export const CEILINGS = new Map([
// 花板,怎么付?」, tap A, verbatim and untranslated: 「A:先削重复,余额按实测提高」.
['skills/objectstack-data/references/seeds.md', 1566],
['skills/objectstack-platform/references/bootstrap.md', 1351],
['skills/objectstack-platform/references/operations.md', 3125],
['skills/objectstack-platform/references/operations.md', 3119],
['skills/objectstack-platform/references/plugin-development.md', 3137],
['skills/objectstack-upgrade/references/examples-upgrade.md', 1197],

Expand Down
34 changes: 4 additions & 30 deletions skills/objectstack-platform/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,18 +104,6 @@ objectstack compile
OS_ARTIFACT_PATH=./dist/objectstack.json objectstack dev
```

Runtime rule of thumb:

| Context | Metadata source | Database role |
|:--------|:----------------|:--------------|
| Local dev | TS files or `dist/objectstack.json` | Business rows only |
| Production runtime | Artifact API response | Business rows only |
| Control plane | Published JSON in metadata storage | Environment revisions, history, overlays |

When generating `objectstack.config.ts`, keep object names short and
`snake_case`; never set `tableName`, and do not add `sys_metadata` objects to an
environment runtime manifest.

---

## Manifest Reference
Expand All @@ -139,10 +127,12 @@ checks it **before it loads anything** and refuses an out-of-range app at the
boundary with the exact migration command. Change it when you deliberately move
to a new protocol major — never to silence a mismatch.

**Object naming:** The object `name` is the canonical identifier and equals the physical table name. Embed any domain prefix directly in the name (e.g. `name: 'crm_account'`); the object-level `namespace` *field* is retired (ADR-0129 D3) and refused at load.
**Object naming:** The object `name` is the canonical identifier and equals the physical table name. Embed any domain prefix directly in the name (e.g. `name: 'crm_account'`); the object-level `namespace` *field* is retired (ADR-0129 D3) and refused at load. Never set `tableName`, and never add a `sys_metadata` object to a runtime manifest.

**`manifest.namespace` (ADR-0048):** Optional, but **enforced once set**. When a package declares `manifest.namespace: 'crm'`, every `object.name` must start with `crm_` or `defineStack` errors (`validateNamespacePrefix` in `@objectstack/spec`); the legacy `<ns>__<short>` double-underscore form is rejected, and `sys_`-prefixed names are platform-reserved and exempt. The namespace is also a package-ownership key — installing two packages that both claim `crm` fails with `NamespaceConflictError` (downgrade to a warning with `OS_METADATA_COLLISION=warn`). `os lint` additionally emits a non-fatal `naming/namespace-prefix` warning for bare-named UI/automation items (app, page, dashboard, flow, action, report, dataset) when a namespace is set.

**Customizing an installed package (ADR-0146):** Customize an installed base `hotcrm` in its paired writable package `hotcrm.custom`, which depends on it: a same-named item there with `overrides: { package: 'hotcrm', basedOn: '3.2.0' }` (its `manifest.id`/`manifest.version`) replaces the base's document whole. The read honours it only from the pair (the row's `package_id`), today only for a view, dashboard, report, email_template or translation (page, app, dataset, doc, book, mapping, flow and action wait for their doors), never judges `basedOn` and ignores any other package's declaration. Then no `?package`, `?package=hotcrm`, the layered read and both lists serve it; the env-wide list holds one item per name one code package ships (a dependent's stays under its own `?package=`); a package-unbound Setup row still wins over the base's until D11. An undeclared pair row or a dependent's row is an orphan in the layered read's `overlay`, `effective` = the base's. An override may narrow a public form's anonymous intake, never re-open one any honoured body withdrew. `GET /packages/hotcrm.custom/export` carries it with `overrides`; the base's is unchanged.

---

## The App / Platform Boundary
Expand Down Expand Up @@ -411,9 +401,7 @@ export const onEnable = async (ctx: { ql: { registerAction: (...a: unknown[]) =>

The stack's `data:` collection is authored with `defineSeed()`, which
**objectstack-data** owns — go there for `externalId` matching, `env:` scoping,
and which keys are derived. In particular `object` is derived from the object
definition: never write it by hand, and never hand-write a raw
`data: [{ object: … }]` literal.
and which keys are derived.

`mode` (`upsert` default · `insert` · `update` · `ignore` · `replace` ⚠️ **data
loss**) decides what a replay does to rows that already exist —
Expand Down Expand Up @@ -450,29 +438,15 @@ needs an account to exist.
## CLI Commands

Daily commands are in [Part 3 — Operations](./references/operations.md#part-3--operations-cli-testing-deployment).
Cheat sheet for the bootstrap loop:

```bash
npx create-objectstack my-app
cd my-app && npm install
os dev --ui # dev server + Console at /_console/
os validate # metadata cross-reference checks
os compile # produce dist/ artifact
os migrate plan # preview metadata↔DB schema drift (additive sync never alters existing columns)
os migrate apply # reconcile DB to metadata (loosening only; --allow-destructive for drops/tightenings)
PORT=8080 os start # production — pin the port explicitly
```

---

→ Moved verbatim to [references/bootstrap.md](./references/bootstrap.md) § Complete Working Example.

---
---

→ Part 2 — Plugin Development & Kernel Extension moved verbatim to [references/plugin-development.md](./references/plugin-development.md) (Quick Reference — Detailed Rules · ObjectKernel vs LiteKernel · Plugin Interface — Quick Overview · PluginContext API · Complete Plugin Example · Using Plugins · Testing Plugins · Well-Known Plugin Names & Services · MetadataPlugin Runtime Boundary · Health Monitoring · Feature Flags); its detailed rules stay at [rules/plugin-lifecycle.md](./rules/plugin-lifecycle.md) · [rules/service-registry.md](./rules/service-registry.md) · [references/plugin-hooks.md](./references/plugin-hooks.md).

---
---

→ Part 3 — Operations: CLI, Testing, Deployment moved verbatim to [references/operations.md](./references/operations.md) (Daily-loop commands · Build & runtime · Verify your work · Ports & networking · Data & migrations · Environments & deploy · Shipping a plugin · Testing pattern · Deployment targets · Health & observability · Common ops pitfalls).
Expand Down
10 changes: 10 additions & 0 deletions skills/objectstack-platform/evals/config-plugins-ops.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,16 @@
"must_contain": ["queues:", "assignmentRules:", "defineQueue", "'sharing'"],
"must_not_contain": ["assigned_to", "beforeInsert"]
}
},
{
"id": 10,
"prompt": "We installed HotCRM (`hotcrm`, version 3.2.0). Replace its `crm_account_grid` list view with our own columns so every read of that view serves ours, and keep the change exportable to staging.",
"expected_output": "The whole view, under the same name `crm_account_grid`, authored in HotCRM's paired writable package `hotcrm.custom` (which depends on `hotcrm`) with `overrides: { package: 'hotcrm', basedOn: '3.2.0' }` in its body — the base's `manifest.id` and `manifest.version`, `basedOn` recorded and never judged. Every read of the name (no `?package`, `?package=hotcrm`, the layered read, both lists) then serves it under `_packageId: 'hotcrm'`, and `GET /packages/hotcrm.custom/export` carries it with the declaration. Not an in-place edit of HotCRM's own read-only item, not a package-unbound Setup row (that path wins only until the D11 migration), not an object extension (that adds fields and replaces no view), and no declaration from any other package (ignored).",
"files": [],
"assertions": {
"must_contain": ["hotcrm.custom", "overrides:", "package: 'hotcrm'", "basedOn"],
"must_not_contain": ["PUT /api/v1/meta/view/crm_account_grid", "PUT /meta/view/crm_account_grid"]
}
}
]
}
9 changes: 4 additions & 5 deletions skills/objectstack-platform/references/operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,9 @@ os build # the same three gates, plus emits dist/objectstack.json
3. **Widget-binding integrity (ADR-0021)** — every dashboard widget's
`dataset` / `dimensions` / `values` resolves to a declared dataset/field.

Both exit non-zero with a located, corrective message; `os build` additionally
emits the artifact. Use `os validate` as the fast inner-loop check after editing
metadata and `os build` when you need `dist/`. In a scaffolded project these are
`npm run validate` / `npm run build`.
Both exit non-zero with a located, corrective message. Use `os validate` as the
fast inner-loop check after editing metadata and `os build` when you need
`dist/`. In a scaffolded project these are `npm run validate` / `npm run build`.

**Rule of thumb: never report a metadata change as done until `os validate`
passes.** (`os lint` is a *separate* style/convention pass — naming, labels,
Expand Down Expand Up @@ -93,7 +92,7 @@ Port resolution is the same for `os dev` and `os start` (both spawn `os serve`):
| `os data create` / `get` / `query` / `update` / `delete` | Record-level CRUD against a running server (there is no `os data seed` / `export` / `import` — seed data in the `data:` collection loads automatically at boot) |
| `os diff` | Compare two ObjectStack config files and detect breaking changes |
| `os meta register` | Register (create/update) metadata on a target server (there is no `os meta apply`) |
| `os migrate plan` / `os migrate apply` | Dry-run / apply physical-DB drift reconciliation from metadata (forward-only — no batch rollback; `os rollback` was removed) |
| `os migrate plan` / `os migrate apply` | Preview / reconcile DB drift from metadata; `apply` loosens only, `--allow-destructive` for drops/tightenings (forward-only; `os rollback` was removed) |

## Environments & deploy

Expand Down
Loading