Repository navigation
docs: the metadata-service contract page documents the SINGULAR read's failure posture but is silent on the plural reads' (list / listNames) — degrade vs refuse #16090
Description
Activity
- addeddocumentationImprovements or additions to documentationImprovements or additions to documentation
on Sep 6, 2026 分诊 ·
domain:devx/documentation/priority:p3/pm:blocked分诊席位。⛔ 不认领、不派发、不写代码、不合并、不裁决 decision-box 卡。
读数取自
origin/main@a4816a7,2026-09-06T02:16Z。卡的三条读数,逐条复现
卡的断言 复现结果 页面存在 ✅ content/docs/kernel/contracts/metadata-service.mdxlistNames只出现两次(签名 + 一行示例)✅ 恰好 2 次: :36接口签名、:103const names = await metadataService.listNames('object');失败姿态散文只覆盖单数读 ✅ ### load / loadDiagnosed一节,开头即「Both read one item through the registered loaders」first-wins/collision/silently/duplicate/listed twice✅ 0(同页活控制: load命中 10 ⇒ grep 起火)⇒ 「沉默不是虚假」成立,E3「已发布必修」确实不咬。卡对自己的判断是准的。
车道:
domain:devx落点是
content/docs/**⇒ devx 车道(content/docs/**、apps/docs、packages/lint、sdui-parser、scripts/gates)。⛔ 不因为它描述的是 metadata 服务就归 engine——domain:*是修复落地的包,不是主题。状态改为
pm:blocked(卡自己要求,但打了pm:queue)卡正文写着「⛔ This card should not land before #16086 does」,而 PR #16086 实测 open / draft / not merged(
mergeable_state: clean,head2aa6373d1,base9c3fda5f)。pm:queue会让派发把它当可派;改pm:blocked。Blocked-by: PR #16086。
⚠️ 提醒历轮教训:hard serial 由合并解除,不由「已武装 / 已 arm」解除。#16086 是 governed draft +needs:contract-review,等的是人工合并。卡请求的去重,做了 —— 两条都不是重复,但第二条有个真约束
卡说「⇒ If triage finds either already carries this gap, ⛔ close this as the duplicate」。读了两张:
#15626 —— 不是重复,而且它根本不是 issue,是一个 PR。
feat(spec)!: duration-shaped number keys carry their unit in the key name。它之所以成为卡的「控制命中」,是因为它改了content/docs/protocol/kernel/**metadata-service.mdx**—— 与本卡的content/docs/**kernel/contracts**/metadata-service.mdx是两个不同路径的同名文件。⚠️ 这条值得记进方法:卡的去重控制在 basename 上撞了车。按文件名去重会把两个不同页面当成一个页面。下次探针请带全路径。#15385 —— 不是重复(不同主张),但与本卡在同一个文件上串行。
它讲的是MetadataManager.loadManyKeyed作为 public member 落地却没有IMetadataService声明,落点packages/spec/src/contracts/metadata-service.ts,已挂domain:spec/pm:queue/ p3。但它正文里有一句直接命中本卡:⚠️ content/docs/kernel/contracts/metadata-service.mdxis not stale today … If option 1 is taken, that page has to move with the declaration⇒ #15385 与 #16090 会写同一个文件。两者都执行时,
content/docs/kernel/contracts/metadata-service.mdx是 hard serial。先落的那个不必等,后落的必须 rebase 后重读该节再写。已在两张卡上互相标注。优先级:p3
页面无虚假陈述(上面已实测),只有沉默;且要补的内容里有一半(refusal 那一行)在 #16086 落地前还不是
main上的行为。⇒ p3。
Generated by Claude Code
UNBLOCKED →
pm:queue— PM seatdomain:devx @ objectstack, sessionsession_01Vbw3RPgdtqesx4azk9SbW8, unlock scan at 2026-09-06T05:58Z on re-fetchedorigin/main@159dbad.Blocker: PR #16086 MERGED at 2026-09-06T03:25:31Z (hard serial released by the merge, per the triage note). Verified by content:
content/docs/kernel/contracts/metadata-service.mdxonorigin/mainstill mentionslistNamesonly at:36(signature) and:103(one example line), and its only failure-posture prose still opens "Both read one item" (:115) — the plural reads' two-outcome posture this card describes is undocumented, exactly as filed, and the behaviour it would document is now onmain.State transition in this stroke:
pm:blocked→pm:queue(one label write, read back). No claim, no dispatch yet — the card re-enters this lane's selection order; its file surface was re-verified on the merged ref above, and the dispatch brief will carry that reading rather than the card's original one.
Generated by Claude Code
Claim: PM loop round 1
Session:session_01Vbw3RPgdtqesx4azk9SbW8
Branch:claude/issue-16090-metadata-service-plural-read-posture
Worktree:objectstack-issue-16090
Domain:domain:devx
File surface:content/docs/kernel/contracts/metadata-service.mdx(a passage beside### load / loadDiagnosedat:113describing the plural reads' two outcomes) (stop on breach; explain in the report)
Container & model:M,mode:subagent,model: opus— no path-derived mandate; default tier because the passage documents engine behaviour that must be read from code (the per-loader degradation is older than #14921 and undocumented anywhere)
Clause-②: no
Serial constraints cleared: no open PR touchescontent/docs/kernel/contracts/**(all open PRs' file lists read 08:0xZ). Unblocked at 06:02Z (5557319372): PR #16086 (#14921) MERGED 03:25Z; the page onorigin/main@0e16fc45still mentionslistNamesonly at:36and:103and its only failure prose opens "Both read one item" (:115).
Generated by Claude Code
os-dev-report
{ "issue": 16090, "status": "done", "branch": "claude/issue-16090-metadata-service-plural-read-posture", "pr": "https://github.com/objectstack-ai/objectstack/pull/16261", "premise_still_valid": true, "summary": "Added a `### list / listNames` subsection to content/docs/kernel/contracts/metadata-service.mdx, placed immediately after `### load / loadDiagnosed` so the singular and plural failure postures sit together. Addition only: the singular passage is byte-unchanged. The passage states the two plural outcomes from the code rather than from the card - degrade (a loader that cannot be read is reported once and skipped, the read resolves over the remaining loaders; `listDiagnosed` carries the degraded/errors verdict) and refuse (AmbiguousMetadataStemError propagates out of BOTH plural reads, ADR-0112 envelope, code AMBIGUOUS_METADATA_STEM, status 500, every colliding path named, sorted) - plus the collision's real conditions (flat files only, case-sensitive, registered extensions only, `.js` not in the default set, nested files exempt). Assignee was already set by the PM dispatch and was not touched; the newest Claim (5557874957) names this branch, so no second claim was posted.", "tests": "No test change: this is documentation of behaviour already on main (PR #16086, merged 2026-09-06T03:25Z), and the diff is one .mdx file. Verification is the gate family plus the code reads behind every sentence. GATES: 38 derived, 38 run, 0 NOT-MEASURED, 0 UNRUN, all exit 0 at HEAD 6dad5a168 - `node scripts/pm/dispatch-gates.mjs --changed --ran ran.txt --repo objectstack-ai/objectstack` printed its own verdict line: '✓ dispatch-gates --ran: 38 derived famil(ies) accounted for - 38 run, 0 NOT-MEASURED.' Exit codes captured by redirect-then-capture, never across a pipe. Own verdict lines quoted: 'check-doc-anchors: 313 internal #fragment link(s) across 408 source file(s) all resolve to a real heading'; 'check-docs-transcript-drift: 4 declared transcript value(s) across 403 page(s) under content/docs/ equal what the registry derives today, and no undeclared block quotes one.'; 'check-nul-bytes: OK (scanned 7960 text file(s) -- 7960 tracked, 0 untracked-not-ignored; skipped 7 binary; no raw ASCII control bytes).'; '✅ 228 generated files in sync with packages/spec'. PREREQUISITE: check:docs-transcript-drift first returned exit 3 'PREREQUISITE NOT MET ... @objectstack/lint is not built' - recorded as NOT MEASURED, not as a red, and re-run green after the build. BUILD: `pnpm --workspace-concurrency=2 --filter '@objectstack/spec...' --filter '@objectstack/lint...' --filter '@objectstack/client-react...' build` under scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=issue-16090; first attempt returned 'VERDICT queue-timeout (exit 99) - never acquired - waited 540s ... holder pid 20402 ... pnpm check:pm-dispatch-gates' (NOT MEASURED; the interval was spent running the 29 build-independent gates), the resumed slot then returned 'VERDICT command-exit 0 - held the lock 394s'. ESLINT: repo-wide `pnpm lint` is not owed and that is measured, not skipped - every eslint.config.mjs config object scopes files to {ts,tsx,mts,cts,js,jsx,mjs,cjs}, `grep -c mdx eslint.config.mjs` is 0, and `eslint --no-inline-config --format json` on the changed file returns 1 result / 0 errors / 1 warning 'File ignored because no matching configuration was supplied.' - the diff touches no linted file, so no untouched file's verdict can move. CONTROL CHARS: `grep -naP` over the changed file returns no hits (exit 1) beyond the gate. NO ABLATION: nothing was mutated or reverted - no new gate or assertion was authored, so there is no failure mode to demonstrate.", "gates": { "head_sha": "6dad5a168", "derivation": "node scripts/pm/dispatch-gates.mjs --changed --commands --repo objectstack-ai/objectstack (38 commands; identical to the dispatch's lead list)", "reconciliation": "node scripts/pm/dispatch-gates.mjs --changed --ran ran.txt --repo objectstack-ai/objectstack yields exit exit 0, '38 derived famil(ies) accounted for - 38 run, 0 NOT-MEASURED'", "all_exit_zero": true, "commands": [ { "command": "node scripts/check-ci-filter-parity.mjs", "exit": 0 }, { "command": "node scripts/check-closing-keyword-parity.mjs", "exit": 0 }, { "command": "node scripts/check-closing-keyword-parity.mjs --self-test", "exit": 0 }, { "command": "node scripts/check-comment-mask-corpus.mjs", "exit": 0 }, { "command": "node scripts/check-doc-frontmatter.mjs", "exit": 0 }, { "command": "node scripts/check-doc-frontmatter.mjs --self-test", "exit": 0 }, { "command": "node scripts/check-doc-route-spelling.mjs --advisory", "exit": 0 }, { "command": "node scripts/check-doc-route-spelling.mjs --self-test", "exit": 0 }, { "command": "node scripts/check-docs-section-name.mjs", "exit": 0 }, { "command": "node scripts/check-docs-section-name.mjs --self-test", "exit": 0 }, { "command": "node scripts/check-section-landing-index.mjs", "exit": 0 }, { "command": "node scripts/check-section-landing-index.mjs --self-test", "exit": 0 }, { "command": "pnpm check:corpus-claim-drift", "exit": 0 }, { "command": "pnpm check:cross-package-test-inputs", "exit": 0 }, { "command": "pnpm check:doc-anchors", "exit": 0 }, { "command": "pnpm check:doc-authoring", "exit": 0 }, { "command": "pnpm check:docs-audit-scope", "exit": 0 }, { "command": "pnpm check:docs-redirects", "exit": 0 }, { "command": "pnpm check:docs-single-h1", "exit": 0 }, { "command": "pnpm check:docs-transcript-drift", "exit": 0 }, { "command": "pnpm check:driver-memory-census", "exit": 0 }, { "command": "pnpm check:nul-bytes", "exit": 0 }, { "command": "pnpm check:published-readme-links", "exit": 0 }, { "command": "pnpm check:react-page-adapter-contract", "exit": 0 }, { "command": "pnpm check:refd-timer-probe", "exit": 0 }, { "command": "pnpm check:role-word", "exit": 0 }, { "command": "pnpm check:skill-identifier-liveness", "exit": 0 }, { "command": "pnpm check:vendor-version-stamps", "exit": 0 }, { "command": "pnpm check:watch-hint-literal", "exit": 0 }, { "command": "pnpm --filter @objectstack/lint run check:doc-formula-expressions", "exit": 0 }, { "command": "pnpm --filter @objectstack/lint run check:doc-security-posture", "exit": 0 }, { "command": "pnpm --filter @objectstack/spec run check:docs", "exit": 0 }, { "command": "pnpm --filter @objectstack/spec run check:empty-state", "exit": 0 }, { "command": "pnpm --filter @objectstack/spec run check:liveness", "exit": 0 }, { "command": "pnpm --filter @objectstack/spec run check:skill-examples", "exit": 0 }, { "command": "pnpm --filter @objectstack/spec run check:strictness-ledger", "exit": 0 }, { "command": "pnpm --filter @objectstack/spec run check:variant-docs", "exit": 0 }, { "command": "pnpm --filter @objectstack/spec run check:yaml-examples", "exit": 0 } ] }, "files_changed": [ "content/docs/kernel/contracts/metadata-service.mdx" ], "line_budget": "n/a", "mcp_calls": "2 - one search_issues (dedupe for the finding, after repo-scoped REST /search/issues answered 403) and one issue_write (filing #16255). Everything else went through repo-scoped REST: issue body, comments, PR create, label write + read-back, PR body read-back, and this report comment.", "deviations": [ "CHANNEL SWITCH (declared): repo-scoped REST reads work in this container (issue, comments, PR, labels all HTTP 200), but GET /search/issues answers HTTP 403 for both the probe query and its control. Per the dedupe rule the fallback was one targeted MCP search_issues; its control fired (returned #15385 and the page-adjacent docs cards), so the zero-hit reading for the finding is a real reading.", "FILE-SURFACE NOTE (declared): the dispatch located the work as 'a passage beside `### load / loadDiagnosed` (:113)'. One further line was added inside the same declared file but outside that passage - `listDiagnosed?` in the `## Interface Definition` fence - because the new prose names a member the fence omitted. One mechanical line copied from packages/spec/src/contracts/metadata-service.ts:410-412. The wider drift it exposed was NOT repaired; filed as #16255.", "A2 - WHAT WAS ACTUALLY MEASURED, beyond the card: the degrade half is a clean 'continue over the remaining loaders' for both plural reads, so the card's row holds. But the two reads are NOT symmetric in what they report: `list` records the loss and offers it through `listDiagnosed` ({items, degraded, errors}); `listNames` has NO diagnosed counterpart at all (`listNamesDiagnosed` occurs zero times in the repo) - a short name set is not distinguishable by its caller, and the lost loader reaches the server log and nowhere else. The passage documents that asymmetry; the card did not name it.", "A1 CONFIRMED verbatim: AMBIGUOUS_METADATA_STEM_CODE at ambiguous-metadata-stem.ts:52, AMBIGUOUS_METADATA_STEM_STATUS = 500 at :63, exported with the error class and the predicate from packages/metadata/src/index.ts:28-33. The pins gave the exact conditions the passage now states (flat files only, case-sensitive, only registered extensions - `.js` is NOT in the default set - and a nested file sharing a flat basename is not a collision).", "A3 CONFIRMED on the fresh tree: the page at :113-125 still opens 'Both read one item through the registered loaders' and still states the null collapse; the new section was placed after it, and it was not rewritten.", "NOT CLAIMED (Zone 1 ruling 3): the passage says nothing about what the SINGULAR read does on an ambiguous tree. No pin asserts it and `findFile` is a different code path (extension precedence, no call into resolvableNames), so it was left unwritten rather than reasoned into the docs.", "NOT MERGED with origin/main: the branch sits on its dispatch base 0e16fc45; origin/main has since moved to 77781151d. dispatch-gates reports that none of the commits this tree can see touched what the gate derivation depends on, and no open PR touches content/docs/kernel/contracts/**. The PR's own CI runs against the merge ref." ], "open_questions": [], "out_of_scope_findings": [ "filed as #16255: the `## Interface Definition` fence on metadata-service.mdx under-declares IMetadataService - getDiagnosed, loadMany, matchEndpoint and subscribe are declared in packages/spec/src/contracts/metadata-service.ts and absent from the page (38 members in the source vs 34 on the page after this PR adds listDiagnosed; drift is one-directional). Filed unassigned and unlabelled for triage; whether the fence is a contract mirror that a gate should hold equal, or an excerpt that should say so, is deliberately left to triage.", "not filed, recorded here: #15385 remains open and writes this same file if its option 1 lands - a hard serial across #15385, #16090 and #16255, already noted on the cards." ] }
Generated by Claude Code
ACCEPT — PR #16261 (head
6dad5a168, read 2026-09-06T08:29Z)Review checklist (read from GitHub and
origin/main, not from the report):- Shape: draft, targets
main, body first lineFixes #16090; merge should close the card. No other issue number next to a closing keyword (docs: the## Interface Definitionblock on metadata-service.mdx under-declaresIMetadataService— 4 members declared in the contract source are absent from the page's listing #16255, fix(metadata): refuse an ambiguous metadata stem with both paths named, instead of listing one name twice and serving it by extension precedence #16086,MetadataManager.loadManyKeyedlands as a public member with noIMetadataServicedeclaration — its two siblingsloadMany?andloadDiagnosed?are declared, and the call site narrows with a local structural type instead #15385 appear in non-closing form). - Scope: 1 file,
content/docs/kernel/contracts/metadata-service.mdx(+46/−0). NOT governed (predicate 0/1). Docs-only ⇒skip-changesetpresent. Nocontent/docs/releases/change. Addition only: the singular### load / loadDiagnosedpassage is byte-unchanged, and the new### list / listNamessection sits directly after it. - One line outside the passage, declared and accepted:
listDiagnosed?(type)added to the## Interface Definitionfence because the new prose names it — copied frompackages/spec/src/contracts/metadata-service.ts(member present onorigin/main77781151d). The wider fence drift the dev noticed was not repaired here and is filed as docs: the## Interface Definitionblock on metadata-service.mdx under-declaresIMetadataService— 4 members declared in the contract source are absent from the page's listing #16255 (unlabelled, triage's). - Diff vs rulings: both plural outcomes are stated from code — degrade (loader reported once and skipped;
listDiagnosedcarriesdegraded/errors) and refuse (AmbiguousMetadataStemErrorre-raised by both reads, ADR-0112 envelope). PM spot-check onorigin/main77781151d:AMBIGUOUS_METADATA_STEM_CODEatpackages/metadata/src/loaders/ambiguous-metadata-stem.ts:52,AMBIGUOUS_METADATA_STEM_STATUS = 500at:63— the doc's code and status are the shipped ones. The asymmetry the doc adds ("listNameshas no diagnosed counterpart") checked as a zero-hit with a control:listNamesDiagnosedmatches 0 files underpackages/, while the controllistDiagnosedmatches the spec contract. The singular read's behaviour on an ambiguous tree is left unwritten, as ruled. - Gates: 38/38 derived commands exit 0 at
6dad5a168, reconciled bydispatch-gates --ran(0 NOT-MEASURED); the one prerequisite miss (check:docs-transcript-drift, lint unbuilt) was recorded and re-run green after the build under the lock (OS_VERIFY_LOCK_SLOT=issue-16090, one exit-99 spent on lock-free gates, then held 394s). CI at head: 16 success / 10 skipped / 7 in progress. Flip waits forLint & Repo Gates,TypeScript Type Check,Test Core (1/6)andBuild Docson their own rows. - Process disclosures accepted: one MCP
search_issuesfor the dedupe with its control firing (REST/searchis 403 here); branch not merged withorigin/main(base0e16fc454; no open PR touchescontent/docs/kernel/contracts/**; CI runs the merge ref). - Serial note carried forward:
MetadataManager.loadManyKeyedlands as a public member with noIMetadataServicedeclaration — its two siblingsloadMany?andloadDiagnosed?are declared, and the call site narrows with a local structural type instead #15385 (open, other route) writes this same file if its option 1 lands — hard serial acrossMetadataManager.loadManyKeyedlands as a public member with noIMetadataServicedeclaration — its two siblingsloadMany?andloadDiagnosed?are declared, and the call site narrows with a local structural type instead #15385 / docs: the metadata-service contract page documents the SINGULAR read's failure posture but is silent on the plural reads' (list / listNames) — degrade vs refuse #16090 / docs: the## Interface Definitionblock on metadata-service.mdx under-declaresIMetadataService— 4 members declared in the contract source are absent from the page's listing #16255, already recorded on those cards.
Landing plan: flip point armed; on all-green → ready → auto-merge (squash); on MERGED → content probe on
origin/main(firing:### list / listNamesandno diagnosed counterpartin the page; negative control:### load / loadDiagnosedstill present exactly once), strippm:dispatched, clear assignee.
Generated by Claude Code
- Shape: draft, targets
LANDED — PM seat
domain:devx @ objectstack, sessionsession_01Vbw3RPgdtqesx4azk9SbW8, 2026-09-06T11:43Z (stroke delayed by the seat's usage-limit pause 09:2xZ–11:30Z; the merge itself was on time).PR #16261 (
Fixes #16090) MERGED 2026-09-06T09:39:54Z as31baf0944onmainvia the merge queue. Content probe on re-fetchedorigin/maindacb73f4f:### list / listNames= 1 andno diagnosed counterpart= 1 incontent/docs/kernel/contracts/metadata-service.mdx; negative control:### load / loadDiagnosedstill exactly 1. Card closed by the merge;pm:dispatchedstripped and assignee cleared in this stroke.
Generated by Claude Code
- added a commit that references this issue
on Sep 28, 2026
Filed by the PM dispatch loop as option B of open question 3 on #14921, which this seat ruled A (no docs edit in PR #16086) with B split out here. ⛔ Unassigned and ungraded —
domain:*, type and priority are triage's.The gap
content/docs/kernel/contracts/metadata-service.mdxdocumentsIMetadataService. Its only failure-mode prose (around lines 113-130) is explicitly scoped to the singular read:⇒ The page is silent on the failure posture of the PLURAL reads (
list/listNames). It documented neither the pre-existing per-loader degradation nor any totality claim.Why it is a card and not a fix on #16086
⭐ Silence is not falsehood. The #14921 round measured that nothing on the page is false — it mentions
listNameson exactly two lines (an interface signature and one line of a signature-illustration block), states no return contract beyond the signature, and a grep forfirst-wins/collision/silently/duplicate/listed twiceon that page returns zero. ⇒ E3 「已发布必修」 does not bite, and editing it inside a ruled engine fix would have been writing new documentation the ruling did not ask for.What the page would need to say, after #16086 lands
The plural reads then have two distinct outcomes, and today the page distinguishes neither:
AmbiguousMetadataStemError, ADR-0112 envelope, codeAMBIGUOUS_METADATA_STEM, every colliding path named (added by #14921)⇒ Placed beside the existing
load/loadDiagnosedpassage, so the singular and plural postures sit together.⛔ Not asserted
⛔ No wording is prescribed and no priority is claimed. ⛔ This card should not land before #16086 does — the refusal half would document behaviour not yet on
main.Dedupe, with its controls
Enumeration proven COMPLETE: 668 open issues fetched; the repo's own
open_issues_countreads 668.plural read failure posturelistNames failure contractmetadata-service.mdxlistNamesMetadataManager.loadManyKeyedlanding as a public member without anIMetadataServicedeclaration, which is the same page's neighbourhood but a different claim. ⇒ If triage finds either already carries this gap, ⛔ close this as the duplicate rather than assuming the zero-hit probes settled it.Refs
#14921 · PR #16086 · #15626 · #15385 (both adjacent, see the caveat above)
Generated by Claude Code