Skip to content

fix(metadata): the store declares its views where it builds them, and a not-held refusal names each holder once - #33174

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-sly4of
Nov 14, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-sly4of

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Tesla · project thread

Fixes #32714

Clause-②: no
Metadata-protocol: none

维护者速读

  1. 这是 metadata: the in-memory registry and the artifact door's MemoryLoader keep one member per (package, name), so a bundled type's second locale is lost on the metadata service's own reads (N9) #32517(按包分槽的注册表键)之后的收尾:两处行为修正,其余全部是给现有行为加钉子。
  2. 修正一:store 是不是“视图”现在由创建它的地方明确声明,不再靠“没记录写入顺序”去猜;没有写入记录的普通 store 按写入顺序回答,不会被误当成视图。
  3. 修正二:一个名字没有被调用方持有时,409 拒绝里每个持有者只列一次(同一个包的两个 locale、同一个包绑在两个对象上的动作,都只算一个候选)。
  4. 邮件模板加第二个 locale 宣布为 added、改已有 locale 为 changed,这由规范文本决定(locale 是 bundle 键的后半),代码本来就这样,这里只钉住。
  5. 包接管环境同名邮件模板时只搬它写的那一个成员,其余成员留在环境下,和 DatabaseLoader 的存储行为一致;兜底宿主的 MCP list_actions / run_action 与同一对象上的两个包也已钉住。
  6. 不放宽任何解析、不扩大 agent 可读写范围;同一对象上两个包的激活门答 404 而非 409,属 runtime: the action activation door drops the declaration's ambiguity refusal, so two packages' same-named global action answers 404 'has no declaration' instead of 409 naming the holders (ADR-0150) #32689 同一缺陷,留给它处理。

What changes

packages/metadata/src/package-slotted-store.ts

  • Views are declared where they are made (card item 3). latestMemberOf used to treat a store with no recorded write order as a view, and answer the slot's first member. A view now carries an explicit mark: newSlotView() builds one, and inheritStoreWriteOrder marks the view it fills. The two places that build views (slotStoreOf here, slotViewOf in metadata-manager.ts) use it. A store that is not a view, with no write recorded, now answers by its write order, the way lastWrittenKeyOf does, instead of being read as a view.
  • refuseSlotNotHeld names each holder once (nit). Its candidates are deduplicated by holder. One package's two locales, or one package's action bound to two objects, is one candidate, which matches how packageSlotsOf and refuseSeveralSlots count slots.

packages/metadata/src/metadata-manager.ts: slotViewOf builds its view with newSlotView().

No public export changes. Only PACKAGE_SLOTTED_STORE_TYPES leaves the module, and it is unchanged.

Pins (no behaviour change)

  • Item 1: a second locale is added. This is decided by governing text, not by choice:
    • email-template.zod.ts calls locale "the second half of the bundle key", so (name, locale) is the item's identity. The header of item-key-discriminators.ts in metadata-core says the same.
    • register() splits "first registration vs overwrite".
    • A new locale is therefore a first registration. Raw events are added, added, changed, changed; service events are registered, registered, updated, updated. The environment's own bundle behaves the same way, and an agent control is included.
    • email_template is not in the spec's closed MetadataEventType, so nothing is published to realtime.
  • Item 2: the bare non-canonical member stays the environment's. A package's first member of a bundled name the environment holds replaces only the bare member it writes. The bare fr-FR stays bare until that package writes fr-FR too.
    • Pinned at three layers: the store rules, the registry and MemoryLoader, and DatabaseLoader's rows ([[null,'fr-FR'],[A,null]]).
    • It is pinned rather than cleaned up, because cleaning it would move a member nobody wrote and would break parity with the stored rows.
  • Item 4: the fallback host (no protocol merged listing).
    • Two packages bound to one object, activation door: 404 RESOURCE_NOT_FOUND and no row (see Acceptance notes).
    • Same case, execution door: 409 RESOURCE_CONFLICT naming both packages, and nothing runs.
    • MCP list_actions: one row per package.
    • MCP bare run_action: the D3 refusal (409 RESOURCE_CONFLICT, both packages named), and nothing runs.
    • Each case has a one-package control that runs.
  • Item 5: the metadata: the artifact door's memory loader and the database loader keep one row per name, so a slotted type's removed or second holder's item is still served (ADR-0150 D4) #32246 pin in metadata-manager-match-endpoint.test.ts is retitled so it names the epic ruling it follows (record 6818307202 on metadata: the in-memory registry and the artifact door's MemoryLoader keep one member per (package, name), so a bundled type's second locale is lost on the metadata service's own reads (N9) #32517: an unscoped plural read answers every package's slot).
  • The package-slotted store now has its own test file, package-slotted-store.test.ts, which pins the rules over plain maps.

Verification

All runs were on HEAD d2fb8a8a (merge of origin/main at 58443466a), through scripts/pm/os-verify-lock.sh, on a shared box.

  • Tests
    • @objectstack/metadata, full suite: 77/77 files, 2314 tests passed.
    • The three pinned metadata files: 704/704 passed.
    • @objectstack/runtime action-activation-unpackaged.test.ts: 21/21 passed.
  • Typecheck: metadata and runtime both exit 0.
  • pnpm lint: exit 0.
  • Derived gates (dispatch-gates.mjs --commands, each run with </dev/null, exit codes on disk, reconciled with --ran): 65 derived, 63 run and passed, 0 not run, 2 NOT MEASURED. Both of those exited 3 because a prerequisite was missing:
    • node scripts/check-plugin-teardown-shape.mjs --self-test needs a full clone, and this checkout is shallow.
    • pnpm check:dual-build-cjs-loads needs every package's dist, and 41 packages have none here.
  • Ablations. Each negative pin was put back to the forbidden behaviour, run red, then restored to HEAD with ablation-replace.mjs:
# Forbidden behaviour put back Red
A1 latestMemberOf infers a view from a missing write order again 1 (non-view answers by write order)
A1b newSlotView no longer marks its view 1 (a view answers its first member)
A2 refuseSlotNotHeld lists one candidate per member 2 (locales; action on two objects)
A3 register() reports a new locale as changed 2 (package bundle; environment bundle)
A4 a package take-over replaces every bare member 4 (registry list + unregister; store rules ×2)
A5 pluralEntryKeys puts every package on one key (metadata dist rebuilt, preflight confirmed the marker present, then --absent after restore) 7 (runtime fallback host: narrowing ×3, one-object activation + execution, MCP list_actions, MCP run_action)

Acceptance notes

🤖 Generated with Claude Code

https://claude.ai/code/session_01L9qibvVVqBGyinb2xpNdvB


Generated by Claude Code

…ch holder once in the not-held refusal

Pin the loose ends of the per-member registry keys: the watcher event a
second locale announces, the members a package's take-over leaves to the
environment, and the fallback host's action doors (two packages on one
object; MCP list_actions and bare run_action).

Claude-Session: https://claude.ai/code/session_01L9qibvVVqBGyinb2xpNdvB
Co-authored-by: Claude <noreply@anthropic.com>
… to one object as measured

Claude-Session: https://claude.ai/code/session_01L9qibvVVqBGyinb2xpNdvB
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

8 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • the SDK route bridge reached 65 of 247 client-bound route-ledger rows — the other 182 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 182: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 59 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 123 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 16 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 5e250a65992192bdc9b3fb3be304a2fca245b2d1 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 61716eea4b67fe51ff7ab8b98a405e48e48c4fb2 — the merge of head d2fb8a8aeac4af3ddb9f918f0b70841f0f0ecd18 into base 5e250a65992192bdc9b3fb3be304a2fca245b2d1, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 61716eea4b67fe51ff7ab8b98a405e48e48c4fb2 && git checkout 61716eea4b67fe51ff7ab8b98a405e48e48c4fb2
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5e250a65992192bdc9b3fb3be304a2fca245b2d1 d2fb8a8aeac4af3ddb9f918f0b70841f0f0ecd18 && git checkout -B drift-repro 5e250a65992192bdc9b3fb3be304a2fca245b2d1 && git merge --no-ff d2fb8a8aeac4af3ddb9f918f0b70841f0f0ecd18

node scripts/docs-audit/affected-docs.mjs --json 5e250a65992192bdc9b3fb3be304a2fca245b2d1

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

epic:#32897#1 · os-tesla · session_01L9qibzoApSyVPuia3NCrBY

维护者速读 · PR #33174 @ d2fb8a8a · 2026-11-14T00:24Z

席位核对

  • 形态: draft → main。正文 Fixes #32714,合并后应当关卡;正文其余处没有挨着别的 open 卡号的闭合关键词。Clause-②: no、Metadata-protocol: none。changeset 32714-registry-action-door-loose-ends.md,@objectstack/metadata patch。没有改 content/docs/releases/。文件面与认领 6867310678 一致。
  • 前提实测: 卡上 5 项加两条 nit 逐条落实(报告 6868051643):第 3 项与 nit 1 是行为修正,第 1、2、4 项是 pin,第 5 项改了 pin 标题,调用方表格只在已合并的 PR fix(metadata)!: the metadata service's own reads keep every locale of a package's email template and every package's item of a by-package type #32687 正文里,记入 Acceptance notes,未改已合并 PR。消融 A1–A5 都转红后已恢复。
  • 门禁 check-runs @ d2fb8a8a: 共 55 个,37 success、18 skipped(路径过滤)、0 失败。本地 65 个派生门禁 63 个 exit 0,2 个 NOT MEASURED(浅克隆、缺 dist,exit 3),以 CI 的 Build Core 与 Lint & Repo Gates 为准(success)。pnpm lint 0。
  • 契约复审: 不适用:Clause-② no,没有 narrowing、schema 或受管路径。席位自读 diff:
    • latestMemberOf 对「没有写入记录、又不是视图」的 store 改按写入顺序回答。席位在 head 上 grep 了全部调用方:readStoreEntry 的三处调用读的是注册表(每次写都经 recordStoreWrite)或 slotStoreOf / slotViewOf 建的视图(现都经 newSlotView 标记);MemoryLoader 的 store 同样经 recordStoreWrite;listedInSlotOrder 读注册表。这些函数不在 @objectstack/metadata 的 exports 里。所以现有读法的回答不变,与 changeset 所述一致。
    • refuseSlotNotHeld 的两处调用都在 database-loader.ts(:1853、:1874,回滚路径,每槽一行),去重后其拒绝内容不变,与 changeset 所述一致。
    • 逐句核对 changeset:视图标记、单持有者、第二个 locale 宣布 added、包接管只接它写的成员、兜底宿主 409 / 无账本行 / list_actions 每包一行 / 裸 run_action 被拒,都有对应 pin。
  • 接受的偏差: 无。
  • 范围外去向: 兜底宿主激活门对同对象两个包答 404 而非 409:class a,承接卡 runtime: the action activation door drops the declaration's ambiguity refusal, so two packages' same-named global action answers 404 'has no declaration' instead of 409 naming the holders (ADR-0150) #32689(open)。第 5 项调用方表格:Acceptance notes(只在已合并 PR 正文中)。
  • 落地条款:
    • 维护者常设落地令「按 pm-dispatch 审过的pr你可以自主落地」(2026-10-30T14:20Z)。
    • 非受管面(check-governed-merges --pr 33174:0/7 命中);521 行 ≤ 3000。
    • K = 3(PRBASE 5e250a65 取自 Test Core 计划的 Affected-set diff base;origin/main 68972cd2),小于 15,不需要先 merge main。GitHub 显示 clean。
    • 没有新的语种包。

Generated by Claude Code

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/l tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

2 participants