From 6dad5a1682afc0c1b7cec6fba287db3710a6975e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 6 Sep 2026 08:03:11 +0000 Subject: [PATCH] docs(kernel): document the plural reads' failure posture on the metadata-service contract page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `content/docs/kernel/contracts/metadata-service.mdx` documented the SINGULAR read's failure posture (`### load / loadDiagnosed`: `load` collapses "no loader has this item" and "every loader failed" into one `null`) and said nothing about the plural reads'. `list` / `listNames` have two distinct outcomes and the page distinguished neither: - degrade — a loader that cannot be read is reported once and skipped, the read resolves over the remaining loaders (`MetadataManager.readListUncached`, `listNames`); `listDiagnosed` carries the `degraded`/`errors` verdict and `listNames` has no diagnosed counterpart at all; - refuse — `AmbiguousMetadataStemError` propagates out of both plural reads (ADR-0112 envelope, `AMBIGUOUS_METADATA_STEM`, status 500, every colliding path named). Addition only: the singular passage is unchanged and the two postures now sit side by side. `listDiagnosed?` is added to the page's interface listing because the new prose names it. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Vbw3RPgdtqesx4azk9SbW8 --- .../kernel/contracts/metadata-service.mdx | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/content/docs/kernel/contracts/metadata-service.mdx b/content/docs/kernel/contracts/metadata-service.mdx index 73b241d33a..3363abd419 100644 --- a/content/docs/kernel/contracts/metadata-service.mdx +++ b/content/docs/kernel/contracts/metadata-service.mdx @@ -31,6 +31,7 @@ export interface IMetadataService { registerInMemory?(type: string, name: string, data: unknown): void; get(type: string, name: string): Promise; list(type: string): Promise; + listDiagnosed?(type: string): Promise<{ items: unknown[]; degraded: boolean; errors: string[] }>; unregister(type: string, name: string): Promise; exists(type: string, name: string): Promise; listNames(type: string): Promise; @@ -131,6 +132,51 @@ if (degraded) { } ``` +### list / listNames + +The plural reads' failure posture. Both read a **set** through the same +registered loaders, and — unlike the singular reads above, which collapse every +fault into one `null` — they answer two different kinds of fault differently. + +| condition | outcome | +|:---|:---| +| A loader cannot be read — a storage outage, an unreachable `sys_metadata`, any other throw | **Degrade** — that loader is reported once and skipped; the read resolves with what the reachable loaders hold | +| One metadata name is derived from more than one file — `twin.json` beside `twin.yaml` in one type directory | **Refuse** — `AmbiguousMetadataStemError` propagates out of both reads | + +**Degrade** is the older of the two postures and the one nothing announces to +the caller: `list` and `listNames` still resolve, the caller still gets an +array, nothing 500s, and the set is quietly short. `listDiagnosed` is what +tells a short set apart from a complete one — it returns the same items plus +`degraded` and `errors`, and `degraded` is true when at least one loader could +not be read while the set was assembled. It says the set is **known-partial**, +never that it is empty and never that it is wrong: a reason to withhold a claim +of *completeness*, never a reason to withhold the items. + +`listNames` has **no diagnosed counterpart**. A short name set is not +distinguishable by its caller at all — the lost loader is reported at `error` +in the server log and nowhere else. + +**Refuse** is an authoring error rather than an outage, so it is deliberately +not absorbed by the degrade seam above. The filesystem loader derives a +metadata name by stripping the extension from a flat file's basename, so two +files under one type directory sharing a stem produce one name that is listed +twice while only one of them is reachable under that name. Instead of picking a +winner by extension precedence, the loader throws, and both plural reads +re-raise it. The error carries the ADR-0112 envelope — code +`AMBIGUOUS_METADATA_STEM`, status `500` (the request is well formed and no +caller can fix it by sending something else; only deleting or renaming a file +does), plus the metadata `type`, the `stem`, and **every** colliding path, +sorted — never just the precedence winner. Catch it with +`isAmbiguousMetadataStemError` from `@objectstack/metadata` wherever you need +to tell it apart from an outage. + + +Only stems the loader would actually resolve collide: the comparison is +case-sensitive, it covers just the extensions whose serializers are registered +(`.js` is not in the default set), and a nested file sharing a flat file's +basename is not a collision. + + ### register / unregister `register` saves (creates or replaces) the full definition for a `(type, name)`.