Skip to content

Commit 493a6c4

Browse files
feat(spec,metadata-protocol): a seed dataset column can be written only when its row is created (columns.FIELD.applyOn: 'create') (#25813)
<!-- claude-projects-attribution: {"github_login":"marchtian"} --> _Requested by **田騏** · [project thread](https://claude.ai/code/project/chan_01VmuUhXU9SSUzqJYyXSmYSi?thread=cmsg_01VmuUhXU9SSUzqJYyXSmYSiVXmt4GnURpCsmf91ag9xzG)_ Fixes #25761 Clause-②: yes Metadata-protocol: added Ruling-ref: hotclm#41 comment 6395830052 (letter A); triage 6396236242 ## Ruling Verbatim, letter A: "job-written columns are written on first seed only — via the platform". Triage 6396236242: "Same name and semantics as the import door's `applyOn` (#25577 / PR #25664): spec key first, then the seed loader's upsert path. Read beside #8740: the driver's empty-merge-set fallback must not re-admit a `'create'` column." ## What changed **Spec (`packages/spec/src/data`).** - `ColumnApplyOnSchema = z.enum(['create', 'always'])` is the one enum, in `data/column-apply-on.zod.ts`. The import door's `FieldMappingEntry.applyOn` (`api/export.zod.ts`) now reads it instead of spelling its own, and `SeedColumn.applyOn` reads the same one. Each keeps its own `.default('always')`. The import door's emitted JSON schema is byte-unchanged. - `SeedColumnSchema` is a closed per-column shape (`{ applyOn }`, default `always`). `SeedSchema` gains an optional `columns` map from a field name to its `SeedColumn`. A map makes two policies for one field unrepresentable. - `SeedSchema` refuses an unknown key on a column (`unrecognized_keys`). A dataset-level `applyOn` is refused with a pointer to `columns` (`guidance`). - `defineSeed` types the `columns` keys against the object's fields plus its injected system columns (the `SeedColumns` type). It also refuses an undeclared key when it runs, in the same refusal as an undeclared record key, with a near miss. **Seed loader (`packages/metadata-protocol/src/seed-loader.ts`).** - `createOnlySeedColumns` is the set a dataset declares `applyOn: 'create'`, minus the fields of its `externalId`. A key field is what a matched row was found by, so it stays in that row's update, as an import's match key does. - On a row the load MATCHED (`existingRecords` has its natural key, the same lookup `decideWriteAction` makes), those columns are deleted from the record before the no-op check and before the write. The batched path and the sequential self-referencing path both read that same record. - A create-only reference on a matched row is skipped in the resolution loop: not resolved, not dropped for failing to resolve, and not deferred. Pass 2's back-fill would otherwise write it after all. Create-only references resolve last, so a reference column of a composite `externalId` has resolved by the time the loader asks whether the row matched. - `warnOnUndeclaredSeedColumns` logs at `error`, once per dataset, when a loaded seed's `columns` names a column the object does not have. The level follows AGENTS.md "Degradation log levels": the load looks normal while a job's write to the intended column is overwritten on every replay. The rows still load. The finding is log-only, because `result.errors` holds per-record `ReferenceResolutionError`s (`recordIndex` is `.int().min(0)`) and has no shape for a dataset-level finding. - A row the seed creates, a dry run, and the `insert` / `ignore` / `replace` modes are unchanged. **Docs, ledger, generated.** - A new section in `content/docs/data-modeling/seed-data.mdx`, and the `defineSeed` API reference. - A `seed/columns` liveness row (live, with `children.applyOn`) and regenerated state counts. - `ColumnApplyOn` names the author state (ADR-0122) and is pinned isomorphic in `type-alias-convention.pin.test.ts` (790 → 791). - The hand-kept schema counts move with the new module: `packages/spec/llms.txt` 206 → 207 (data 30 → 31), and `content/docs/getting-started/quick-reference.mdx` data 29 → 30. - Regenerated: the reference docs; the api-surface, export-origins, declaration-map, authorable-surface and json-schema manifest shards; and the two skills reference indexes. ## PM assumptions, measured 1. "Runtime: `packages/runtime/src/seed-loader.ts`". Refuted: that file is a re-export shim. The loader body is `packages/metadata-protocol/src/seed-loader.ts`, so the change lives there (file-surface deviation below). 2. "The import door's enum lives at `packages/spec/src/api/export.zod.ts` (~:173)". Held, but it was spelled inline. It is extracted to `data/column-apply-on.zod.ts` because `data/` never imports from `api/`, and both doors read it. A test pins one-enum parity in `seed.test.ts`: the options and default of `FieldMappingEntrySchema`'s `applyOn` equal `SeedColumnSchema`'s. 3. "The seed loader has no per-column policy today". Held: at `5f5f0b42` there was no per-column key on `SeedSchema`, and nothing in the loader read one. ## Read beside #8740 The driver's merge-ALL fallback for an empty merge set lives on `driver.upsert`. The seed path never calls `driver.upsert`: there is no `upsert(` call in `seed-loader.ts`, and a matched row is written with `engine.update(object, record, { where: { id } })`. With every non-key column create-only, the patch is left holding only the key columns, which equal the stored row. `isNoOpReplay` therefore skips the row, and no write is issued. A test pins it ("every non-key column create-only → no update call"). ## Tests - `packages/metadata-protocol/src/seed-loader-replay.test.ts` has a new describe, "a column declared applyOn: create is written on first seed only (#25761)". It runs on the faithful engine double, over both the batched path and the sequential self-referencing path, and covers: - a fresh insert writes both kinds of column; - a re-seed after a "job" changed a create-only column keeps it while an always column re-asserts, for `upsert` and `update`, with the update's keys pinned exactly; - a control dataset without the policy re-asserts; - a no-op replay; - every non-key column create-only → no update; - a create-only reference on a matched row is neither resolved, deferred nor dropped; - a control: an unresolvable reference on a new row is still deferred; - a composite `externalId` with a create-only reference declared before a key reference; - an `externalId` column declared create-only stays in the update (both rows updated); - a dry run; - the `error` report on a misspelt column, and no report on an injected system column (`owner_id`). - `packages/spec/src/data/seed.test.ts` "column policies": - accepts both values and defaults to `always`; - refuses `'insert'` with `code: invalid_value` at `['columns','status','applyOn']`; - refuses an unknown column key with `code: unrecognized_keys` at `['columns','status']`; - points a dataset-level `applyOn` at `columns`; - pins one-enum parity. - `packages/spec/src/data/define-seed-record-keys.test.ts`: four `columns` key cases, including the compile-time refusal (`@ts-expect-error`). At the final head `0a22461b`, under the shared verify lock (`VERDICT command-exit 0`): - `pnpm --filter @objectstack/spec typecheck` and `pnpm --filter @objectstack/metadata-protocol typecheck` are green. - `pnpm --filter @objectstack/spec test`: Test Files 721 passed (721), Tests 21844 passed, 1 todo. - `pnpm --filter @objectstack/metadata-protocol test`: Test Files 263 passed, 3 skipped (266), Tests 21511 passed, 19 skipped. Ablations: each was red, and each was restored byte-identical to `HEAD` (blob hash compared, `git diff HEAD` empty): | mutation | result | |---|---| | strip of create-only columns on a matched row removed | 9 failed / 45 | | in-loop skip of a create-only reference removed | 2 failed / 45 | | create-only references resolved in declared order | 1 failed / 46 | | `warnOnUndeclaredSeedColumns` call removed | 1 failed / 46 | | `defineSeed` columns-key check removed | 3 failed / 13 | | undeclared-column report at `warn` instead of `error` | 1 failed / 46 | ## Review An independent full review of the whole diff by an isolated `fable` reviewer ran in four rounds. Every round returned no must-fix. Round 1's should-fix notes were applied in `986fb436`. Round 2's dataset-level `result.errors` note is answered in `d2319759`, and the reviewer accepted that answer. The verdicts and each note's disposition are posted as a comment on this PR. ## Gates `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at `0a22461b` derives 120 commands. All 120 ran on that head, each exit code captured before any pipe, and all 120 exited 0. The `--ran` reconciliation over those codes reads: "120 derived famil(ies) accounted for — 120 run, 0 NOT-MEASURED (a DERIVED zero — all 120 recorded an exit code and none of them is 3)". The local-only `check:dev-prereqs` and `check:skill-frame-freshness`, and `check:nul-bytes`, also exit 0. What the first pass (before the merge and the last fixes) found: - Red, and fixed here: `check:llms-txt` and `check:quick-reference-counts` (the hand-kept counts), and `check:spec-parsed-alias` (the ADR-0122 alias). - Red only on main's `translation/flows.screens` row: `check:liveness` and `check:generated`. They are green after the merge. - Exit 3 until the environment was fixed: `check:plugin-teardown-shape --self-test`, on the shallow clone until its pinned fixture commit was fetched; `check:skill-examples`, `check:dual-build-cjs-loads` and `check:lean-entry-closure`, until the packages were built. The PR head `aea8b0d3` adds one commit after that run, the changeset title, text only (`FIELD` for an angle-bracket placeholder). The derived list is unchanged on it, and the 7 changeset families rerun there exit 0. The dogfood battery is not owed: there is no `!` in the title, and `Clause-②: yes` has no narrowing arm. The families CI runs are reported by this PR's check runs. `main` moved three commits (#25748, #25789, #25804) after the merge. `git merge-tree` against it is clean, none of their files is in this diff, and none of them lands a sibling card. ## Deviations - File surface: the claim names `packages/runtime/src/seed-loader.ts`, but the loader body is in `packages/metadata-protocol/src/seed-loader.ts` (assumption 1). The extracted enum is a new file in `packages/spec/src/data`, and `packages/spec/src/api/export.zod.ts` changes one import. Hand-kept counts in `packages/spec/llms.txt` and `content/docs/getting-started/quick-reference.mdx`, plus the alias pin file, moved with the new module. - `skills/**` is touched only by generator-owned rows (`gen:skill-refs`). The readings: - `skills/objectstack-data/references/_index.md`: 72 → 73 lines; - `skills/objectstack-platform/references/_index.md`: 41 → 41 lines; - all `SKILL.md` together: 4383 → 4383 lines; - the whole `skills/` tree: 13350 → 13351 lines. The order carried no skills net-add budget. - Line budget: 36 files, +877 / −62. Hand-written: 10 files, +352 / −25. Tests: 4 files, +411 / −4. Generated: 22 files, +114 / −33. ## Acceptance notes - `packages/rest/src/import-prepare.ts:620` still spells `'always'` / `'create'` by hand where it validates a mapping entry's `applyOn`, instead of reading `ColumnApplyOnSchema`. The schema itself has one spelling now, so the remaining copy is unexercised drift in another package. Carrier: none. - A `columns` key that names no column is refused by `defineSeed`, and only logged at `error` for a seed that arrives stored (the publish door accepts it, since `SeedSchema` cannot know the object's fields). Carrying a dataset-level finding in the load result, or refusing it at the stored-seed publish door, would change the seed loader's result contract or the publish path. Carrier: none. - `check:liveness` was red on `main` `5f5f0b42` (`translation/flows.screens` → `engine.ts`), unrelated to this diff. `main`'s #25796 fixed it, and it is green here after the merge. ## 维护者速读(草稿) **改了什么**:种子数据(seed)现在可以给单个字段声明写入策略,写法是 `columns: { status: { applyOn: 'create' } }`。声明为 `create` 的字段只在种子第一次创建这条记录时写入。之后每次重放(开发服务器启动、包重新发布),只要匹配到已有记录,这个字段就不再被覆盖,其余字段照旧刷新。取值和含义与导入映射的 `applyOn` 完全一致,两边共用同一个枚举。 **为什么改**:应用自己的作业会在首次播种后改写某些字段,比如付款作业推进的状态、续约作业设置的标记。以前每次重放种子都会把这些字段改回初始值,悄悄撤销作业的结果,日志里也没有痕迹。这是维护者裁决 A:"作业写入的字段只在首次播种时写入,由平台负责"。 **风险与代价(含回滚)**:新增的是可选键,默认值 `always` 就是改动前的行为,不声明就没有任何变化。影响只限于 `upsert` 和 `update` 两种模式下已经匹配到的行。如果 `columns` 写错了字段名,`defineSeed` 会直接拒绝;存储的种子则在加载时记一条 error 日志,数据照常加载。回滚只需撤销本 PR,已经写入的数据不受影响。`skills/**` 的变化只有两份生成的索引,一共多一行。 **席位意见**: **你要做的**:审阅并批准。本 PR 触及受管路径 `skills/**`,需要维护者批准后才能合并。 --- _Generated by [Claude Code](https://claude.ai/code/session_018HE4VY52U3L5Bfzz51dsFv)_ 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_018HE4VY52U3L5Bfzz51dsFv --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 17c5e68 commit 493a6c4

35 files changed

Lines changed: 875 additions & 60 deletions

File tree

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/metadata-protocol': minor
4+
'@objectstack/skills': minor
5+
---
6+
7+
feat(spec,metadata-protocol): a seed dataset column can be written only when its row is created (`columns.FIELD.applyOn: 'create'`)
8+
9+
Clause-②: yes
10+
Metadata-protocol: added
11+
12+
A seed is replayed on every dev-server boot and every package re-publish, and an `upsert` replay re-asserts every column a record declares on the row it matched. That reverts the columns an app's own jobs write after the first seed (a status a payment job advances, a flag a renewal job sets), so a re-run undid the jobs' work with nothing in the log.
13+
14+
A seed dataset now takes a per-column write policy, keyed by field name:
15+
16+
```ts
17+
defineSeed(Instalment, {
18+
externalId: 'code',
19+
columns: {
20+
status: { applyOn: 'create' },
21+
is_expiring: { applyOn: 'create' },
22+
},
23+
records: [{ code: 'INS-001', amount: 1200, status: 'pending', is_expiring: false }],
24+
});
25+
```
26+
27+
- **`applyOn: 'create'`** writes the column when the seed creates the row and leaves it out of every write to a row the seed matched by its `externalId`: an `upsert` replay over an existing row, and every row of an `update` dataset. The matched row keeps the value it holds, while the `always` columns are refreshed. A row whose only differences are create-only columns is not written at all. A create-only reference column on a matched row is not resolved and not deferred to pass 2, so nothing later in the load writes it back. A column of the `externalId` is the key the row was matched by and stays in its update. `insert`, `ignore` and `replace` never update a matched row, so nothing changes on those modes.
28+
- **`applyOn: 'always'`** (the default, and every column without a `columns` entry) is the behaviour before this change.
29+
- The values, the default and the meaning are those of the import door's `FieldMappingEntry.applyOn`. Both doors now read one enum, exported as `ColumnApplyOnSchema` from `@objectstack/spec/data`. `SeedColumnSchema` (with `SeedColumn` / `SeedColumnParsed`) is the per-column shape.
30+
- A `columns` key must name a column the object has: a declared field, or a system column the platform injects on it such as `owner_id`. `defineSeed` refuses one that does not when it runs (and at compile time for an object literal), naming the key with a near miss. The seed loader logs an `error` naming the column, once per dataset, whenever it loads a seed that names one, since a stored seed never passes through `defineSeed`. The rows still load.
31+
- `SeedSchema` refuses an unknown key on a column (`unrecognized_keys`), and a dataset-level `applyOn` is pointed at `columns`.
32+
33+
`@objectstack/skills` only regenerates its schema reference indexes, which now list `SeedColumnSchema` and `ColumnApplyOnSchema`.

‎content/docs/data-modeling/seed-data.mdx‎

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,52 @@ so a typo like `'complete'` for `'completed'` still surfaces before boot.
147147

148148
---
149149

150+
## Columns Your App Owns After the First Seed — `applyOn: 'create'`
151+
152+
An `upsert` replay re-asserts every column a record declares on the row it
153+
matched. That is right for master data and wrong for a column the app itself
154+
owns once the row exists: a status a scheduled job advances, a flag a job sets
155+
after it sends a notice, a reference a workflow re-points. Replayed, the seed
156+
would put those back, undoing the job's work on every boot.
157+
158+
Declare such a column `applyOn: 'create'` under `columns`:
159+
160+
```typescript
161+
defineSeed(Instalment, {
162+
externalId: 'code',
163+
mode: 'upsert',
164+
columns: {
165+
status: { applyOn: 'create' }, // the payment job owns it after the first seed
166+
is_expiring: { applyOn: 'create' }, // the renewal job sets it
167+
},
168+
records: [
169+
{ code: 'INS-001', amount: 1200, status: 'pending', is_expiring: false },
170+
],
171+
});
172+
```
173+
174+
- When the seed **creates** the row, every column is written, `status` and
175+
`is_expiring` included.
176+
- When a replay **matches** an existing row, `status` and `is_expiring` are left
177+
out of the write: the row keeps the values the jobs gave it, while `amount`
178+
(`applyOn: 'always'`, the default) is refreshed. A row whose only differences
179+
are create-only columns is not written at all.
180+
- An `update` dataset never writes a create-only column. `insert`, `ignore` and
181+
`replace` never update a matched row, so the policy changes nothing there.
182+
- A create-only **reference** on a matched row is not resolved at all, so a
183+
replay neither re-points it nor reports it.
184+
- A column of the `externalId` is what the row was matched by, so it stays in
185+
the update whatever it declares.
186+
187+
`applyOn` takes the same values, with the same default and meaning, as an import
188+
mapping entry's [`applyOn`](/docs/data-modeling/import-mappings). A `columns` key
189+
must name a field of the object (or a system column the platform injects on it,
190+
such as `owner_id`): `defineSeed` refuses one that does not, and the loader logs
191+
an error naming the column whenever it loads a seed that names one, since a
192+
stored seed never passes through `defineSeed`.
193+
194+
---
195+
150196
## Environment Scoping
151197

152198
The `env` array controls which deployment environments receive the records. The
@@ -587,6 +633,7 @@ function defineSeed<
587633
config: {
588634
externalId?: string | string[]; // single field, or a composite list (join tables); default: 'name'
589635
mode?: 'insert' | 'update' | 'upsert' | 'replace' | 'ignore'; // default: 'upsert'
636+
columns?: { [field: string]: { applyOn?: 'create' | 'always' } }; // per-column write policy; default: 'always'
590637
env?: Array<'prod' | 'dev' | 'test'>; // default: ['prod','dev','test']
591638
locale?: string[]; // BCP-47 tags; omitted = every locale
592639
records: Array<SeedRecord<TObj['fields']>>; // declared fields + injectable system columns
@@ -596,7 +643,8 @@ function defineSeed<
596643

597644
`SeedRecord` keys are the object's declared fields (a `lookup` / `master_detail`
598645
field takes its target's natural-key string) plus the injectable system column
599-
names. Every record key is checked again when the call runs.
646+
names. Every record key, and every `columns` key, is checked again when the call
647+
runs.
600648

601649
The returned `Seed` object is a plain serialisable value — pass it to your
602650
stack's seed runner or store it in an export array.

‎content/docs/getting-started/quick-reference.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Categories that have no section here at all are named under
2222
[Categories Without a Section](#categories-without-a-section) — that curation is stated,
2323
not left implicit.
2424

25-
## Data Protocol (15 of 29 schemas)
25+
## Data Protocol (15 of 30 schemas)
2626

2727
Core business logic and data modeling schemas.
2828

‎content/docs/references/api/package-api-assembled.mdx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ Installed package row whose manifest is the assembled package body
9999
| **dependencies** | `Record<string, string>` | optional | Package dependencies |
100100
| **configuration** | `never` | optional | [REMOVED] `manifest.configuration` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — nothing ever read the block: no settings UI rendered it and no loader resolved a setting from it, so authoring it configured nothing. Worse, `properties.*.secret` promised "value is encrypted/masked (e.g. API Keys)" while nothing encrypted, masked or even parsed the flag — a false assurance about credential handling. Delete the key. A plugin is configured by the host that composes it: pass options to its constructor in `defineStack({ plugins: [new MyPlugin({ … })] })`, which is the enforced channel. A declarative settings surface must be designed with an enforcing reader first, not revived here. |
101101
| **contributes** | `{ kinds?: object[] }` | optional | Platform contributions |
102-
| **data** | `{ object: string; externalId?: string \| string[]; mode?: Enum<'insert' \| 'update' \| 'upsert' \| 'replace' \| 'ignore'>; env?: Enum<'prod' \| 'dev' \| 'test'>[]; … }[]` | optional | Seed Data / Fixtures for bootstrapping |
102+
| **data** | `{ object: string; externalId?: string \| string[]; mode?: Enum<'insert' \| 'update' \| 'upsert' \| 'replace' \| 'ignore'>; columns?: Record<string, object>; … }[]` | optional | Seed Data / Fixtures for bootstrapping |
103103
| **capabilities** | `{ name: string; label?: string; description?: string; scope?: Enum<'platform' \| 'org'>; … }[]` | optional | [ADR-0066 D1] Authorization capabilities this package defines (seeded with package provenance) |
104104
| **extensions** | `never` | optional | [REMOVED] `manifest.extensions` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — an untyped map with zero readers: whatever was parked here was stored and never consulted. Delete the key. Extend the platform through the enforced channels instead: `contributes.kinds` registers metadata kinds, `navigationContributions` injects navigation into other packages' apps, and code-level extension happens in the plugin itself (`init`/`start`). |
105105
| **navigationContributions** | `{ app: string; group?: string; priority?: integer; items: (object \| … +9 more)[] }[]` | optional | Navigation items this package contributes into apps owned by other packages |
@@ -281,7 +281,7 @@ This schema accepts one of the following structures:
281281
| **dependencies** | `Record<string, string>` | optional | Package dependencies |
282282
| **configuration** | `never` | optional | [REMOVED] `manifest.configuration` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — nothing ever read the block: no settings UI rendered it and no loader resolved a setting from it, so authoring it configured nothing. Worse, `properties.*.secret` promised "value is encrypted/masked (e.g. API Keys)" while nothing encrypted, masked or even parsed the flag — a false assurance about credential handling. Delete the key. A plugin is configured by the host that composes it: pass options to its constructor in `defineStack({ plugins: [new MyPlugin({ … })] })`, which is the enforced channel. A declarative settings surface must be designed with an enforcing reader first, not revived here. |
283283
| **contributes** | `{ kinds?: object[] }` | optional | Platform contributions |
284-
| **data** | `{ object: string; externalId?: string \| string[]; mode?: Enum<'insert' \| 'update' \| 'upsert' \| 'replace' \| 'ignore'>; env?: Enum<'prod' \| 'dev' \| 'test'>[]; … }[]` | optional | Initial seed data (prefer top-level data field) |
284+
| **data** | `{ object: string; externalId?: string \| string[]; mode?: Enum<'insert' \| 'update' \| 'upsert' \| 'replace' \| 'ignore'>; columns?: Record<string, object>; … }[]` | optional | Initial seed data (prefer top-level data field) |
285285
| **capabilities** | `never` | optional | [REMOVED] `manifest.capabilities` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — no discovery path ever consulted the block: nothing read `implements`, `provides`, `requires`, `extensionPoints` or `extensions`, so the declared "interoperability and automatic discovery" never happened. Delete the key. Real dependency resolution runs off top-level `manifest.dependencies`, which stays. Capability-based discovery must be designed with an enforcing reader first, not revived here. |
286286
| **extensions** | `never` | optional | [REMOVED] `manifest.extensions` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — an untyped map with zero readers: whatever was parked here was stored and never consulted. Delete the key. Extend the platform through the enforced channels instead: `contributes.kinds` registers metadata kinds, `navigationContributions` injects navigation into other packages' apps, and code-level extension happens in the plugin itself (`init`/`start`). |
287287
| **navigationContributions** | `{ app: string; group?: string; priority?: integer; items: (object \| … +9 more)[] }[]` | optional | Navigation items this package contributes into apps owned by other packages |
@@ -345,7 +345,7 @@ This schema accepts one of the following structures:
345345
| **dependencies** | `Record<string, string>` | optional | Package dependencies |
346346
| **configuration** | `never` | optional | [REMOVED] `manifest.configuration` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — nothing ever read the block: no settings UI rendered it and no loader resolved a setting from it, so authoring it configured nothing. Worse, `properties.*.secret` promised "value is encrypted/masked (e.g. API Keys)" while nothing encrypted, masked or even parsed the flag — a false assurance about credential handling. Delete the key. A plugin is configured by the host that composes it: pass options to its constructor in `defineStack({ plugins: [new MyPlugin({ … })] })`, which is the enforced channel. A declarative settings surface must be designed with an enforcing reader first, not revived here. |
347347
| **contributes** | `{ kinds?: object[] }` | optional | Platform contributions |
348-
| **data** | `{ object: string; externalId?: string \| string[]; mode?: Enum<'insert' \| 'update' \| 'upsert' \| 'replace' \| 'ignore'>; env?: Enum<'prod' \| 'dev' \| 'test'>[]; … }[]` | optional | Seed Data / Fixtures for bootstrapping |
348+
| **data** | `{ object: string; externalId?: string \| string[]; mode?: Enum<'insert' \| 'update' \| 'upsert' \| 'replace' \| 'ignore'>; columns?: Record<string, object>; … }[]` | optional | Seed Data / Fixtures for bootstrapping |
349349
| **capabilities** | `{ name: string; label?: string; description?: string; scope?: Enum<'platform' \| 'org'>; … }[]` | optional | [ADR-0066 D1] Authorization capabilities this package defines (seeded with package provenance) |
350350
| **extensions** | `never` | optional | [REMOVED] `manifest.extensions` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — an untyped map with zero readers: whatever was parked here was stored and never consulted. Delete the key. Extend the platform through the enforced channels instead: `contributes.kinds` registers metadata kinds, `navigationContributions` injects navigation into other packages' apps, and code-level extension happens in the plugin itself (`init`/`start`). |
351351
| **navigationContributions** | `{ app: string; group?: string; priority?: integer; items: (object \| … +9 more)[] }[]` | optional | Navigation items this package contributes into apps owned by other packages |

0 commit comments

Comments
 (0)