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
11 changes: 7 additions & 4 deletions .changeset/duration-unit-in-key-name.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"@objectstack/spec": minor
---

feat(spec)!: a duration-shaped `z.number()` key carries its unit in the key name — `hook.timeout` / `job.timeout` / `DriverOptions.timeout` → `timeoutMs`, `MetadataManagerConfig.cache.ttl` → `ttlSeconds`, `cache.databaseLoader.ttl` → `ttlMs`, tenant `idleTimeout` / `sessionTimeout` → `*Seconds`; new gate `check:duration-unit-keys` (#14478, #14519)
feat(spec)!: a duration-shaped `z.number()` key carries its unit in the key name — `hook.timeout` / `job.timeout` / `DriverOptions.timeout` → `timeoutMs`, `MetadataManagerConfig.cache.databaseLoader.ttl` → `ttlMs` (the outer `cache.ttl` leaves outright under #15624 — nothing read it), tenant `idleTimeout` / `sessionTimeout` → `*Seconds`; new gate `check:duration-unit-keys` (#14478, #14519)

<!-- adr-0087: registered hook-timeout-to-timeout-ms, job-timeout-to-timeout-ms, metadata-manager-config-cache-ttl-unit-in-key, driver-options-timeout-to-timeout-ms, tenant-timeouts-unit-in-key -->

Expand Down Expand Up @@ -38,7 +38,7 @@ comment and published a bare `300` / `3600` to the reference page (#14519).
| `HookSchema` (`hooks[]`) | `timeout` | `timeoutMs` | unchanged (ms) |
| `JobSchema` (`jobs[]`) | `timeout` | `timeoutMs` | unchanged (ms) |
| `DriverOptionsSchema` | `timeout` | `timeoutMs` | unchanged (ms) |
| `MetadataManagerConfigSchema` | `cache.ttl` | `cache.ttlSeconds` | unchanged (s, default 3600) |
| `MetadataManagerConfigSchema` | `cache.ttl` | *(deleted — its respelling `ttlSeconds` was retired before it shipped, #15624; the outer `cache` block was read by nothing, and the live TTL is `cache.databaseLoader.ttlMs`)* | — |
| `MetadataManagerConfigSchema` | `cache.databaseLoader.ttl` | `cache.databaseLoader.ttlMs` | unchanged (ms, default 60000) |
| `DatabaseLevelIsolationStrategySchema` | `connectionPool.idleTimeout` | `connectionPool.idleTimeoutSeconds` | unchanged (s, default 300) |
| `TenantSecurityPolicySchema` | `accessControl.sessionTimeout` | `accessControl.sessionTimeoutSeconds` | unchanged (s, default 3600) |
Expand All @@ -52,10 +52,13 @@ new MetadataManager({ cache: { ttl: 3600, databaseLoader: { ttl: 60_000 } } });
// after — rename the key; the number is unchanged
defineHook({ name: 'audit_order', object: 'order', events: ['afterInsert'], handler: 'auditOrder', timeoutMs: 5000 });
defineJob({ name: 'nightly_sweep', schedule: { type: 'cron', expression: '0 1 * * *' }, handler: 'sweep', timeoutMs: 300000 });
new MetadataManager({ cache: { ttlSeconds: 3600, databaseLoader: { ttlMs: 60_000 } } });
new MetadataManager({ cache: { databaseLoader: { ttlMs: 60_000 } } }); // the outer `ttl` is deleted, not renamed (#15624)
```

**Migration.** Rename each key; no value changes. Authoring an old spelling
**Migration.** Rename each key; no value changes — with one exception: the outer
`MetadataManagerConfig.cache.ttl` is DELETED, not renamed (its respelling `ttlSeconds`
was retired before it shipped, #15624; nothing ever read the outer `cache` block, and
the nested `cache.databaseLoader.ttl → ttlMs` rename above is unchanged). Authoring an old spelling
fails to compile (`tsc`: the input type is `never`) and fails to parse with a
prescription naming the new key. For `hooks[]` / `jobs[]` the rename is a
mechanical D2 conversion (`hook-timeout-to-timeout-ms`,
Expand Down
85 changes: 85 additions & 0 deletions .changeset/metadata-manager-inert-cache-keys-retired.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
---
"@objectstack/spec": minor
"@objectstack/metadata": patch
---

feat(spec)!: retire the three inert outer keys of `MetadataManagerConfig.cache` — `enabled`, `ttlSeconds` (formerly `ttl`) and `maxSize` — read by nothing; `cache.databaseLoader` is the only live half (#15624, ADR-0049)

<!-- adr-0087: registered metadata-manager-config-inert-cache-keys-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).
ADR-0049 enforce-or-remove decides it: a declared-but-unenforced key with zero
measured readers comes off, and the published reference page stops teaching it.

`MetadataManagerConfig.cache` declared three outer knobs — `enabled` (default
`true`), `ttlSeconds` (default 3600; spelled `ttl` until #14478) and `maxSize`
("Max cache size in bytes") — beside the nested `databaseLoader` block, and
**nothing read the outer three**. The only runtime consumer of the block is
`MetadataManager` (`packages/metadata`), which hands `cache.databaseLoader` and
nothing else to `new DatabaseLoader({ cache })`; a reader census over
`packages/**` (tests and changelogs excluded) found no runtime reader of any
outer key, while the same grep shape found the nested `cache?.databaseLoader`
read twice — the control that makes the zero a measurement. An author writing
`cache: { enabled: false }` or `cache: { ttlSeconds: 60 }` got a clean parse
and a cache that behaved exactly as before, with no error and no warning, and
the published reference page (`references/kernel/metadata-loader`) documented
all three as if they configured something.

**What is refused:** authoring `cache.enabled`, `cache.ttlSeconds`, `cache.ttl`
or `cache.maxSize` on `MetadataManagerConfig`, with any value — directly, through
`MetadataManagerOptions`, or through `MetadataPluginConfig.storage`. The nested
object is not `.strict()`, so each key is a `retiredKey()` tombstone rather than
a bare deletion (a deletion would have stripped it in silence — the same no-op
one layer down): authoring it is a `tsc` error (`never`) and a parse error
carrying the prescription, which names the live nested knob.

**What stays, byte-identical:** the DatabaseLoader read-through cache under
`cache.databaseLoader` — `enabled` (default `true`), `maxSize` (an entry count,
default 500) and `ttlMs` (milliseconds, default 60000) — and every runtime
path. Parsed configs no longer carry the two former defaults (`enabled: true`,
`ttlSeconds: 3600`) that were materialized and never consulted.

**The #14478 rename is folded in.** `cache.ttl` → `cache.ttlSeconds` was
registered under this same unreleased major and never reached a published
release, so it is absorbed by the removal: `cache.ttl`'s tombstone now
prescribes deletion (naming `cache.databaseLoader.ttlMs`) instead of a rename to
a key that is itself retired — an author upgrading from a published 17.x sees
one hop. The nested `cache.databaseLoader.ttl` → `ttlMs` half of that rename is
unchanged.

## FROM → TO

```ts
// before — parsed green; no runtime ever read the three outer numbers
new MetadataManager({
datasource: 'default',
cache: { enabled: true, ttlSeconds: 3600, maxSize: 10_485_760, databaseLoader: { ttlMs: 60_000 } },
});

// after — delete the outer keys; the nested block is the cache that runs
new MetadataManager({
datasource: 'default',
cache: { databaseLoader: { enabled: true, maxSize: 500, ttlMs: 60_000 } },
});
```

**Migration.** Delete `cache.enabled`, `cache.ttlSeconds` / `cache.ttl` and
`cache.maxSize`; nothing replaces them, because nothing ever consumed them. If
you meant to switch the cache off, cap it or set its TTL, write
`cache.databaseLoader.enabled` / `.maxSize` (entries) / `.ttlMs` (milliseconds)
— those are honoured. No `os migrate meta` conversion runs on this surface: a
`MetadataManager` config is not a stack collection member and never a stored
row, so the chain has no seam for it; the D3 semantic entry
`metadata-manager-config-inert-cache-keys-retired` carries the prescription
into `spec-changes.json`, the upgrade guide and the `spec_changes` MCP tool.

The retirement kit: `retiredKey()` tombstones on all three (and the absorbed
`ttl`), `RETIRED_KEYS_BY_MAJOR[18]` entries for each, the D3 semantic entry
above (the #14478 entry's outer half is re-worded from a rename to a deletion),
negative pins asserting each prescription and a positive pin asserting the
parse output no longer materializes the retired defaults, the published
reference pages regenerated, and the hand-written docs page and this package's
README (`@objectstack/metadata` ships `README.md`, hence its `patch`) no longer
authoring `cache.enabled`.
11 changes: 10 additions & 1 deletion content/docs/protocol/kernel/metadata-service.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -254,12 +254,21 @@ parse-time error carrying the prescription.)
new MetadataManager({
datasource: 'default',
cache: {
enabled: true,
databaseLoader: { enabled: true, maxSize: 500, ttlMs: 60_000 },
},
});
```

`cache.databaseLoader` is the only live member of the `cache` block. The outer
`cache.enabled`, `cache.ttlSeconds` (formerly `cache.ttl`) and `cache.maxSize`
were removed in #15624 (ADR-0049 enforce-or-remove): they were declared and
documented but read by nothing — `MetadataManager` hands `cache.databaseLoader`
and nothing else to the loader — so `cache: { enabled: false }` never switched
anything off. Authoring any of them is now a compile-time and parse-time error
carrying the prescription; the switch, TTL and cap that are honoured are
`databaseLoader.enabled`, `databaseLoader.ttlMs` (milliseconds) and
`databaseLoader.maxSize` (an entry count).

The cache exposes `LRUCache.stats()` (`size` / `hits` / `misses` / `hitRate`) for metrics.

### 4. Single-Source Schema Discipline
Expand Down
10 changes: 5 additions & 5 deletions content/docs/references/kernel/metadata-loader.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ const result = MetadataFallbackStrategySchema.parse(data);
| **fallback** | `Enum<'filesystem' \| 'memory' \| 'none'>` | optional (default: `"none"`) | Fallback strategy when datasource is unavailable |
| **rootDir** | `string` | optional | Root directory path |
| **formats** | `Enum<'yaml' \| 'json' \| 'typescript' \| 'javascript'>[]` | optional (default: `["typescript","json","yaml"]`) | Enabled formats |
| **cache** | `{ enabled: boolean; ttlSeconds: integer; maxSize?: integer; databaseLoader?: object }` | optional | Cache settings |
| **cache** | `{ databaseLoader?: object }` | optional | Cache settings — only `databaseLoader` is read at runtime; the outer keys are retired |
| **watch** | `boolean` | optional (default: `false`) | Enable file watching |
| **watchOptions** | `{ ignored?: string[]; persistent: boolean; ignoreInitial: boolean }` | optional | File watcher options |
| **validation** | `{ strict: boolean; throwOnError: boolean }` | optional | Validation settings |
Expand All @@ -60,10 +60,10 @@ const result = MetadataFallbackStrategySchema.parse(data);

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **enabled** | `boolean` | optional (default: `true`) | Enable caching |
| **ttlSeconds** | `integer` | optional (default: `3600`) | Cache TTL in seconds |
| **ttl** | `never` | optional | [REMOVED] `cache.ttl` was removed from `MetadataManagerConfig` in @objectstack/spec 17 — its unit (seconds) lived only in the description, while the nested `cache.databaseLoader.ttl` spelled the same word in milliseconds, so one key name meant two magnitudes 1000× apart. Rename the key to `ttlSeconds`; the value (seconds) is unchanged. |
| **maxSize** | `integer` | optional | Max cache size in bytes |
| **enabled** | `never` | optional | [REMOVED] `cache.enabled` was removed from `MetadataManagerConfig` in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — nothing ever read it: the outer `cache` block was declared and documented but consumed by no runtime, so `enabled: false` switched nothing off. Delete the key. The cache that actually runs is the DatabaseLoader read-through LRU under `cache.databaseLoader`; its `enabled` is the switch that is honoured. |
| **ttlSeconds** | `never` | optional | [REMOVED] `cache.ttlSeconds` was removed from `MetadataManagerConfig` in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — nothing ever read it: the outer `cache` block was declared and documented but consumed by no runtime, so the number expired nothing. Delete the key. The TTL that is honoured is `cache.databaseLoader.ttlMs` (milliseconds, default 60000) on the DatabaseLoader read-through cache. |
| **ttl** | `never` | optional | [REMOVED] `cache.ttl` was removed from `MetadataManagerConfig` in @objectstack/spec 17 — nothing ever read it: the outer `cache` block was declared and documented but consumed by no runtime, and its unit-suffixed respelling `ttlSeconds` was retired with it before it shipped (ADR-0049 enforce-or-remove). Delete the key. The TTL that is honoured is `cache.databaseLoader.ttlMs` (milliseconds, default 60000) on the DatabaseLoader read-through cache. |
| **maxSize** | `never` | optional | [REMOVED] `cache.maxSize` was removed from `MetadataManagerConfig` in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — nothing ever read it: the outer `cache` block was declared and documented but consumed by no runtime, so the byte cap capped nothing. Delete the key. The cap that is honoured is `cache.databaseLoader.maxSize` (an entry count, default 500) on the DatabaseLoader read-through cache. |
| **databaseLoader** | `{ enabled: boolean; maxSize: integer; ttlMs: integer }` | optional | DatabaseLoader read-through cache |

### Nested Shape: `MetadataManagerConfig.watchOptions`
Expand Down
2 changes: 1 addition & 1 deletion content/docs/references/kernel/metadata-plugin.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ const result = MetadataBulkResultSchema.parse(data);
| **fallback** | `Enum<'filesystem' \| 'memory' \| 'none'>` | optional (default: `"none"`) | Fallback strategy when datasource is unavailable |
| **rootDir** | `string` | optional | Root directory path |
| **formats** | `Enum<'yaml' \| 'json' \| 'typescript' \| 'javascript'>[]` | optional (default: `["typescript","json","yaml"]`) | Enabled formats |
| **cache** | `{ enabled: boolean; ttlSeconds: integer; maxSize?: integer; databaseLoader?: object }` | optional | Cache settings |
| **cache** | `{ databaseLoader?: object }` | optional | Cache settings — only `databaseLoader` is read at runtime; the outer keys are retired |
| **watch** | `boolean` | optional (default: `false`) | Enable file watching |
| **watchOptions** | `{ ignored?: string[]; persistent: boolean; ignoreInitial: boolean }` | optional | File watcher options |
| **validation** | `{ strict: boolean; throwOnError: boolean }` | optional | Validation settings |
Expand Down
10 changes: 5 additions & 5 deletions content/docs/references/system/metadata-persistence.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ Metadata file format
| **fallback** | `Enum<'filesystem' \| 'memory' \| 'none'>` | optional (default: `"none"`) | Fallback strategy when datasource is unavailable |
| **rootDir** | `string` | optional | Root directory path |
| **formats** | `Enum<'yaml' \| 'json' \| 'typescript' \| 'javascript'>[]` | optional (default: `["typescript","json","yaml"]`) | Enabled formats |
| **cache** | `{ enabled: boolean; ttlSeconds: integer; maxSize?: integer; databaseLoader?: object }` | optional | Cache settings |
| **cache** | `{ databaseLoader?: object }` | optional | Cache settings — only `databaseLoader` is read at runtime; the outer keys are retired |
| **watch** | `boolean` | optional (default: `false`) | Enable file watching |
| **watchOptions** | `{ ignored?: string[]; persistent: boolean; ignoreInitial: boolean }` | optional | File watcher options |
| **validation** | `{ strict: boolean; throwOnError: boolean }` | optional | Validation settings |
Expand All @@ -253,10 +253,10 @@ Metadata file format

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **enabled** | `boolean` | optional (default: `true`) | Enable caching |
| **ttlSeconds** | `integer` | optional (default: `3600`) | Cache TTL in seconds |
| **ttl** | `never` | optional | [REMOVED] `cache.ttl` was removed from `MetadataManagerConfig` in @objectstack/spec 17 — its unit (seconds) lived only in the description, while the nested `cache.databaseLoader.ttl` spelled the same word in milliseconds, so one key name meant two magnitudes 1000× apart. Rename the key to `ttlSeconds`; the value (seconds) is unchanged. |
| **maxSize** | `integer` | optional | Max cache size in bytes |
| **enabled** | `never` | optional | [REMOVED] `cache.enabled` was removed from `MetadataManagerConfig` in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — nothing ever read it: the outer `cache` block was declared and documented but consumed by no runtime, so `enabled: false` switched nothing off. Delete the key. The cache that actually runs is the DatabaseLoader read-through LRU under `cache.databaseLoader`; its `enabled` is the switch that is honoured. |
| **ttlSeconds** | `never` | optional | [REMOVED] `cache.ttlSeconds` was removed from `MetadataManagerConfig` in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — nothing ever read it: the outer `cache` block was declared and documented but consumed by no runtime, so the number expired nothing. Delete the key. The TTL that is honoured is `cache.databaseLoader.ttlMs` (milliseconds, default 60000) on the DatabaseLoader read-through cache. |
| **ttl** | `never` | optional | [REMOVED] `cache.ttl` was removed from `MetadataManagerConfig` in @objectstack/spec 17 — nothing ever read it: the outer `cache` block was declared and documented but consumed by no runtime, and its unit-suffixed respelling `ttlSeconds` was retired with it before it shipped (ADR-0049 enforce-or-remove). Delete the key. The TTL that is honoured is `cache.databaseLoader.ttlMs` (milliseconds, default 60000) on the DatabaseLoader read-through cache. |
| **maxSize** | `never` | optional | [REMOVED] `cache.maxSize` was removed from `MetadataManagerConfig` in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — nothing ever read it: the outer `cache` block was declared and documented but consumed by no runtime, so the byte cap capped nothing. Delete the key. The cap that is honoured is `cache.databaseLoader.maxSize` (an entry count, default 500) on the DatabaseLoader read-through cache. |
| **databaseLoader** | `{ enabled: boolean; maxSize: integer; ttlMs: integer }` | optional | DatabaseLoader read-through cache |

### Nested Shape: `MetadataManagerConfig.watchOptions`
Expand Down
6 changes: 4 additions & 2 deletions packages/metadata/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,13 +167,15 @@ generic LRU cache (see `src/utils/lru-cache.ts`). Writes invalidate the
affected entries, so reads always observe writes made through the same loader
instance; out-of-band SQL writes are honored within `ttlMs` milliseconds.

Configuration lives under `cache.databaseLoader`:
Configuration lives under `cache.databaseLoader` — the only live member of the
`cache` block (the outer `cache.enabled` / `cache.ttlSeconds` / `cache.maxSize`
were removed in #15624: nothing ever read them, and authoring one is now a
compile-time and parse-time error naming this nested half):

```typescript
new MetadataManager({
datasource: 'default',
cache: {
enabled: true,
databaseLoader: {
enabled: true,
maxSize: 500, // Max cached (type, name) entries
Expand Down
2 changes: 1 addition & 1 deletion packages/metadata/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
| Structural validation | Basic name/type/label validation |
| **DatabaseLoader read-through cache** | Generic `LRUCache` (lazy TTL, promote-on-get, write invalidation) wrapping `load`/`loadMany`/`list`/`stat`. Configured via `cache.databaseLoader`. |
| **Bootstrap modes** | `MetadataPluginConfig.bootstrap` = `eager` \| `lazy` \| `artifact-only` — supports edge / serverless / read-only deployments. |
| **Persistence write gates** | `MetadataManagerConfig.persistence.{ writable, overlayWritable }` — runtime freeze for sealed kernels. |
| **Persistence write gates** | `MetadataManagerConfig.persistence.writable` — runtime freeze for sealed kernels (`overlayWritable` was retired in #13135 with the paper metadata-customization protocol; authoring it is a compile-time and parse-time error). |
| **Single-source schema discipline** | Canonical `MetadataManagerConfigSchema` / `MetadataFallbackStrategySchema` live in `kernel/metadata-loader.zod.ts` and are re-exported from `system/metadata-persistence.zod.ts`. |
| **Remote artifact boot** | `MetadataPlugin` boots from a compiled artifact via `artifactSource: { mode: 'local-file', path }`, where `path` may be an `http(s)` URL — e.g. the control plane's public `/pub/v1/environments/:id/artifact[?commit=…]` route. Wired across `eager` / `lazy` / `artifact-only` bootstrap modes. Configurable timeout via `fetchTimeoutMs` or `OS_ARTIFACT_FETCH_TIMEOUT_MS` (default 60 s). A dedicated `artifact-api` mode (Bearer-authenticated control-plane pull) was removed in #4246 — zero consumers in any repo; the cloud runtime uses its own `ArtifactApiClient`. |

Expand Down
Loading
Loading