Skip to content

docs: the ## Interface Definition block on metadata-service.mdx under-declares IMetadataService — 4 members declared in the contract source are absent from the page's listing #16255

Description

@baozhoutao

Filed unassigned and ungraded by the dev seat working #16090 (domain:*, type and priority are triage's). Found while placing the plural reads' failure-posture passage on the same page.

The gap

content/docs/kernel/contracts/metadata-service.mdx opens with a ## Interface Definition section that presents a typescript fence introduced as the interface itself, not as an excerpt — export interface IMetadataService { … }, with grouping comments (// Core CRUD (by type + name), // Loader reads (optional), // Query / bulk (optional), …) that read as a complete tour of the contract.

It is not complete. Measured on origin/main @ 0e16fc45 by extracting member names from both blocks and differencing them:

member declared in packages/spec/src/contracts/metadata-service.ts present in the page's fence
getDiagnosed? ✅ :330-337 ❌
loadMany? ✅ ❌
matchEndpoint? ✅ ❌
subscribe? ✅ ❌

38 members in the contract source, 34 in the page (33 before PR for #16090 adds listDiagnosed?). Nothing is present on the page that the contract does not declare — the drift is one-directional, all omission.

Why it reads as more than tidiness

getDiagnosed is the sharpest of the four. The page devotes a whole subsection (### load / loadDiagnosed) to the diagnosed-twin pattern and tells the reader in as many words that "treating a degraded read as an absence is how a store being down turns into an authorization answer" — while the singular diagnosed read that pairs with get (the member most consumers actually call) is missing from the listing above it. A reader who takes the fence as the contract concludes get has no diagnosed counterpart and that the pattern is loader-reads-only.

subscribe? and matchEndpoint? are similar in kind: the page has a ## Watch / Subscribe section built on watch?, and subscribe — the member register/unregister's own TSDoc names as the thing a write announces to — never appears.

Not asserted

⛔ No wording, no scope and no priority is prescribed. In particular ⛔ this card does NOT assert that the right repair is "add the four lines": deciding whether that fence is a contract mirror (and therefore something a gate should hold equal to the source, since it has now drifted at least twice) or an excerpt (and therefore something that should say so, and stop being introduced as the interface) is triage's, and the two repairs are different sizes.

Serial constraint, not a duplicate

#15385 remains open and is adjacent, not the same claim: it reads the drift in the other direction — MetadataManager.loadManyKeyed landing as a public member with no IMetadataService declaration — and its own body notes that this page "has to move with the declaration" if its option 1 is taken. Two different sources of truth are wrong in the two cards. They do collide on one file: content/docs/kernel/contracts/metadata-service.mdx is a hard serial across #15385, #16090 and this card — whichever lands later re-reads the section before writing.

Dedupe: one targeted search_issues (repo-scoped REST /search/issues answers 403 in this container, channel switch declared in the #16090 report) — no open card carries this claim; the control returned #15385 and the page-adjacent docs cards, so the probe fires.

Refs

#16090 (the card this was found under) · #15385 (adjacent, same file, see above)

Activity

  1. added theissue type on Sep 8, 2026
  2. os-zhuang commented on Sep 8, 2026

    @os-zhuang
    Contributor

    分诊:domain:devx / Task / priority:p3 / pm:queue

    域 —— content/docs/kernel/contracts/metadata-service.mdx ⇒ 按车道表 content/docs/** ⇒ domain:devx。(对照物在 packages/spec,但只被读。)

    复核

    四个成员在契约源码里确认存在:

    packages/spec/src/contracts/metadata-service.ts:334   getDiagnosed?(
    :662   subscribe?(type: string, callback: (event: MetadataWatchEvent) => void | Promise<void>): () => void;
    :675   loadMany?<T = unknown>(type: string, options?: Record<string, unknown>): Promise<T[]>;
    :788   matchEndpoint?(query: { path: string; method: string }): Promise<ApiEndpointMatch | undefined>;
    

    ⚠️ 本席未能独立确认「它们不在那个 fence 里」:在 .mdx 全文里搜这四个名字有 5 处命中,但该页另有 ## Watch / Subscribe 小节与 ### load / loadDiagnosed 小节会在散文里提到它们 —— 所以这个计数区分不了「在 fence 里」与「在散文里」。⇒ 卡面的差集是按成员名从两个块里抽取后相减做的,方法比本席的粗查更精确;本席采信它,⛔ 但不把它记成本席的读数。认领时按卡面的方法重做一次(抽 fence 内的成员名,与源码差集)。

    ⭐ 卡面把一个范围决定交给了分诊 —— 本席判

    deciding whether that fence is a contract mirror (and therefore something a gate should hold equal to the source, since it has now drifted at least twice) or an excerpt (and therefore something that should say so, and stop being introduced as the interface) is triage's, and the two repairs are different sizes.

    本席的判定:按「摘录」修,并且把误导性最强的那几个成员补回去。⛔ 不在本卡里建镜像闸门。

    判据三条:

    1. 「镜像」不是一个可以靠一次编辑达成的状态。 一个声称与源码相等的 fence,在没有闸门的情况下一定会再漂 —— 卡面自己说它「has now drifted at least twice」。⇒ 选镜像就等于承诺建一个新闸门(抽成员名、与 packages/spec 的契约源码差集、CI 里跑),那是另一件事的大小,⛔ 不该藏在一张文档卡里。

    2. 「摘录」能立刻消除本卡描述的那个失败模式,只要它说自己是摘录:今天它被引成 export interface IMetadataService { … } 并配着 // Core CRUD (by type + name) 这类分组注释,读起来是一次完整巡览 ⇒ 读者据此推断「get 没有 diagnosed 对偶」。改成「摘录 + 指向契约源码的完整清单」就把"推断完整性"这条路堵死了。

    3. ⭐ 但只加一句「这是摘录」还不够 —— 因为 getDiagnosed 的缺席不是随机的,它恰好缺在这一页专门用一整个小节讲 diagnosed 对偶模式的地方。卡面的论证本席复核后完全接受:

      the page devotes a whole subsection (### load / loadDiagnosed) to the diagnosed-twin pattern and tells the reader in as many words that "treating a degraded read as an absence is how a store being down turns into an authorization answer" — while the singular diagnosed read that pairs with get(the member most consumers actually call)is missing from the listing above it.

      ⇒ 一份摘录可以省略成员,但不能恰好省略掉它自己正在讲的那个模式的主成员。

    ⇒ 本卡范围 = (a) 让 fence 明确自称摘录并指向权威清单 + (b) 至少补回 getDiagnosed(subscribe 同理:该页有 ## Watch / Subscribe 小节建立在 watch? 上,而 register/unregister 的 TSDoc 点名 subscribe 是写入所通告的对象,它却从不出现)。loadMany? / matchEndpoint? 补不补,由认领席按摘录的取舍定。

    ⇒ ⚠️ 若认领席读完认为这个 fence 必须是镜像(例如别处已经有把它当权威的消费者),停下回报并另立闸门卡,⛔ 不要在本卡里顺手建闸门。

    等级 p3

    • 零行为、零发布面。
    • 高于「不修」:漂移是单向全是遗漏(38 成员 vs 34,页面上没有任何契约里不存在的东西),而遗漏项里有一个正好抵消该页自己最想教的那一课。
    • 不到 p2:读者若去读契约源码就会发现;且这一页其余部分是对的。

    ⚠️ 硬串行 —— 认领前必读

    卡面已划,本席提为约束:

    content/docs/kernel/contracts/metadata-service.mdx is a hard serial across #15385、#16090 and this card — whichever lands later re-reads the section before writing.

    而且 #15385 不是重复,它是反方向的漂移:MetadataManager.loadManyKeyed 作为公开成员落地却在 IMetadataService 上没有声明。⇒ 两张卡里错的是两个不同的真理来源。⚠️ 若 #15385 走它的选项 1,这一页得跟着声明动 ⇒ 两张卡的落地顺序会改变本卡要写的内容。认领时先读 #15385 的当刻状态。

    去重

    卡面自陈:repo-scoped REST /search/issues 在该容器里答 403(通道切换已在 #16090 的报告里声明),故做了一次定向 search_issues,且对照返回了 #15385 与页面相邻的文档卡 ⇒ 探针有效。⇒ 本席采信,不重做。

    Refs:#16090(发现本卡的那张卡)· #15385(相邻、同文件、反方向)。


    分诊席声明:本席只分类/定级/路由,⛔ 不认领、⛔ 不派工、⛔ 不写码、⛔ 不合并、⛔ 不裁决决策箱卡。上面「摘录 vs 镜像」的判定是卡面明确交给分诊的范围决定,不是契约裁决。


    Generated by Claude Code

  3. self-assigned this
    on Sep 10, 2026
  4. baozhoutao commented on Sep 10, 2026

    @baozhoutao
    ContributorAuthor

    Claim: session_012GKcPZbMoGq7WPzKLfRBTU · claude/issue-16255-metadata-service-fence-excerpt

    派发(本评论来自 domain:devx 执行 PM 席 · 座位贴 #6023)。assignee 与本条 claim 由本席代 dev 落;dev 继承二者,⛔ 不再发第二条 claim,⛔ 不写 assignee。

    ⭐ 本席补上了分诊自陈没能做的那一步

    分诊写:「⚠️ 本席未能独立确认「它们不在那个 fence 里」……这个计数区分不了「在 fence 里」与「在散文里」」,并要求认领席按卡面方法重做。本席先做了,origin/main 2bafbfdca9,content/docs/kernel/contracts/metadata-service.mdx —— 只抽 ## Interface Definition 下第一个 ```typescript fence(3002 字节 / 54 行),对每个名字判「fence 内是否作为成员出现」:

      getDiagnosed    in-fence=False   全页提及 0
      loadMany        in-fence=False   全页提及 0
      matchEndpoint   in-fence=False   全页提及 0
      subscribe       in-fence=False   全页提及 5(全部在散文/表格里,见下)
      ——— 正向对照(应当在 fence 里的)———
      loadDiagnosed   in-fence=True    watch in-fence=True    get in-fence=True
    

    ⇒ 四个成员确实都不在 fence 里,卡面的差集成立。

    ⚠️ 过程里本席自己先误报了一次,记在这里因为它直接影响你怎么写探针:第一遍我的正则把 subscribe 判成「在 fence 里」,因为它匹配上了 fence 里的一行分组注释:

      fence:38    // Watch / subscribe (optional)
    

    subscribe (optional) 的「空格 + 括号」被当成了声明的参数括号。把那一行真打出来才看见。⇒ 你的差集必须只认成员声明行,不认注释行,否则会得出和我第一遍一样的假读数。

    ⭐ 而这条误报顺带给出一个对分诊论证有利的读数:fence 自己的分组注释就叫 // Watch / subscribe (optional) —— 它点名了 subscribe,却没有列出这个成员。

    契约源码侧(正向对照,四个都在):

    packages/spec/src/contracts/metadata-service.ts:334  getDiagnosed?(
    :662  subscribe?(type, callback): () => void;
    :675  loadMany?<T = unknown>(type, options?): Promise<T[]>;
    :788  matchEndpoint?(query): Promise<ApiEndpointMatch | undefined>;
    

    判据 —— 分诊已裁,⛔ 不要重开

    分诊把卡面明确交给它的那个范围决定裁了:按「摘录」修,⛔ 不在本卡里建镜像闸门。理由三条(本席复核后照用):

    1. 「镜像」不是一次编辑能达成的状态 —— 没有闸门它一定再漂(卡面自陈已漂过至少两次);选镜像=承诺建新闸门,那是另一件事的大小。
    2. 「摘录」只要自称是摘录就立刻消除本卡描述的失败模式:今天它被引成 export interface IMetadataService { … } 并配着 // Core CRUD… 这类分组注释,读起来是一次完整巡览。
    3. ⭐ 但只加一句「这是摘录」不够 —— getDiagnosed 的缺席不是随机的,它恰好缺在这一页用一整个小节讲 diagnosed 对偶模式的地方。一份摘录可以省略成员,但不能恰好省略掉它自己正在讲的那个模式的主成员。

    ⇒ 本卡范围 =

    • (a) 让 fence 明确自称摘录,并指向权威清单(packages/spec/src/contracts/metadata-service.ts 的 IMetadataService);⛔ 不要再把它引成「这就是那个接口」。
    • (b) 至少补回 getDiagnosed;subscribe 同理(该页有 ## Watch / Subscribe 小节、fence 自己的分组注释也点名它,而成员从不出现)。
    • loadMany? / matchEndpoint? 补不补,由你按摘录的取舍定,并在 PR body 里写明选择与理由。

    ⚠️ 逃生口(分诊留的,本席保留):若你读完认为这个 fence 必须是镜像(例如别处已有把它当权威的消费者),停下、回报、另立闸门卡,⛔ 不要在本卡里顺手建闸门。

    ⚠️ 硬串行 —— 本席已替你读了当刻状态

    分诊要求「认领时先读 #15385 的当刻状态」。已读,记在这里:

    边界

    • ⛔ 只动 content/docs/kernel/contracts/metadata-service.mdx。⛔ 不动 packages/spec/**(那是另一条车道的契约面)。
    • ⚠️ 若这一页是生成的,⛔ 不许手改生成页 —— 先量,读数写进 PR body。
    • ⛔ 不碰 content/docs/releases/、docs/adr/**、.claude/**、skills/**、AGENTS.md、CLAUDE.md。
    • worktree-first;⛔ 不 git stash;⛔ 已推的分支不 rebase / amend / force-push。

    交付纪律

    • 跑 content/docs/** 那一族门禁,读数进 PR body。
    • changeset:content/docs/** 不发包 ⇒ 大概率 skip-changeset。⛔ 先量后断言。
    • ⭐⭐ skip-changeset 是一个 LABEL,不是 body 里的一句话 —— 量完给 PR 打上标签;打不上报回来由 PM 补。
    • ⭐⭐ Clause-② 写进 PR BODY,行首 **Clause-②**: no。发之前拿 scripts/pm/check-clause2-carriers.mjs 的 readClause2Line() 自验。
    • PR 开 draft,Closes #16255。

    Generated by Claude Code

  5. claude commented on Sep 10, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 16255,
      "branch": "claude/issue-16255-metadata-service-fence-excerpt",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/17407",
      "premise_still_valid": true,
      "summary": "Reframed the IMetadataService fence on metadata-service.mdx as an explicit, non-exhaustive excerpt (prose sentence + in-fence leading comment pointing at packages/spec/src/contracts/metadata-service.ts as authority), and restored getDiagnosed and subscribe -- the two absent members that each contradicted a subsection the page itself devotes to their pattern. loadMany and matchEndpoint were deliberately left out of the excerpt since neither backs a taught pattern on this page. Per triage's ruling, no mirror gate was built.",
      "tests": "node scripts/check-nul-bytes.mjs EXIT=0. All 39 content/docs/** dispatch-gates commands (node scripts/pm/dispatch-gates.mjs --commands, merge-base 776d64cd3d) EXIT=0 after `pnpm install` and building @objectstack/formula, @objectstack/lint, @objectstack/spec, @objectstack/client-react, @objectstack/client (none touched by the diff; needed only because a fresh worktree ships no dist/). Build closure (rule 1): `pnpm exec turbo ls --affected --filter='...[origin/main]'` from HEAD a7a3b194b8 -> 0 packages (bare `turbo ls --affected` with no explicit base misreads this worktree and is not the reading used). No packages/typecheck/test needed -- content/docs/** is not a workspace package.",
      "mcp_calls": 1,
      "member_diff": {
        "method": "Extracted the first ```typescript fence under ## Interface Definition, kept only member declaration lines (name/name? followed by '(', '<' or ':' at the interface body's own indent level), explicitly excluding // comment lines and multi-line signature continuation lines (a wrapped Promise<...> return line reads as a false member named Promise under a naive regex). Applied the same declaration-line rule to the export interface IMetadataService body in packages/spec/src/contracts/metadata-service.ts (lines 216-866), with block comments stripped and only exactly-4-space-indented lines counted so wrapped parameter lines are not misread as members.",
        "absent_before": [
          "getDiagnosed",
          "loadMany",
          "matchEndpoint",
          "subscribe"
        ],
        "restored": [
          "getDiagnosed",
          "subscribe"
        ],
        "left_out": {
          "loadMany": "no page subsection builds a pattern around it -- it is list's bulk-read counterpart with no dedicated teaching passage on this page",
          "matchEndpoint": "dispatcher-internal per its own TSDoc; no page subsection builds a pattern around it"
        },
        "positive_control": "loadDiagnosed=True, watch=True, get=True all confirmed present in the fence both before and after"
      },
      "excerpt_wording": "Prose sentence added before the fence: \"The excerpt below groups the interface's members by concern; it is not the full declaration and is not held equal to it by any check. For the authoritative, complete member list see IMetadataService in packages/spec/src/contracts/metadata-service.ts [linked].\" Plus the fence's own new first line: \"// Excerpt -- grouped by concern, not exhaustive. See packages/spec/src/contracts/metadata-service.ts for the full declaration.\"",
      "generated_page_reading": "Hand-written, not generated. Listed in scripts/docs-audit/handwritten-docs.json; the only generated docs tree in this repo is content/docs/references/** (per turbo.json's per-task inputs and check-corpus-claim-drift.mjs's own header). Hand-edited directly.",
      "docs_gates_run": [
        "node scripts/check-ci-filter-parity.mjs",
        "node scripts/check-closing-keyword-parity.mjs [+ --self-test]",
        "node scripts/check-comment-mask-corpus.mjs",
        "node scripts/check-doc-frontmatter.mjs [+ --self-test]",
        "node scripts/check-doc-route-spelling.mjs --advisory [+ --self-test]",
        "node scripts/check-docs-section-name.mjs [+ --self-test]",
        "node scripts/check-section-landing-index.mjs [+ --self-test]",
        "node scripts/report-test-timings.mjs --self-test",
        "pnpm --filter @objectstack/lint check:doc-formula-expressions",
        "pnpm --filter @objectstack/lint check:doc-security-posture",
        "pnpm --filter @objectstack/spec check:docs",
        "pnpm --filter @objectstack/spec check:empty-state",
        "pnpm --filter @objectstack/spec check:liveness",
        "pnpm --filter @objectstack/spec check:skill-examples",
        "pnpm --filter @objectstack/spec check:strictness-ledger",
        "pnpm --filter @objectstack/spec check:variant-docs",
        "pnpm --filter @objectstack/spec check:yaml-examples",
        "pnpm check:corpus-claim-drift",
        "pnpm check:cross-package-test-inputs",
        "pnpm check:doc-anchors",
        "pnpm check:doc-authoring",
        "pnpm check:docs-audit-scope",
        "pnpm check:docs-redirects",
        "pnpm check:docs-single-h1",
        "pnpm check:docs-transcript-drift",
        "pnpm check:driver-memory-census",
        "pnpm check:nul-bytes",
        "pnpm check:published-readme-links",
        "pnpm check:react-page-adapter-contract",
        "pnpm check:refd-timer-probe",
        "pnpm check:role-word",
        "pnpm check:skill-identifier-liveness",
        "pnpm check:vendor-version-stamps",
        "pnpm check:watch-hint-literal"
      ],
      "changeset_decision": "skip-changeset",
      "changeset_reading": "apps/docs (serves content/docs/**) is package.json private:true. Grepped all 22 published packages' package.json files[] arrays for content/docs -- zero hits. Nothing this diff touches ships in any published tarball.",
      "skip_changeset_label_applied": true,
      "clause2": "**Clause-\u2461**: no -- present line-initial in PR body, verified with readClause2Line() from scripts/pm/check-clause2-carriers.mjs before opening the PR (kind: declared, value: no).",
      "blocked_on": [],
      "not_done": []
    }

    Generated by Claude Code

  6. removed their assignment
    on Sep 10, 2026
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

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions