Uh oh!
There was an error while loading. Please reload this page.
fix(objectql): log a contributed kind by its declared id, and report that the kind bucket IS reachable via GET /metadata/:type - #11168
Conversation
`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📓 Docs Drift Check1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ✅ What this run could not see
Coarse fallback — 14 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # 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 |
Uh oh!
There was an error while loading. Please reload this page.
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 32606009382 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
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/specis untouched.Half ② — the log line (implemented)
ObjectQL.registerApp()emits one'Registered Kind'debug line per entry in a manifest'scontributes.kinds. It read two fields that do not exist:contributes.kindsitems are declared{ id, globs, description? }(packages/spec/src/kernel/manifest.zod.ts:327-331) andSchemaRegistry.registerKindtypes its parameter{ id: string, globs: string[] }(packages/objectql/src/registry.ts:3748). Neither shape declaresnameortype, so the expression evaluatedundefined || undefinedand every conforming manifest loggedkind: 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.registerKindfiles the descriptor withregisterItem('kind', kind, 'id')— soidis simultaneously (a) the only identifying field the schema declares and (b) the exact key the item is stored under. That makes the log line andregistry.listItems('kind')name the item the same way, so a reader of the log can look the item straight back up.globsanddescriptionare neither identifying nor keys;descriptionis optional besides.No
?? kind.namefallback 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, realSchemaRegistry, 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:id;registry.listItems('kind')files the descriptor under — the round trip, not the string twice;name/typeis still logged byid, which pins the direction as well as the value.A debug field that silently goes
undefinedis 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 —and case 3 reproduces the drift itself:
+ "kind": "Report (undeclared)". Restored withgit checkout HEAD -- ...;git status --porcelainclean.Half ① — the terminal-bucket question (investigated, NOT acted on)
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 makesgetMetaItemsanswer 503 for every type alike, which would have told me nothing about routing):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:metaUrlSpellingRefusalrefuses only a spelling whose singular is a type the platform itself declares, andkindis not declared (it is absent fromDEFAULT_METADATA_TYPE_REGISTRY, hence from the generatedmeta-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-749—protocol.getMetaItems({ type: typeOrName, ... }), which atpackages/metadata-protocol/src/protocol.ts:5939doesthis.engine.registry.listItems(request.type, packageId). That is the same bucketregisterKindwrites 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.maskObjectSchemaListreturns its input unchanged for any type whose singular is notobject(meta.ts:160), andslimDocListis a no-op for anything that is notdoc(meta.ts:64).So
getAllKinds()' zero callers are explained. I re-derived that reading rather than taking it on faith:git grep getAllKindsover the tree returns exactly one hit, its own definition, while the positive controlgetAllObjectsreturns hits acrosscli,client,metadata-protocolandobjectql— the query style reaches callers when callers exist. The typed accessor is unused because the generic route reads the same bucket throughlistItems('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 bucketReported for triage, deliberately not acted on:
globshas zero consumers. Tree-wide, the only non-comment occurrences are the schema declaration,registerKind's parameter type andgetAllKinds' return type. Nothing readskind.globs.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 globsfilePatternsoff the metadata type registry (packages/metadata/src/plugin.ts:847), whichcontributes.kindsdoes not extend — stated outright inpackages/spec/src/kernel/metadata-plugin.zod.ts:56-61.contributes.kindsat 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
globsgap.What this PR deliberately does not do
contributes.kindsandgetAllKindsare 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 HEADfrom the run itself; this is also the final commit).pnpm --filter @objectstack/objectql testTest Files 229 passed (229)·Tests 4049 passed (4049)Test Files 1 passed (1)·Tests 3 passed (3), all three named in the reporter outputpnpm --filter @objectstack/objectql typechecktsc --noEmit && tsc --noEmit -p tsconfig.scripts.jsonechoed, so not a zero-match silent passpnpm lint(repo-wideeslint . --no-inline-config)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-contractThe 14 path-matched gates were derived by
node scripts/pm/dispatch-gates.mjsfrom 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:patchon@objectstack/objectql.Generated by Claude Code