Skip to content

Commit 7629f4d

Browse files
os-trumpclaude
andauthored
feat(spec)!: retire the inert outer keys of MetadataManagerConfig.cache — enabled, ttlSeconds, maxSize; cache.databaseLoader is the only live half (#15624, ADR-0049) (#16636)
* wip(spec): retire MetadataManagerConfig.cache.{enabled,ttlSeconds,maxSize} tombstones + kit (pre-build) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F8SRGcf2eKTK7RRpWCGxwf * wip(spec): regenerate reference docs for the retired MetadataManagerConfig.cache keys Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F8SRGcf2eKTK7RRpWCGxwf * docs(spec,metadata): the absorbed rename's migration sentence names the outer deletion; ROADMAP drops the retired overlayWritable gate Patch round on #15624 after the contract review (VERDICT: PASS): the duration-unit changeset's "Rename each key" sentence now states the one exception its own table already carried (the outer cache.ttl is deleted, not renamed), and packages/metadata/ROADMAP.md stops listing persistence.overlayWritable, retired in #13135, as a live write gate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F8SRGcf2eKTK7RRpWCGxwf --------- Co-authored-by: Claude Code <noreply@anthropic.com>
1 parent 68d5dfd commit 7629f4d

18 files changed

Lines changed: 475 additions & 67 deletions

.changeset/duration-unit-in-key-name.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"@objectstack/spec": minor
33
---
44

5-
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)
5+
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)
66

77
<!-- 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 -->
88

@@ -38,7 +38,7 @@ comment and published a bare `300` / `3600` to the reference page (#14519).
3838
| `HookSchema` (`hooks[]`) | `timeout` | `timeoutMs` | unchanged (ms) |
3939
| `JobSchema` (`jobs[]`) | `timeout` | `timeoutMs` | unchanged (ms) |
4040
| `DriverOptionsSchema` | `timeout` | `timeoutMs` | unchanged (ms) |
41-
| `MetadataManagerConfigSchema` | `cache.ttl` | `cache.ttlSeconds` | unchanged (s, default 3600) |
41+
| `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`)* | |
4242
| `MetadataManagerConfigSchema` | `cache.databaseLoader.ttl` | `cache.databaseLoader.ttlMs` | unchanged (ms, default 60000) |
4343
| `DatabaseLevelIsolationStrategySchema` | `connectionPool.idleTimeout` | `connectionPool.idleTimeoutSeconds` | unchanged (s, default 300) |
4444
| `TenantSecurityPolicySchema` | `accessControl.sessionTimeout` | `accessControl.sessionTimeoutSeconds` | unchanged (s, default 3600) |
@@ -52,10 +52,13 @@ new MetadataManager({ cache: { ttl: 3600, databaseLoader: { ttl: 60_000 } } });
5252
// after — rename the key; the number is unchanged
5353
defineHook({ name: 'audit_order', object: 'order', events: ['afterInsert'], handler: 'auditOrder', timeoutMs: 5000 });
5454
defineJob({ name: 'nightly_sweep', schedule: { type: 'cron', expression: '0 1 * * *' }, handler: 'sweep', timeoutMs: 300000 });
55-
new MetadataManager({ cache: { ttlSeconds: 3600, databaseLoader: { ttlMs: 60_000 } } });
55+
new MetadataManager({ cache: { databaseLoader: { ttlMs: 60_000 } } }); // the outer `ttl` is deleted, not renamed (#15624)
5656
```
5757

58-
**Migration.** Rename each key; no value changes. Authoring an old spelling
58+
**Migration.** Rename each key; no value changes — with one exception: the outer
59+
`MetadataManagerConfig.cache.ttl` is DELETED, not renamed (its respelling `ttlSeconds`
60+
was retired before it shipped, #15624; nothing ever read the outer `cache` block, and
61+
the nested `cache.databaseLoader.ttl → ttlMs` rename above is unchanged). Authoring an old spelling
5962
fails to compile (`tsc`: the input type is `never`) and fails to parse with a
6063
prescription naming the new key. For `hooks[]` / `jobs[]` the rename is a
6164
mechanical D2 conversion (`hook-timeout-to-timeout-ms`,
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/metadata": patch
4+
---
5+
6+
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)
7+
8+
<!-- adr-0087: registered metadata-manager-config-inert-cache-keys-retired -->
9+
10+
**BREAKING** accept-set narrowing, landing after the v17.0.0 cut (the lockstep
11+
launch-window convention ships it as `minor`; the migration prescription is
12+
registered under protocol major 18, where `os migrate meta` users will look).
13+
ADR-0049 enforce-or-remove decides it: a declared-but-unenforced key with zero
14+
measured readers comes off, and the published reference page stops teaching it.
15+
16+
`MetadataManagerConfig.cache` declared three outer knobs — `enabled` (default
17+
`true`), `ttlSeconds` (default 3600; spelled `ttl` until #14478) and `maxSize`
18+
("Max cache size in bytes") — beside the nested `databaseLoader` block, and
19+
**nothing read the outer three**. The only runtime consumer of the block is
20+
`MetadataManager` (`packages/metadata`), which hands `cache.databaseLoader` and
21+
nothing else to `new DatabaseLoader({ cache })`; a reader census over
22+
`packages/**` (tests and changelogs excluded) found no runtime reader of any
23+
outer key, while the same grep shape found the nested `cache?.databaseLoader`
24+
read twice — the control that makes the zero a measurement. An author writing
25+
`cache: { enabled: false }` or `cache: { ttlSeconds: 60 }` got a clean parse
26+
and a cache that behaved exactly as before, with no error and no warning, and
27+
the published reference page (`references/kernel/metadata-loader`) documented
28+
all three as if they configured something.
29+
30+
**What is refused:** authoring `cache.enabled`, `cache.ttlSeconds`, `cache.ttl`
31+
or `cache.maxSize` on `MetadataManagerConfig`, with any value — directly, through
32+
`MetadataManagerOptions`, or through `MetadataPluginConfig.storage`. The nested
33+
object is not `.strict()`, so each key is a `retiredKey()` tombstone rather than
34+
a bare deletion (a deletion would have stripped it in silence — the same no-op
35+
one layer down): authoring it is a `tsc` error (`never`) and a parse error
36+
carrying the prescription, which names the live nested knob.
37+
38+
**What stays, byte-identical:** the DatabaseLoader read-through cache under
39+
`cache.databaseLoader``enabled` (default `true`), `maxSize` (an entry count,
40+
default 500) and `ttlMs` (milliseconds, default 60000) — and every runtime
41+
path. Parsed configs no longer carry the two former defaults (`enabled: true`,
42+
`ttlSeconds: 3600`) that were materialized and never consulted.
43+
44+
**The #14478 rename is folded in.** `cache.ttl``cache.ttlSeconds` was
45+
registered under this same unreleased major and never reached a published
46+
release, so it is absorbed by the removal: `cache.ttl`'s tombstone now
47+
prescribes deletion (naming `cache.databaseLoader.ttlMs`) instead of a rename to
48+
a key that is itself retired — an author upgrading from a published 17.x sees
49+
one hop. The nested `cache.databaseLoader.ttl``ttlMs` half of that rename is
50+
unchanged.
51+
52+
## FROM → TO
53+
54+
```ts
55+
// before — parsed green; no runtime ever read the three outer numbers
56+
new MetadataManager({
57+
datasource: 'default',
58+
cache: { enabled: true, ttlSeconds: 3600, maxSize: 10_485_760, databaseLoader: { ttlMs: 60_000 } },
59+
});
60+
61+
// after — delete the outer keys; the nested block is the cache that runs
62+
new MetadataManager({
63+
datasource: 'default',
64+
cache: { databaseLoader: { enabled: true, maxSize: 500, ttlMs: 60_000 } },
65+
});
66+
```
67+
68+
**Migration.** Delete `cache.enabled`, `cache.ttlSeconds` / `cache.ttl` and
69+
`cache.maxSize`; nothing replaces them, because nothing ever consumed them. If
70+
you meant to switch the cache off, cap it or set its TTL, write
71+
`cache.databaseLoader.enabled` / `.maxSize` (entries) / `.ttlMs` (milliseconds)
72+
— those are honoured. No `os migrate meta` conversion runs on this surface: a
73+
`MetadataManager` config is not a stack collection member and never a stored
74+
row, so the chain has no seam for it; the D3 semantic entry
75+
`metadata-manager-config-inert-cache-keys-retired` carries the prescription
76+
into `spec-changes.json`, the upgrade guide and the `spec_changes` MCP tool.
77+
78+
The retirement kit: `retiredKey()` tombstones on all three (and the absorbed
79+
`ttl`), `RETIRED_KEYS_BY_MAJOR[18]` entries for each, the D3 semantic entry
80+
above (the #14478 entry's outer half is re-worded from a rename to a deletion),
81+
negative pins asserting each prescription and a positive pin asserting the
82+
parse output no longer materializes the retired defaults, the published
83+
reference pages regenerated, and the hand-written docs page and this package's
84+
README (`@objectstack/metadata` ships `README.md`, hence its `patch`) no longer
85+
authoring `cache.enabled`.

content/docs/protocol/kernel/metadata-service.mdx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,12 +254,21 @@ parse-time error carrying the prescription.)
254254
new MetadataManager({
255255
datasource: 'default',
256256
cache: {
257-
enabled: true,
258257
databaseLoader: { enabled: true, maxSize: 500, ttlMs: 60_000 },
259258
},
260259
});
261260
```
262261

262+
`cache.databaseLoader` is the only live member of the `cache` block. The outer
263+
`cache.enabled`, `cache.ttlSeconds` (formerly `cache.ttl`) and `cache.maxSize`
264+
were removed in #15624 (ADR-0049 enforce-or-remove): they were declared and
265+
documented but read by nothing — `MetadataManager` hands `cache.databaseLoader`
266+
and nothing else to the loader — so `cache: { enabled: false }` never switched
267+
anything off. Authoring any of them is now a compile-time and parse-time error
268+
carrying the prescription; the switch, TTL and cap that are honoured are
269+
`databaseLoader.enabled`, `databaseLoader.ttlMs` (milliseconds) and
270+
`databaseLoader.maxSize` (an entry count).
271+
263272
The cache exposes `LRUCache.stats()` (`size` / `hits` / `misses` / `hitRate`) for metrics.
264273

265274
### 4. Single-Source Schema Discipline

content/docs/references/kernel/metadata-loader.mdx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ const result = MetadataFallbackStrategySchema.parse(data);
4949
| **fallback** | `Enum<'filesystem' \| 'memory' \| 'none'>` | optional (default: `"none"`) | Fallback strategy when datasource is unavailable |
5050
| **rootDir** | `string` | optional | Root directory path |
5151
| **formats** | `Enum<'yaml' \| 'json' \| 'typescript' \| 'javascript'>[]` | optional (default: `["typescript","json","yaml"]`) | Enabled formats |
52-
| **cache** | `{ enabled: boolean; ttlSeconds: integer; maxSize?: integer; databaseLoader?: object }` | optional | Cache settings |
52+
| **cache** | `{ databaseLoader?: object }` | optional | Cache settings — only `databaseLoader` is read at runtime; the outer keys are retired |
5353
| **watch** | `boolean` | optional (default: `false`) | Enable file watching |
5454
| **watchOptions** | `{ ignored?: string[]; persistent: boolean; ignoreInitial: boolean }` | optional | File watcher options |
5555
| **validation** | `{ strict: boolean; throwOnError: boolean }` | optional | Validation settings |
@@ -60,10 +60,10 @@ const result = MetadataFallbackStrategySchema.parse(data);
6060

6161
| Property | Type | Required | Description |
6262
| :--- | :--- | :--- | :--- |
63-
| **enabled** | `boolean` | optional (default: `true`) | Enable caching |
64-
| **ttlSeconds** | `integer` | optional (default: `3600`) | Cache TTL in seconds |
65-
| **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. |
66-
| **maxSize** | `integer` | optional | Max cache size in bytes |
63+
| **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. |
64+
| **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. |
65+
| **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. |
66+
| **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. |
6767
| **databaseLoader** | `{ enabled: boolean; maxSize: integer; ttlMs: integer }` | optional | DatabaseLoader read-through cache |
6868

6969
### Nested Shape: `MetadataManagerConfig.watchOptions`

content/docs/references/kernel/metadata-plugin.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ const result = MetadataBulkResultSchema.parse(data);
118118
| **fallback** | `Enum<'filesystem' \| 'memory' \| 'none'>` | optional (default: `"none"`) | Fallback strategy when datasource is unavailable |
119119
| **rootDir** | `string` | optional | Root directory path |
120120
| **formats** | `Enum<'yaml' \| 'json' \| 'typescript' \| 'javascript'>[]` | optional (default: `["typescript","json","yaml"]`) | Enabled formats |
121-
| **cache** | `{ enabled: boolean; ttlSeconds: integer; maxSize?: integer; databaseLoader?: object }` | optional | Cache settings |
121+
| **cache** | `{ databaseLoader?: object }` | optional | Cache settings — only `databaseLoader` is read at runtime; the outer keys are retired |
122122
| **watch** | `boolean` | optional (default: `false`) | Enable file watching |
123123
| **watchOptions** | `{ ignored?: string[]; persistent: boolean; ignoreInitial: boolean }` | optional | File watcher options |
124124
| **validation** | `{ strict: boolean; throwOnError: boolean }` | optional | Validation settings |

content/docs/references/system/metadata-persistence.mdx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ Metadata file format
242242
| **fallback** | `Enum<'filesystem' \| 'memory' \| 'none'>` | optional (default: `"none"`) | Fallback strategy when datasource is unavailable |
243243
| **rootDir** | `string` | optional | Root directory path |
244244
| **formats** | `Enum<'yaml' \| 'json' \| 'typescript' \| 'javascript'>[]` | optional (default: `["typescript","json","yaml"]`) | Enabled formats |
245-
| **cache** | `{ enabled: boolean; ttlSeconds: integer; maxSize?: integer; databaseLoader?: object }` | optional | Cache settings |
245+
| **cache** | `{ databaseLoader?: object }` | optional | Cache settings — only `databaseLoader` is read at runtime; the outer keys are retired |
246246
| **watch** | `boolean` | optional (default: `false`) | Enable file watching |
247247
| **watchOptions** | `{ ignored?: string[]; persistent: boolean; ignoreInitial: boolean }` | optional | File watcher options |
248248
| **validation** | `{ strict: boolean; throwOnError: boolean }` | optional | Validation settings |
@@ -253,10 +253,10 @@ Metadata file format
253253

254254
| Property | Type | Required | Description |
255255
| :--- | :--- | :--- | :--- |
256-
| **enabled** | `boolean` | optional (default: `true`) | Enable caching |
257-
| **ttlSeconds** | `integer` | optional (default: `3600`) | Cache TTL in seconds |
258-
| **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. |
259-
| **maxSize** | `integer` | optional | Max cache size in bytes |
256+
| **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. |
257+
| **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. |
258+
| **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. |
259+
| **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. |
260260
| **databaseLoader** | `{ enabled: boolean; maxSize: integer; ttlMs: integer }` | optional | DatabaseLoader read-through cache |
261261

262262
### Nested Shape: `MetadataManagerConfig.watchOptions`

packages/metadata/README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,13 +167,15 @@ generic LRU cache (see `src/utils/lru-cache.ts`). Writes invalidate the
167167
affected entries, so reads always observe writes made through the same loader
168168
instance; out-of-band SQL writes are honored within `ttlMs` milliseconds.
169169

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

172175
```typescript
173176
new MetadataManager({
174177
datasource: 'default',
175178
cache: {
176-
enabled: true,
177179
databaseLoader: {
178180
enabled: true,
179181
maxSize: 500, // Max cached (type, name) entries

packages/metadata/ROADMAP.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
| Structural validation | Basic name/type/label validation |
3030
| **DatabaseLoader read-through cache** | Generic `LRUCache` (lazy TTL, promote-on-get, write invalidation) wrapping `load`/`loadMany`/`list`/`stat`. Configured via `cache.databaseLoader`. |
3131
| **Bootstrap modes** | `MetadataPluginConfig.bootstrap` = `eager` \| `lazy` \| `artifact-only` — supports edge / serverless / read-only deployments. |
32-
| **Persistence write gates** | `MetadataManagerConfig.persistence.{ writable, overlayWritable }` — runtime freeze for sealed kernels. |
32+
| **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). |
3333
| **Single-source schema discipline** | Canonical `MetadataManagerConfigSchema` / `MetadataFallbackStrategySchema` live in `kernel/metadata-loader.zod.ts` and are re-exported from `system/metadata-persistence.zod.ts`. |
3434
| **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`. |
3535

0 commit comments

Comments
 (0)