Skip to content

feat(spec,metadata,mcp): let a plural metadata read say it is known-partial (#6504) - #7721

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-6504-list-diagnosed
Aug 11, 2026
Merged

feat(spec,metadata,mcp): let a plural metadata read say it is known-partial (#6504)#7721
os-zhuang merged 1 commit into
mainfrom
claude/issue-6504-list-diagnosed

Conversation

@claude

@claude claude Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Part of #6504

IMetadataService.list(type) returns an array whether every loader answered or one of them was down. A consumer receiving a short list therefore had no way to ask whether it was short because that is all anyone declared, or because a loader was unreachable.

The verdict already existed and was already being discarded: MetadataManager.readListUncached() has computed a degraded flag since #5184, and list() spent it entirely on picking a cache TTL. reportLoaderReadFailure's own message says what that costs — "every list served from now on is a PARTIAL set presented as a complete one, and the server keeps reporting healthy" — but until now that sentence was addressed to a log reader only, because no caller had a way to ask.

Why this needed no new maintainer decision

The card asks whether adding listDiagnosed?(type) to IMetadataService is a maintainer call. It is not an open question, on two independent grounds:

  1. It was already ruled. The maintainer approved Option A on this very card on 2026-08-08 (comment 5225533472): "IMetadataService gains the optional listDiagnosed(type) read so plural reads can expose the degraded verdict the cache already computes", with execution notes requiring the PR fix(metadata,metadata-protocol,objectql): getDiagnosed —— get() 不再把 loader outage 答成「这一项没声明」 (#5840) #6051 per-consumer discipline.
  2. It would inherit a ruling even without that. MetadataManager.get() 丢弃 loadDiagnosed 的 degraded 判定:loader 读不到与「这一项没声明」在 6 个消费点上不可分辨 #5840 / PR fix(metadata,metadata-protocol,objectql): getDiagnosed —— get() 不再把 loader outage 答成「这一项没声明」 (#5840) #6051 closed the identical shape on MetadataManager.get() by adding getDiagnosed(type, name). The boundary check that would invalidate inheritance — whether the parent's reasoning was specific to the singular branch — does not hold: MetadataManager.get() 丢弃 loadDiagnosed 的 degraded 判定:loader 读不到与「这一项没声明」在 6 个消费点上不可分辨 #5840's reasoning is that a MISS and an OUTAGE reach consumers as the same value, and on the plural that reasoning is stronger, because list is the read that carries a count, and a count is the strongest positive claim a read can make.

What changed

packages/spec/src/contracts/metadata-service.ts — new optional listDiagnosed?(type) returning { items, degraded, errors }, following the getDiagnosed shape already in the file. Optional for the same reason its singular twin is: an implementation that predates it cannot report the distinction, so a consumer probes for it and falls back. list's TSDoc now names the ambiguity and points at it, as get's does.

packages/metadata/src/metadata-manager.ts — the producer. list() and listDiagnosed() are now one read seen at two widths: a private readList() owns the cache entry and the single-flight slot, list() narrows to .items, and listDiagnosed() returns the record. Consequences worth naming:

  • the in-flight promise now carries the whole record, so a listDiagnosed caller joining an in-flight read receives that read's verdict instead of having to start a second walk — the inflightListReads contract already promised sharers "the outcome", which was true of list() alone;
  • ListCacheEntry gains errors, so a consumer served from the memoized entry learns the same thing as one served by the read that filled it;
  • readListUncached() collects the loader messages it was already producing and handing only to a logger that speaks once per outage episode;
  • list() is unchanged in every direction — same items, same array instance, same best-effort posture — which is what keeps every existing caller working. The two members cannot drift because they are the same read.

packages/mcp/src/mcp-server-runtime.ts — the two measured consumers. Per PR #6051, each is qualified separately rather than by one rule, and they are genuinely different cases:

consumer class what it does now
objectstack://objects mis-describing healthy: { objects, totalCount }, byte-identical to before. Degraded: same objects, totalCount absent, plus partial: true, returnedCount, a warning sentence, and the code: 'SERVICE_UNAVAILABLE' / status: 503 envelope the sibling resource already carries.
agent_prompt skill bridge snapshot output unchanged. The verdict goes to the operator at warn.

The resource emitted a confident totalCount that is wrong during an outage — the one place here where a count is the strongest thing a read can wrongly say. Dropping the key rather than reporting a smaller number is the point: a client reading body.totalCount now gets undefined, which fails or renders as nothing, where a plausible-looking integer would have been believed. returnedCount says the one thing that is known, in a name that cannot be mistaken for a total. The objects themselves are still served — withholding them would be a new functional regression rather than the removal of a false statement.

The skill bridge is not that case. It publishes no count and makes no completeness claim to any client; what a degraded read costs it is skills that are silently not registered as prompts. There is nobody on the wire to tell — the reply it shapes is the SDK's own prompts/list, and inventing a placeholder entry would put a fabrication in a list whose entire purpose is to say what exists. So it reports to the operator at warn (a functional degradation, which is the level AGENTS.md prescribes), naming the loader, that the skills are missing rather than undeclared, and the consequence that makes it worth saying at all: the stdio transport takes the snapshot once at bridge time, so it stays short until restart even after the loader heals, while the HTTP transport rebuilds per request and self-heals.

Contract-first note. The resource keeps listObjects() as its resolver and asks listDiagnosed('object') only for the verdict. listObjects is its own contract member and declares no equivalence to list('object'), so presuming one at a consumer would be the private dialect Prime Directive #12 forbids. This is the plural instance of the fork #6055 recorded for getObject / diagnoseEmptyRead, taken the same way. On MetadataManager the two coincide (listObjects() is list('object')) and share one cache entry, so the probe is a cache hit rather than a second walk.

Scope — deliberately narrower than the card

Shipped: the declaration, the producer, and the two measured packages/mcp consumers. Not the wider sweep across metadata-protocol, rest, runtime and the plugins — #7674 is in flight on packages/metadata-protocol/src/protocol.ts, which carries two list/listObjects call sites, so that sweep would collide head-on. It becomes a follow-up card once #7674 lands.

Hence Part of #6504, not Fixes: merging this with Fixes would close a card whose remaining half nobody would see again.

The two mcp consumers are in scope rather than deferred with the rest for a specific reason — shipping listDiagnosed with nothing calling it would produce exactly the declared-but-unconsumed surface ADR-0049 exists to retire.

Tests

Two new files, 21 cases, all green.

packages/metadata/src/metadata-manager-list-diagnosed.test.ts drives a real loader failure: a DatabaseLoader over a driver whose find() throws ECONNRESET, so readListUncached()'s catch is what produces degraded and the verdict is computed rather than injected. The load-bearing case pins the count claim in both directions — two managers, one whose loader is down and one that genuinely holds nothing more, produce list() answers that are deep-equal and equal in length, while listDiagnosed() separates them. A healthy load yields the full count of 3 with degraded: false and no errors; the outage yields 1, degraded: true, and the loader's message.

packages/mcp/src/mcp-server-runtime.list-outage.test.ts pins what each consumer does with such a record, including that a degraded body has no totalCount key at all, that the healthy body is unchanged, and that a service without listDiagnosed behaves exactly as before.

Stated plainly rather than papered over: the mcp file uses doubles, not a live MetadataManager. packages/mcp depends on spec/core/types/formula and deliberately not on @objectstack/metadata, so adding that dependency to drive a test would be a larger architectural change than the fix. The real failure is driven where the loader lives, and the metadata file asserts the exact record shape the mcp doubles return — the same split #6055 used for getDiagnosed.

Reverse verification — direction predicted before running, and measured

Both predictions were written into the test headers before either was run, and both came out exactly as predicted.

revert predicted measured
delete listDiagnosed() from the producer 9 red / 1 green 9 red / 1 green (TypeError: manager.listDiagnosed is not a function)
remove the degraded branch + the skill-bridge warn, keeping the extraction 5 red / 6 green 5 red / 6 green

The predicted-green cases are invariant pins, not gaps, and each is green in both directions on purpose: list()'s answer and the healthy resource body are deliberately unchanged by this PR, and "a degraded listing still serves the objects it could read" is what would go red if a future change here started withholding data instead of withholding the claim.

The consumer revert is defined behaviourally, not textually. Reverting the whole file to origin/main would delete the extracted builder's export, fail the import, and turn all eleven cases red — a result that measures the extraction rather than the decision, and so proves nothing about either.

Gates run locally

  • @objectstack/metadata + @objectstack/mcp suites: 728 passed (603 + 125), 42 files.
  • Downstream consumer sweep, prefix direction (--filter '...@objectstack/spec' = downstream consumers): 61 packages green, zero failures.
  • pnpm --filter @objectstack/spec check:generated: all 13 generated artifacts up to date (build run first, no OS_SKIP_DTS).
  • pnpm check:type-check-debt: OK, none above its recorded number. Both new test files were then measured individually and contribute 0 errors each. The mcp file initially added 8 (TS2345, an untyped logger double) — under the recorded ceiling and therefore invisible to the gate, which is precisely the surplus [finding][devx] check:type-check-debt 的 ledger 余量会让新写的 pin 变哑:mongodb 曾有 33 条余量吞掉一次真实回退,另有 5 条目前带 4–19 余量 #6376 warns about, so they were fixed rather than spent. @objectstack/mcp TEST_DEBT measures 53 against a recorded 63; @objectstack/metadata DEBT measures 89 against a recorded 92. Neither ledger number was raised, and neither was lowered: those surpluses are pre-existing and not this PR's to claim.
  • eslint on all five changed files: clean. node scripts/check-nul-bytes.mjs: OK, plus a control-character self-scan over the changed files.
  • check-adr-0087-registration: no declared-breaking changeset, so no disposition marker is owed.

Changeset added (minor on spec/metadata/mcp), naming the one thing a consumer may need to act on: branch on partial before treating any count from objectstack://objects as a total.


Generated by Claude Code

…artial (#6504)

`IMetadataService.list(type)` returns an array whether every loader answered or
one was down, so a consumer receiving a short list could not ask which it was.
`MetadataManager.readListUncached()` has computed the `degraded` verdict since
#5184 and `list()` spent it entirely on a cache TTL.

This is the #5840 / PR #6051 shape on the plural read, and sharper there: `list`
is the read whose answer carries a count, and a count is the strongest positive
claim a read can make.

- spec: new optional `IMetadataService.listDiagnosed?(type)` returning
  `{ items, degraded, errors }` — the plural counterpart of `getDiagnosed`.
- metadata: `MetadataManager.listDiagnosed()`, sharing `list()`'s cache entry
  and single-flight slot, so the verdict costs no extra loader walk and the two
  members cannot drift. `list()` is unchanged in every direction.
- mcp: the two measured consumers, classified individually per PR #6051.
  `objectstack://objects` mis-described, so a degraded read now withholds
  `totalCount` while still serving the objects it could read; the skill bridge
  is a snapshot and reports its incompleteness to the operator instead.

Part of #6504 — the wider consumer sweep (metadata-protocol, rest, runtime,
plugins) is deliberately excluded while #7674 is in flight on
`packages/metadata-protocol/src/protocol.ts`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoUxMErFzTQVpQzjNgDAGm
@vercel

vercel Bot commented Aug 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 11, 2026 12:12pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/mcp, @objectstack/metadata, @objectstack/spec.

112 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/actions-as-tools.mdx (via @objectstack/mcp)
  • content/docs/ai/agents.mdx (via @objectstack/mcp, @objectstack/spec)
  • content/docs/ai/connect-mcp.mdx (via @objectstack/mcp)
  • content/docs/ai/index.mdx (via @objectstack/mcp)
  • content/docs/ai/natural-language-queries.mdx (via @objectstack/mcp)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/mcp, @objectstack/spec)
  • content/docs/automation/approvals.mdx (via @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/metadata, packages/spec)
  • content/docs/concepts/north-star.mdx (via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/environment-variables.mdx (via @objectstack/mcp)
  • content/docs/deployment/tenancy-modes.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via packages/metadata, @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/metadata, @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/spec)
  • content/docs/permissions/authorization.mdx (via @objectstack/mcp, @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/permissions/system-context.mdx (via packages/mcp, packages/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/mcp, @objectstack/metadata, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/metadata, @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/metadata-service.mdx (via @objectstack/metadata)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/mcp, @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/apps.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/field-grouping-and-order.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

7 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx (via @objectstack/mcp, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/metadata, @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/metadata, @objectstack/spec)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Aug 11, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 11, 2026 12:40
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 11, 2026
Merged via the queue into main with commit 2f8328c Aug 11, 2026
27 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-6504-list-diagnosed branch August 11, 2026 12:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants