Skip to content

fix(objectql): log a contributed kind by its declared id, and report that the kind bucket IS reachable via GET /metadata/:type - #11168

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-10729-kinds-terminal-bucket
Aug 22, 2026
Merged

fix(objectql): log a contributed kind by its declared id, and report that the kind bucket IS reachable via GET /metadata/:type#11168
os-zhuang merged 1 commit into
mainfrom
claude/issue-10729-kinds-terminal-bucket

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#10729

Two halves were dispatched. Half ② is implemented here. Half ① is an investigation whose answer is reported, not acted on — and it landed on the reachable branch of the fork, so no spec change is proposed and packages/spec is untouched.


Half ② — the log line (implemented)

ObjectQL.registerApp() emits one 'Registered Kind' debug line per entry in a manifest's contributes.kinds. It read two fields that do not exist:

// packages/objectql/src/engine.ts (before)this.logger.debug('Registered Kind',{kind: kind.name||kind.type,from: id});

contributes.kinds items are declared { id, globs, description? } (packages/spec/src/kernel/manifest.zod.ts:327-331) and SchemaRegistry.registerKind types its parameter { id: string, globs: string[] } (packages/objectql/src/registry.ts:3748). Neither shape declares name or type, so the expression evaluated undefined || undefined and every conforming manifest logged kind: undefined.

The line now reads kind.id.

Why id, specifically. I read both the declared shape and the registration site's actual input before choosing. registerKind files the descriptor with registerItem('kind', kind, 'id') — so id is simultaneously (a) the only identifying field the schema declares and (b) the exact key the item is stored under. That makes the log line and registry.listItems('kind') name the item the same way, so a reader of the log can look the item straight back up. globs and description are neither identifying nor keys; description is optional besides.

No ?? kind.name fallback for older manifests. Reading an undeclared alias in a consumer is precisely the tolerance Prime Directive #12 rejects, and it would fossilize the drifted shape as a second de-facto contract. Nothing in this repo authors the older shape (see the census below), so there is nothing to accommodate.

Pinned

New packages/objectql/src/engine-kind-registration-log.test.ts — real engine, real SchemaRegistry, no doubles (the assertion is about what the registration seam does with a manifest, so a mocked registry would be asserting on the mock). Three cases:

  1. a conforming manifest is logged by its declared id;
  2. the logged value equals the key registry.listItems('kind') files the descriptor under — the round trip, not the string twice;
  3. an off-spec manifest that does carry name/type is still logged by id, which pins the direction as well as the value.

A debug field that silently goes undefined is exactly the class of defect that survives forever because nothing asserts on it — that is why the two-token fix ships with a pin.

Reverse verification (fix committed first, then git restore --source=origin/main -- packages/objectql/src/engine.ts, pin left in place): all 3 cases go red, in the predicted direction —

AssertionError: expected { kind: undefined, …(1) } to deeply equal { kind: 'sys.bi.report', …(1) }
- "kind": "sys.bi.report",
+ "kind": undefined,

and case 3 reproduces the drift itself: + "kind": "Report (undeclared)". Restored with git checkout HEAD -- ...; git status --porcelain clean.


Half ① — the terminal-bucket question (investigated, NOT acted on)

Is the kind bucket reachable through the generic GET /metadata/:type passthrough?

Answer: YES — reachable. The bucket is not terminal.

Measured end to end against a real engine and the real ObjectStackProtocolImplementation, with the metadata store answering as an empty healthy store (a missing store makes getMetaItems answer 503 for every type alike, which would have told me nothing about routing):

PROBE-1 spelling boundary for "kind":
canonicalMetaUrlType("kind") = "kind" (no fold)
metaUrlSpellingRefusal("kind") = null (not refused)
PROBE-2 registry bucket directly (the dispatcher's registry fallback door):
registry.listItems("kind") = [{"id":"sys.bi.report","globs":["**/*.report.ts"]}]
PROBE-3 protocol.getMetaItems({ type: "kind" }) (the GET /metadata/:type door):
RESULT = {"type":"kind","items":[{"id":"sys.bi.report","globs":["**/*.report.ts"]}]}
PROBE-4 negative control — an unregistered type through the same door:
RESULT = {"type":"nonexistent_bucket","items":[]}
PROBE-5 positive control — a declared-type misspelling through the same door:
THREW code=INVALID_REQUEST status=400 "'objectes' is not a recognised spelling of metadata type 'object'"

PROBE-4 shows PROBE-3's non-empty result is the registry bucket rather than an echo of the request; PROBE-5 shows the boundary gate is live and simply does not refuse kind. That last part is by construction, not by luck: metaUrlSpellingRefusal refuses only a spelling whose singular is a type the platform itself declares, and kind is not declared (it is absent from DEFAULT_METADATA_TYPE_REGISTRY, hence from the generated meta-url-data.generated.ts). Its own docblock states the intent — "a plugin kind can therefore never be refused by it".

The route reaches both of those doors, and neither drops the payload:

  • packages/runtime/src/domains/meta.ts:745-749protocol.getMetaItems({ type: typeOrName, ... }), which at packages/metadata-protocol/src/protocol.ts:5939 does this.engine.registry.listItems(request.type, packageId). That is the same bucket registerKind writes into.
  • packages/runtime/src/domains/meta.ts:805 — the dispatcher's own fallback, qlService.registry.listItems?.(typeOrName, packageId), reached when no protocol service is resolvable.
  • maskObjectSchemaList returns its input unchanged for any type whose singular is not object (meta.ts:160), and slimDocList is a no-op for anything that is not doc (meta.ts:64).

So getAllKinds()' zero callers are explained. I re-derived that reading rather than taking it on faith: git grep getAllKinds over the tree returns exactly one hit, its own definition, while the positive control getAllObjects returns hits across cli, client, metadata-protocol and objectql — the query style reaches callers when callers exist. The typed accessor is unused because the generic route reads the same bucket through listItems('kind') and bypasses it, not because the value is unreadable. The card's framing ("registers into a terminal bucket") needs revising on that point.

The part that is genuinely inert is globs, not the bucket

Reported for triage, deliberately not acted on:

  • globs has zero consumers. Tree-wide, the only non-comment occurrences are the schema declaration, registerKind's parameter type and getAllKinds' return type. Nothing reads kind.globs.
  • The capability the schema advertises is implemented — from a different source. The schema says contributes.kinds "enables the system to parse and validate new file types … Example: Registering a BI plugin to handle *.report.ts". Glob-driven artifact discovery does exist, and it globs filePatterns off the metadata type registry (packages/metadata/src/plugin.ts:847), which contributes.kinds does not extend — stated outright in packages/spec/src/kernel/metadata-plugin.zod.ts:56-61.
  • No in-repo manifest authors contributes.kinds at all — the only two occurrences are test fixtures (packages/objectql/src/engine.test.ts:180, packages/spec/src/kernel/manifest.test.ts:180).

So the honest disposition is not "dead bucket": the storage and read paths are live and generic, while the documented behaviour is declared-but-unenforced. That is a materially different question from the one the card framed, and its answer is an ADR-0049 enforce-or-remove judgement over an authorable spec surface — which is the spec lane's, not this PR's. Filing it is left to the PM, which reserved this card's spec-lane routing; a search of open issues found no existing card for the globs gap.

What this PR deliberately does not do

contributes.kinds and getAllKinds are not retired, deprecated or removed here, under either branch of the fork. Deleting an unused method resembles the tidy-up arm of Prime Directive #10 and here it is not: the thing it feeds is an authorable spec surface, and removing one goes through ADR-0049 enforce-or-remove with a liveness ledger, never a lane PR. packages/spec/** is untouched — see the file list.


Verification

All of the below on the pushed tree, 061901a07 (git rev-parse --short HEAD from the run itself; this is also the final commit).

CheckResult
pnpm --filter @objectstack/objectql testTest Files 229 passed (229) · Tests 4049 passed (4049)
new pin, targeted + verboseTest Files 1 passed (1) · Tests 3 passed (3), all three named in the reporter output
pnpm --filter @objectstack/objectql typecheckexit 0 — tsc --noEmit && tsc --noEmit -p tsconfig.scripts.json echoed, so not a zero-match silent pass
pnpm lint (repo-wide eslint . --no-inline-config)exit 0 in 103s — the whole population, no narrowing claimed
pnpm check:type-check-debt--re-measure: OK — 33 ledger entries re-measured, 1895 raw tsc errors, none above its recorded number (run with the full workspace closure built, as it demands)
check:engine-double-contractexit 0 — this PR's new test declares no engine double (it drives the real engine)

The 14 path-matched gates were derived by node scripts/pm/dispatch-gates.mjs from the change set rather than recalled, then re-derived after the final commit; all 14 plus the 5 convention-triggered ones (new test file) exit 0: check:changeset-gate-self-tests, check:durability-log-level, check:objectui-changeset, check:slot-lookup, check:stack-collection-maps, check:test-source-alias, check:type-source-resolution, check-adr-0087-registration, check-changeset-no-major, check-ci-filter-parity, check-empty-changeset, check-engine-split-ratio, check-plugin-teardown-shape, check-affected-docs, check:engine-double-contract, check:where-matcher, check:query-options-erasure, check:type-check-coverage, check:nul-bytes.

Behaviour change is confined to the text of one debug-level line; nothing branches on it. Changeset: patch on @objectstack/objectql.


Generated by Claude Code

`registerApp()` logged `kind.name || kind.type` for every entry in a
manifest's `contributes.kinds`. Neither field is declared: the schema says
`{ id, globs, description? }` and `registerKind` types its parameter
`{ id: string, globs: string[] }`, so the expression evaluated
`undefined || undefined` and every conforming manifest logged
`kind: undefined`.
Log `kind.id` instead — the only identifying field the schema declares, and
the exact key `registerItem('kind', kind, 'id')` files the descriptor under,
so the log line and `registry.listItems('kind')` name the item the same way.
No `?? kind.name` fallback: reading an undeclared alias in a consumer is the
tolerance Prime Directive #12 rejects.
Pinned by `engine-kind-registration-log.test.ts` — a debug field that silently
goes `undefined` survives forever precisely because nothing asserts on it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RfyXxZ2WPjcjhuXpiQQc3y
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ✅

What this run could not see
  • the SDK route bridge reached 45 of 221 client-bound route-ledger rows — the other 176 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run: node scripts/docs-audit/affected-docs.mjs --bridge-coverage

Coarse fallback — 14 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 ec79b11510ea2e8522ef9cfabe0ebadee9323ab1packageMentionDocs.

Which tree this was computed on

This run read content/docs from da8b2a96b6108987ff1667274c3710521352f345 — the merge of head 061901a07716a6f9db2c88fe3b2442efc8fa8196 into base ec79b11510ea2e8522ef9cfabe0ebadee9323ab1, 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 da8b2a96b6108987ff1667274c3710521352f345 && git checkout da8b2a96b6108987ff1667274c3710521352f345
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ec79b11510ea2e8522ef9cfabe0ebadee9323ab1 061901a07716a6f9db2c88fe3b2442efc8fa8196 && git checkout -B drift-repro ec79b11510ea2e8522ef9cfabe0ebadee9323ab1 && git merge --no-ff 061901a07716a6f9db2c88fe3b2442efc8fa8196
node scripts/docs-audit/affected-docs.mjs --json ec79b11510ea2e8522ef9cfabe0ebadee9323ab1

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

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 32606009382 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Console Pin Gate — 失败步骤: Build the Console SPA at the pinned objectui SHA

    ✗ Build failed in 5.79s
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

跨 PR 相同签名(24h,按失败测试文件聚合):

  • ⚠️本次没有可用的聚合签名(日志里没有能解析出测试文件名的 FAIL 行)—— 这不是「没有同签名的其他 PR」,是这一轮没测到。跨 PR 聚合本次不可用,请手工比对其他 PR 的同类评论。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 107 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@os-zhuang
os-zhuang deleted the claude/issue-10729-kinds-terminal-bucket branch August 22, 2026 23:56
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@os-zhuang@claude