diff --git a/.changeset/28509-custom-override-read-teaching.md b/.changeset/28509-custom-override-read-teaching.md new file mode 100644 index 00000000000..e267b903c87 --- /dev/null +++ b/.changeset/28509-custom-override-read-teaching.md @@ -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 `.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. diff --git a/content/docs/concepts/metadata-lifecycle.mdx b/content/docs/concepts/metadata-lifecycle.mdx index ffbd75d5aa2..843ebcd21b3 100644 --- a/content/docs/concepts/metadata-lifecycle.mdx +++ b/content/docs/concepts/metadata-lifecycle.mdx @@ -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`). diff --git a/content/docs/ui/forms.mdx b/content/docs/ui/forms.mdx index f477cc1707b..1c1f0b57814 100644 --- a/content/docs/ui/forms.mdx +++ b/content/docs/ui/forms.mdx @@ -20,7 +20,7 @@ Both modes: - Honor `?prefill_=` 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 diff --git a/scripts/check-skills-token-ratchet.mjs b/scripts/check-skills-token-ratchet.mjs index b52b8b38466..b31e701fe4e 100644 --- a/scripts/check-skills-token-ratchet.mjs +++ b/scripts/check-skills-token-ratchet.mjs @@ -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: @@ -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], diff --git a/skills/objectstack-platform/SKILL.md b/skills/objectstack-platform/SKILL.md index c9f6588cae7..bc3d05b51b6 100644 --- a/skills/objectstack-platform/SKILL.md +++ b/skills/objectstack-platform/SKILL.md @@ -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 @@ -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 `__` 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 @@ -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 — @@ -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). diff --git a/skills/objectstack-platform/evals/config-plugins-ops.json b/skills/objectstack-platform/evals/config-plugins-ops.json index 8664aa018f2..1193b0bf2d5 100644 --- a/skills/objectstack-platform/evals/config-plugins-ops.json +++ b/skills/objectstack-platform/evals/config-plugins-ops.json @@ -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"] + } } ] } diff --git a/skills/objectstack-platform/references/operations.md b/skills/objectstack-platform/references/operations.md index c3bf776b4a2..0bfca5358c7 100644 --- a/skills/objectstack-platform/references/operations.md +++ b/skills/objectstack-platform/references/operations.md @@ -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, @@ -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