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
60 changes: 60 additions & 0 deletions .changeset/manifest-contributes-dead-members-retired.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
---
"@objectstack/spec": minor
---

feat(spec): retire the nine dead members of the plugin-manifest `contributes` block — `events` / `menus` / `themes` / `translations` / `actions` / `drivers` / `fieldTypes` / `functions` / `commands` (#10724, ADR-0049 enforce-or-remove)

<!-- adr-0087: registered plugin-manifest-contributes-dead-members-retired -->

**BREAKING** accept-set narrowing, landing after the v17.0.0 cut (the lockstep
launch-window convention ships it as `minor`; the migration prescription is
registered under protocol major 18, where `os migrate meta` users will look).

The census behind it (#10627, controlled and re-verified at claim time across
objectstack, objectui and cloud) measured that the ENTIRE monorepo contains
exactly one non-test read of `manifest.contributes`, and it reads `kinds`
(`packages/objectql/src/engine.ts` → `registry.registerKind`). The other nine
members parsed, entered the manifest, and changed nothing — while published
material kept teaching them: `commands` documented Commander.js runtime
resolution the CLI dropped for oclif auto-discovery, `fieldTypes` advertised a
registration seam that has never existed, and `events` was decorative even for
its only in-repo author, which already subscribes imperatively.

**What is refused:** authoring any of the nine keys. Each is a `retiredKey()`
tombstone (the `manifest.loading` precedent — neither `ManifestSchema` nor the
`contributes` object is `.strict()`, so a plain deletion would have silently
stripped the keys), so authoring one is a `tsc` error and a parse error
carrying the per-key prescription.

**FROM → TO, per member** (each tombstone carries its own one-line fix):

- `contributes.events` → subscribe in plugin code (`ctx.hook('kernel:ready', …)`
from `init`/`start`); delete the key.
- `contributes.menus` → app `navigation` / `manifest.navigationContributions`
(ADR-0029 D7); delete the key.
- `contributes.themes` → the stack-level `themes` metadata collection (an
unrelated `ThemeSchema` surface); delete the key.
- `contributes.translations` → the `translation` metadata type:
`defineTranslationBundle` in `defineStack({ translations })`; delete the key.
- `contributes.actions` → the stack `actions` collection or
`engine.registerAction`; delete the key.
- `contributes.drivers` → register a kernel service named `driver.*`; delete
the key.
- `contributes.fieldTypes` → nothing (no registration seam exists; the
vocabulary is the spec `FieldType` enum); delete the key.
- `contributes.functions` → `defineStack({ functions })`; delete the key.
- `contributes.commands` → oclif native plugin auto-discovery (an `oclif`
section in the plugin's own `package.json`; see `cli-extension.zod.ts`);
delete the key.

**What stays:** `contributes.kinds` (the block's one live member) and
`contributes.routes` (an open enforce-or-remove fork, #10726 — deliberately
untouched here). Runtime behaviour is unchanged: nothing ever read the nine
members, so removing them removes no behaviour; a stored manifest still
carrying one degrades to a single `[metadata_spec_invalid]` log line at
registration rather than a boot failure.

D3 semantic entry `plugin-manifest-contributes-dead-members-retired`; no D2
conversion, because a package manifest is not a stack collection member
(`PLURAL_TO_SINGULAR` has no `packages`/`plugins` entry) and a conversion
would be a transform with no seam that ever runs.
39 changes: 39 additions & 0 deletions .changeset/manifest-kind-globs-retired.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
---
"@objectstack/spec": minor
---

feat(spec): retire `contributes.kinds[].globs` — the declared file-type watch patterns nothing ever read (#11169, ADR-0049 enforce-or-remove; maintainer-ruled 2026-08-24)

<!-- adr-0087: registered plugin-manifest-kind-globs-retired -->

**BREAKING** accept-set narrowing, landing after the v17.0.0 cut (the lockstep
launch-window convention ships it as `minor`; the migration prescription is
registered under protocol major 18, where `os migrate meta` users will look).

**Removed key:** `manifest.contributes.kinds[].globs`.

The schema promised that declaring `globs` "enables the system to parse and
validate new file types" (its own example: a BI plugin handling
`*.report.ts`). The promise was never kept: real glob-driven artifact
discovery reads `filePatterns` off the metadata type registry — which
`contributes.kinds` does not extend, as `metadata-plugin.zod.ts` records
outright — so an authored `globs` was accepted, stored, served back through
`GET /metadata/kind`, and never consulted. Measured (PR #11168, re-verified
with positive control at claim): zero value reads anywhere; the only non-test
occurrences of the path were the schema declaration and two type positions.

**FROM → TO:** `kinds: [{ id, globs: […], description? }]` →
`kinds: [{ id, description? }]` — delete the `globs` key; the kind's `id` and
`description` are unchanged and still register. The key is a `retiredKey()`
tombstone, so authoring it is a `tsc` error and a parse error carrying this
prescription.

**What stays:** the `contributes.kinds` bucket itself and its `id` field
(live: engine → `registry.registerKind`, served via `GET /metadata/kind`).
File-type discovery remains single-channel on the metadata type registry's
`filePatterns`; if plugin-extensible discovery is ever wanted, it gets
designed against that registry, not revived here. The `registerKind` /
`getAllKinds` type positions drop `globs` (type-only; the parameter widens).

D3 semantic entry `plugin-manifest-kind-globs-retired`; no D2 conversion (a
manifest is not a stack collection member — no seam would ever run it).
77 changes: 48 additions & 29 deletions content/docs/protocol/kernel/i18n-standard.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -590,44 +590,63 @@ context.i18n.formatNumber(smallNumber, {

## Plugin Integration

Plugins register translation bundles in their manifest:
Translations are **metadata**, declared on the stack — not manifest entries.
(The former `contributes.translations` manifest key was removed in v17 (#10724,
ADR-0049): no loader ever read its `{ locale, path }` entries, so a manifest
that still carries it now fails the parse with the upgrade prescription.)

A package declares a translation bundle and registers it in its stack's
`translations` collection:

{/* os:check */}
```typescript
// plugin.manifest.ts — a plugin manifest validated by `ManifestSchema`
// from `@objectstack/spec/kernel` (there is no `definePlugin()` helper).
// Translation files are registered under `contributes.translations` as
// { locale, path } entries (see packages/spec/src/kernel/manifest.zod.ts).
const manifest = {
id: 'com.mycompany.crm',
name: '@mycompany/crm',
version: '1.0.0',

contributes: {
translations: [
{ locale: 'en', path: 'i18n/en/account.json' },
{ locale: 'de', path: 'i18n/de/account.json' },
],
// src/translations/crm.translation.ts
import { defineTranslationBundle } from '@objectstack/spec/system';

export const CrmTranslationBundle = defineTranslationBundle({
en: {
objects: {
crm_account: {
label: 'Account',
pluralLabel: 'Accounts',
fields: {
name: { label: 'Account Name' },
},
},
},
},
};

export default manifest;
de: {
objects: {
crm_account: {
label: 'Konto',
pluralLabel: 'Konten',
fields: {
name: { label: 'Kontoname' },
},
},
},
},
});
```

### Translation File Registration
```typescript
// objectstack.config.ts
import { defineStack } from '@objectstack/spec';
import { CrmTranslationBundle } from './src/translations/crm.translation.js';

```
@mycompany/crm/
i18n/
en/
account.json → Namespace: crm.account
contact.json → Namespace: crm.contact
de/
account.json
contact.json
export default defineStack({
manifest: {
id: 'com.mycompany.crm',
version: '1.0.0',
type: 'app',
name: 'CRM',
},
translations: [CrmTranslationBundle],
});
```

**Namespace Convention:** `{pluginName}.{filename}`
The engine registers the collection as `translation` metadata (the governed
`translation` type), and the i18n pipeline serves it.

### Using Plugin Translations

Expand Down
2 changes: 1 addition & 1 deletion content/docs/references/kernel/manifest.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,7 +40,7 @@ const result = ManifestSchema.parse(data);
| **datasources** | `string[]` | optional | Glob patterns for Datasource definitions |
| **dependencies** | `Record<string, string>` | optional | Package dependencies |
| **configuration** | `{ title?: string; properties: Record<string, object> }` | optional | Plugin configuration settings |
| **contributes** | `{ kinds?: object[]; events?: string[]; menus?: Record<string, object[]>; themes?: object[]; … }` | optional | Platform contributions |
| **contributes** | `{ kinds?: object[]; routes?: object[] }` | optional | Platform contributions |
| **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) |
| **capabilities** | `{ implements?: object[]; provides?: object[]; requires?: object[]; extensionPoints?: object[]; … }` | optional | Plugin capability declarations for interoperability |
| **extensions** | `Record<string, any>` | optional | Extension points and contributions |
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -261,7 +261,7 @@ directory rather than per file.
| `cloud/` | 83 |
| `identity/` | 32 |
| `integration/` | 10 |
| `kernel/` | 296 |
| `kernel/` | 288 |
| `qa/` | 6 |
| `shared/` | 20 |
| `system/` | 360 |
13 changes: 4 additions & 9 deletions packages/drivers/driver-memory/objectstack.config.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -247,15 +247,10 @@ const MemoryDriverPlugin: ObjectStackManifest = {
extensions: [],
},

contributes: {
drivers: [
{
id: 'memory',
label: 'In-Memory Storage',
description: 'Stores data in memory (volatile, for testing/development)',
},
],
}
// `contributes.drivers` was retired (#10724, ADR-0049): the declaration drove
// nothing — this driver is wired by registering the `driver.memory` kernel
// service (the objectql plugin calls `registerDriver` on `driver.*` services),
// which is the enforced channel.
};

export default MemoryDriverPlugin;
9 changes: 4 additions & 5 deletions packages/objectql/src/engine-kind-registration-log.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -52,9 +52,9 @@ describe('[#10729] contributes.kinds registration logging', () => {
engine.registerApp({
id: 'com.example.bi',
contributes: {
// Exactly the schema's shape — and exactly its own documented example
// ("Registering a BI plugin to handle *.report.ts").
kinds: [{ id: 'sys.bi.report', globs: ['**/*.report.ts'] }],
// Exactly the schema's shape: `{ id, description? }` (`globs` was
// retired unread, #11169).
kinds: [{ id: 'sys.bi.report', description: 'BI reports' }],
},
});

Expand All@@ -73,7 +73,7 @@ describe('[#10729] contributes.kinds registration logging', () => {

engine.registerApp({
id: 'com.example.bi',
contributes: { kinds: [{ id: 'sys.bi.report', globs: ['**/*.report.ts'] }] },
contributes: { kinds: [{ id: 'sys.bi.report' }] },
});

// `registerKind` → `registerItem('kind', kind, 'id')`. The value in the log
Expand All@@ -91,7 +91,6 @@ describe('[#10729] contributes.kinds registration logging', () => {
contributes: {
kinds: [{
id: 'sys.bi.report',
globs: ['**/*.report.ts'],
// Neither key is declared by the schema. They are what the old line
// reached for, so an author who copied an ancient example could put
// them here — and the log must NOT start preferring them again.
Expand Down
3 changes: 2 additions & 1 deletion packages/objectql/src/engine.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4606,7 +4606,8 @@ export class ObjectQL implements IObjectQLEngine {
for (const kind of manifest.contributes.kinds) {
this._registry.registerKind(kind);
// [#10729] Name the kind by its declared `id`. `contributes.kinds`
// items are `{ id, globs, description? }` (`manifest.zod.ts`) and
// items are `{ id, description? }` (`manifest.zod.ts`; `globs` was
// retired unread, #11169) and
// `registerKind` keys the item on `id` (`registerItem('kind', kind, 'id')`),
// so `id` is BOTH the only identifying field the schema declares and the
// exact key the item is stored under — a reader of this line can look the
Expand Down
9 changes: 6 additions & 3 deletions packages/objectql/src/registry.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3745,11 +3745,14 @@ export class SchemaRegistry {
// Kind Helpers
// ==========================================

registerKind(kind: { id: string, globs: string[] }) {
// #11169: `globs` left the kind shape — nothing ever read the value here or
// downstream (the two type positions below were its only non-schema
// occurrences); a kind entry is `{ id, description? }`, keyed on `id`.
registerKind(kind: { id: string; description?: string }) {
this.registerItem('kind', kind, 'id');
}
getAllKinds(): { id: string, globs: string[] }[] {

getAllKinds(): { id: string; description?: string }[] {
return this.listItems('kind');
}

Expand Down
10 changes: 3 additions & 7 deletions packages/plugins/plugin-hono-server/objectstack.config.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -230,13 +230,9 @@ const HonoServerPlugin: ObjectStackManifest = {
extensions: [],
},

contributes: {
// System Events
events: [
'kernel:ready',
'kernel:listening',
],
},
// `contributes.events` was retired (#10724, ADR-0049): the declaration drove
// nothing — this plugin already subscribes to `kernel:ready` / `kernel:listening`
// imperatively in its own code, which is the enforced channel.
};

export default HonoServerPlugin;
Loading
Loading