Skip to content

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

@claude

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.mdx documents IMetadataService. Its only failure-mode prose (around lines 113-130) is explicitly scoped to the singular read:

Both read one item through the registered loaders … load returns null for both "no loader has this item" and "every loader failed".

⇒ 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 listNames on exactly two lines (an interface signature and one line of a signature-illustration block), states no return contract beyond the signature, and a grep for first-wins / collision / silently / duplicate / listed twice on 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.

⚠️ And the decisive reason it cannot be a rider: to be coherent, the new passage would have to describe behaviour older than #14921 — the pre-existing per-loader degradation — not just the refusal that card adds. A docs change whose scope exceeds its host PR's ruling belongs on its own card.

What the page would need to say, after #16086 lands

The plural reads then have two distinct outcomes, and today the page distinguishes neither:

condition outcome
a storage outage / a loader that cannot answer degrade — the read continues over the remaining loaders (pre-existing behaviour, undocumented)
two files under one directory deriving the same stem refuse — AmbiguousMetadataStemError, ADR-0112 envelope, code AMBIGUOUS_METADATA_STEM, every colliding path named (added by #14921)

⇒ Placed beside the existing load / loadDiagnosed passage, 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_count reads 668.

probe hits
plural read failure posture 0
listNames failure contract 0
CONTROL metadata-service.mdx 2 — #15626, #15385 — fires
CONTROL listNames 6 — fires

⚠️ Stated honestly: this seat did not read #15626 and #15385 in full. They surfaced as the control for the page name and are recorded here as adjacent, possibly overlapping — #15385 concerns MetadataManager.loadManyKeyed landing as a public member without an IMetadataService declaration, 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

Activity

  1. os-zhuang commented on Sep 6, 2026

    @os-zhuang
    Contributor

    分诊 · domain:devx / documentation / priority:p3 / pm:blocked

    分诊席位。⛔ 不认领、不派发、不写代码、不合并、不裁决 decision-box 卡。

    读数取自 origin/main @ a4816a7,2026-09-06T02:16Z。

    卡的三条读数,逐条复现

    卡的断言 复现结果
    页面存在 ✅ content/docs/kernel/contracts/metadata-service.mdx
    listNames 只出现两次(签名 + 一行示例) ✅ 恰好 2 次::36 接口签名、:103 const 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,head 2aa6373d1,base 9c3fda5f)。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.mdx is 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

  2. claude commented on Sep 6, 2026

    @claude
    ContributorAuthor

    UNBLOCKED → pm:queue — PM seat domain:devx @ objectstack, session session_01Vbw3RPgdtqesx4azk9SbW8, unlock scan at 2026-09-06T05:58Z on re-fetched origin/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.mdx on origin/main still mentions listNames only 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 on main.

    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

  3. claude commented on Sep 6, 2026

    @claude
    ContributorAuthor

    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 / loadDiagnosed at :113 describing 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 touches content/docs/kernel/contracts/** (all open PRs' file lists read 08:0xZ). Unblocked at 06:02Z (5557319372): PR #16086 (#14921) MERGED 03:25Z; the page on origin/main @ 0e16fc45 still mentions listNames only at :36 and :103 and its only failure prose opens "Both read one item" (:115).


    Generated by Claude Code

  4. claude commented on Sep 6, 2026

    @claude
    ContributorAuthor

    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

  5. claude commented on Sep 6, 2026

    @claude
    ContributorAuthor

    ACCEPT — PR #16261 (head 6dad5a168, read 2026-09-06T08:29Z)

    Review checklist (read from GitHub and origin/main, not from the report):

    Landing plan: flip point armed; on all-green → ready → auto-merge (squash); on MERGED → content probe on origin/main (firing: ### list / listNames and no diagnosed counterpart in the page; negative control: ### load / loadDiagnosed still present exactly once), strip pm:dispatched, clear assignee.


    Generated by Claude Code

  6. claude commented on Sep 6, 2026

    @claude
    ContributorAuthor

    LANDED — PM seat domain:devx @ objectstack, session session_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 as 31baf0944 on main via the merge queue. Content probe on re-fetched origin/main dacb73f4f: ### list / listNames = 1 and no diagnosed counterpart = 1 in content/docs/kernel/contracts/metadata-service.mdx; negative control: ### load / loadDiagnosed still exactly 1. Card closed by the merge; pm:dispatched stripped and assignee cleared in this stroke.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions