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
64 changes: 64 additions & 0 deletions .changeset/metadata-customization-protocol-retired.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
---
"@objectstack/spec": minor
"@objectstack/metadata": minor
---

feat(spec): retire the paper metadata-customization protocol with its full coupling set (#13135, re-charter of #12057; ADR-0049, ADR-0126)

<!-- adr-0087: registered metadata-customization-protocol-retired -->

**BREAKING** export removal + authorable-key retirement, landing after the
v17.0.0 cut (the lockstep launch-window convention ships it as `minor`; the
prescriptions are registered under protocol major 18 —
`RETIRED_DEFS_BY_MAJOR[18]`, `RETIRED_KEYS_BY_MAJOR[18]` and the D3 semantic
entry `metadata-customization-protocol-retired` — where `os migrate meta`
users will look).

`kernel/metadata-customization.zod.ts` declared a three-layer platform/user
patch-overlay protocol (field-level change tracking, customization policies, a
3-way-merge story) that nothing reachable implemented: no route ever served
the paper `…/overlay` / `…/effective` endpoints, the only implementation
(`packages/metadata`'s manager limb) was called solely by its own unit tests,
no merge engine ever existed, and no code read a `CustomizationPolicy`.
ADR-0126 §6 wall 4 supersedes the protocol as a matter of record ("nothing may
build against it"); the maintainer adopted retirement on #12057 (2026-08-29,
「同意」), and #13135 charters the full coupling set the fork report measured.

FROM → TO:

- `MetadataOverlaySchema` / `FieldChangeSchema` / `CustomizationOriginSchema` /
`MergeConflictSchema` / `MergeStrategyConfigSchema` / `MergeResultSchema` /
`CustomizationPolicySchema` and their `…`/`…Parsed` types
(`@objectstack/spec/kernel`) → *(removed — no replacement protocol)*. The
customization that actually ships: ADR-0005's org-scoped overlay
(`allowOrgOverride` on `DEFAULT_METADATA_TYPE_REGISTRY`, `sys_metadata` org
rows, layered read `code`/`overlay`/`effective`) and ADR-0126's
packaged-metadata model (clone + ledger disable).
- `MetadataOverlayResponseSchema` / `MetadataOverlaySaveRequestSchema` /
`MetadataEffectiveResponseSchema` (`@objectstack/spec/api` §5) →
*(removed)* — contracts for endpoints no adapter ever served; the layered
read's contracts (`getMetaItemLayered`) are the live API.
- `IMetadataService.getOverlay` / `.saveOverlay` / `.removeOverlay` /
`.getEffective` optional members (`@objectstack/spec/contracts`) →
*(removed)*, together with `packages/metadata`'s in-memory limb and its
`'overlay'` feature log entry.
- `MetadataPluginConfig.customizationPolicies` / `.mergeStrategy` and
`MetadataManagerConfig.persistence.overlayWritable` → *(removed — retiredKey
tombstones)*: authoring one is now a `tsc` error and a parse error carrying
the prescription. Delete the keys; nothing replaces them (`persistence.writable`
remains the base write gate).

One-line fix: delete the keys and any code building against the removed
exports — they configured and described nothing that ever ran; org-level
customization keeps riding the ADR-0005 overlay unchanged.

The retirement kit: whole-module deletion + kernel barrel line; 10
`RETIRED_DEFS_BY_MAJOR[18]` entries (7 kernel defs + 3 api §5 contracts); 3
`RETIRED_KEYS_BY_MAJOR[18]` tombstone entries (no D2 conversion —
plugin/manager configs are not stack collection members, the
`kernel/MetadataPluginConfig:additionalTypes` precedent); D3 semantic entry
`metadata-customization-protocol-retired`; retirement pin test
(`kernel/metadata-customization-retirement.test.ts`); type-alias pin rows
Iso408-411 vacated; api-surface / export-origins / json-schema manifest /
authorable-surface / reference docs regenerated (the
`kernel/metadata-customization` reference page disappears with the module).
2 changes: 1 addition & 1 deletion content/docs/getting-started/quick-reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ Presentation layer - views, forms, dashboards, and app branding.
| **[Chart](/docs/references/ui/chart)** | `chart.zod.ts` | Chart, ChartType | Chart definitions |
| **[Widget Contract](/docs/protocol/objectui/widget-contract)** ↗ | `widget.zod.ts` | FieldWidgetProps | Props a custom field widget receives — the contract is documented with ObjectUI, outside `references/ui/` |

## Kernel Protocol (17 of 31 schemas)
## Kernel Protocol (17 of 30 schemas)

Plugin architecture, manifests, and kernel runtime.

Expand Down
82 changes: 17 additions & 65 deletions content/docs/kernel/contracts/metadata-service.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: IMetadataService Contract
description: Reference for the Metadata Service contract — CRUD operations for object and field definitions, schema registry, overlay management, and import/export
description: Reference for the Metadata Service contract — CRUD operations for object and field definitions, schema registry, and import/export
---

The Metadata Service manages all object and field definitions at runtime. It serves as the **schema registry** — plugins, the Kernel, and the API layer all query this service to discover what objects exist and what fields they contain.
Expand Down Expand Up @@ -61,12 +61,6 @@ export interface IMetadataService {
bulkRegister?(items: Array<{ type: string; name: string; data: unknown }>, options?: { continueOnError?: boolean; validate?: boolean }): Promise<MetadataBulkResult>;
bulkUnregister?(items: Array<{ type: string; name: string }>): Promise<MetadataBulkResult>;

// Overlay / customization (optional)
getOverlay?(type: string, name: string, scope?: 'platform' | 'user'): Promise<MetadataOverlay | undefined>;
saveOverlay?(overlay: MetadataOverlay): Promise<void>;
removeOverlay?(type: string, name: string, scope?: 'platform' | 'user'): Promise<void>;
getEffective?(type: string, name: string, context?: { userId?: string; tenantId?: string; positions?: string[]; permissions?: string[] }): Promise<unknown | undefined>;

// Watch / subscribe (optional)
watch?(type: string, callback: MetadataWatchCallback): MetadataWatchHandle;

Expand Down Expand Up @@ -206,35 +200,16 @@ const validation = await metadataService.validate('object', definition);

---

## Overlay Management

Overlays customize a metadata item without modifying the base (system) definition.
A `MetadataOverlay` references the target by `baseType` + `baseName`, carries a JSON
Merge Patch in `patch`, and resolves in the order **system ← platform ← user**.

```typescript
// Save a platform-scope overlay
await metadataService.saveOverlay({
id: 'overlay-platform-1',
baseType: 'object',
baseName: 'task',
scope: 'platform',
patch: { label: 'Work Item' },
});
## Overlay Management — removed

// Read the merged (effective) definition with overlays applied
const effective = await metadataService.getEffective('object', 'task', {
userId: 'user-123',
});
```

| Property | Type | Description |
|:---|:---|:---|
| `baseType` | `string` | Metadata type being customized |
| `baseName` | `string` | Metadata name being customized |
| `scope` | `'platform' \| 'user'` | Customization scope (default `platform`) |
| `owner` | `string` | Owner user ID, for `user`-scope overlays |
| `patch` | `object` | JSON Merge Patch (changed fields only) |
The optional `getOverlay` / `saveOverlay` / `removeOverlay` / `getEffective`
members and their `MetadataOverlay` record were removed in #13135 (ADR-0049
enforce-or-remove): they belonged to a paper customization protocol no route
ever served, and ADR-0126 supersedes it on the record. Org-scoped
customization is [ADR-0005's metadata overlay](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0005-metadata-customization-overlay.md)
— opt-in per type via `allowOrgOverride`, written through the REST meta write
doors, and read back through the layered read (`code` / `overlay` /
`effective`).

---

Expand Down Expand Up @@ -293,7 +268,6 @@ console.log(result.failed); // failed
| Type | Description |
|:---|:---|
| `MetadataQuery` / `MetadataQueryResult` | Query parameters and paginated result for `query()` |
| `MetadataOverlay` | Runtime customization layer (`baseType`, `baseName`, `scope`, `patch`) |
| `MetadataExportOptions` | `{ types?, namespaces?, format? }` for `exportMetadata` |
| `MetadataImportOptions` | `{ conflictResolution?, validate?, dryRun? }` for `importMetadata` |
| `MetadataImportResult` | `{ total, imported, skipped, failed, errors? }` |
Expand Down Expand Up @@ -321,36 +295,14 @@ const views = await metadataService.listViews('account');
const dashboard = await metadataService.get('dashboard', 'sales_overview');
```

### User-Level Customization
### Org-Level Customization

Users can customize views via the overlay system:

```typescript
// Admin customizes a view for all users
await metadataService.saveOverlay({
id: 'overlay-platform-1',
baseType: 'view',
baseName: 'account.default',
scope: 'platform',
patch: { columns: ['name', 'email', 'status', 'created_at'] },
});

// A specific user saves personal column preferences
await metadataService.saveOverlay({
id: 'overlay-user-123',
baseType: 'view',
baseName: 'account.default',
scope: 'user',
owner: 'user-123',
patch: { columns: ['name', 'status'] }, // user only wants 2 columns
});

// Resolve effective view for a specific user
const effectiveView = await metadataService.getEffective('view', 'account.default', {
userId: 'user-123',
});
// Result: base view ← platform overlay ← user-123 overlay
```
Per-org view customization rides ADR-0005's metadata overlay (opt-in per type
via `allowOrgOverride`, `view` among the overlay types): an org-scoped write
through the REST meta doors stores a `sys_metadata` row, and the layered read
returns `code` / `overlay` / `effective` for it. The per-user, per-field patch
overlay a previous revision of this page taught here was removed in #13135 —
it was never served by any route.

### Permission-Based UI Filtering

Expand Down
12 changes: 8 additions & 4 deletions content/docs/protocol/kernel/metadata-service.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -227,21 +227,25 @@ instance goes through `@objectstack/cloud-connection` (`os package install`).
A still-configured `artifact-api` source fails loudly at `start()` rather
than silently falling back to the filesystem scan.

### 2. Persistence Write Gates
### 2. Persistence Write Gate

`MetadataManagerConfigSchema.persistence` is a two-axis runtime freeze. Both flags default to `true`.
`MetadataManagerConfigSchema.persistence` is a runtime freeze. The flag defaults to `true`.

| Flag | Effect when `false` |
| :--- | :--- |
| `persistence.writable` | `register()` becomes a no-op (or throws when `validation.throwOnError`). |
| `persistence.overlayWritable` | `saveOverlay()` is rejected. Disables Studio overlays in sealed deployments. |

```typescript
new MetadataManager({
persistence: { writable: false, overlayWritable: false },
persistence: { writable: false },
});
```

(`persistence.overlayWritable` was removed in #13135 with the paper
metadata-customization protocol — the `saveOverlay()` it gated was never
reachable from any served surface. Authoring it is now a compile-time and
parse-time error carrying the prescription.)

### 3. DatabaseLoader Read-Through Cache

`DatabaseLoader` wraps `load` / `loadMany` / `list` / `stat` results in a generic LRU cache (lazy TTL, promote-on-get, write invalidation). Reads always observe writes performed through the same loader instance; out-of-band SQL writes are honored within `ttl` milliseconds.
Expand Down
Loading
Loading