Skip to content

perf(objectql): index short name to FQN so registry lookups stop scanning the whole registry - #11102

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-10945-registry-shortname-index
Aug 22, 2026
Merged

perf(objectql): index short name to FQN so registry lookups stop scanning the whole registry#11102
os-zhuang merged 2 commits into
mainfrom
claude/issue-10945-registry-shortname-index

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #10945

SchemaRegistry.resolveObjectKey answered the short-name direction by walking every key of objectContributors and calling parseFQN on each. It is reached from seven call sites — getObject among them — so a kernel boot that registers N objects and resolves O(N) names did O(N²) string work.

The consequence was a silence, not a failure: boot got slower purely by an environment accumulating metadata, and once bootstrap outgrew the request waiter every request answered kernel_warming and the environment could never be opened, with no error anywhere. Refs objectstack-ai/cloud#1555.

The change

resolveObjectKey now reads a short-name to FQN index Map maintained beside objectContributors.

Both containers are mutated only through two private choke points — openObjectEntry / closeObjectEntry — so they cannot drift apart by construction rather than by discipline. After this change objectContributors.set / .delete appear only inside those two methods, plus the paired .clear() in reset(). A future caller cannot add a contributor list and forget the index half.

Resolution is deliberately unchanged. The index array holds the same members in the same order as the list the scan built, so:

  • an ambiguous short name still resolves to the first key registered under it,
  • the ambiguity warning still fires and still names every match, in registry order,
  • the legacy ns__name fallback and the explicit-full-key form still work.

Behaviour change check (dispatch Zone 2, item 2)

The pre-fix scan returned matches[0], i.e. first-registered-wins for an ambiguous bare name. An index could as easily have become last-writer-wins with nothing failing, so that is not asserted by hand — the pin re-implements the old loop as an oracle and asserts exact equivalence with it across all 24 permutations of a name set mixing a plain key, two legacy keys colliding on one short name, and a plain key colliding with a legacy one. Any future drift in which FQN wins reds there.

Net: no observable behaviour change, and the winner is now pinned rather than incidental.

Verification

Three-leg ablation, with the mutation confirmed on disk at every leg by marker counts (not by an editor's exit code):

leg objectKeysByShortName occurrences full-registry scan present new pin file
fixed (HEAD) 10 1 (getAllObjects, legitimate) 12 passed
ablated (registry.ts from origin/main) 0 2 (the scan is back) 7 failed / 5 passed
restored 10 1 12 passed

No rebuild is involved in that ablation and none is owed: the pin imports its subject as ./registry.js, a same-package relative specifier, which never resolves through package exports and therefore never reads dist/. Independent proof that vitest read the edited source: the structural assertions compare objectKeysByShortName, a field that exists only in this diff and in no built artifact, and they pass.

Measured on this container, resolving one name per registered object (32,000 lookups over 4,000 objects vs 4,000 over 500):

registry big window scaling ratio at 8x input
full-registry scan 4,888 ms 62.5x (8x input, 64x cost — quadratic)
short-name index 2.3 ms 5.7x

The scaling pin asserts a ceiling of 30x, placed between the two measured populations (~2.7x above the worst healthy sample, ~2x below the ablated one) rather than close to either.

⚠️ The card's own evidence rig lives in objectstack-ai/cloud (scripts/dev-local/bootstrap-curve.mjs) and was not re-run — it is not reachable from this container. The numbers above are the local scaling substitute, which triage named as an acceptable alternative.

Gates

pnpm --filter @objectstack/objectql test — 228 files / 4034 tests passed, at the final commit.
pnpm --filter @objectstack/objectql typecheck — exit 0.

18 gate families green, derived from the actual changed paths with node scripts/pm/dispatch-gates.mjs (which named five the dispatch list did not: check:query-options-erasure, check:type-check-coverage, check:type-check-debt, check:engine-double-contract, check:where-matcher).

check:type-check-debt is the one family not run in its CI form — its --re-measure needs the whole ledgered dependency closure built. It is probed directly instead, which answers the only question this diff can move: tsc over objectql with tests included reports 0 diagnostics naming the new test file and 0 naming registry.ts. The probe's total (355) is the ledger's 354 plus exactly one TS6059 rootDir diagnostic, which is an artifact of probing through the package's own rootDir: src config rather than the gate's generated project — the same one the ledger note records leaving when it went 355 to 354.

Repo-wide pnpm lint is CI's run. Locally it is narrowed to the two changed files and the narrowing is measured, not assumed: population taken from eslint's own config, file count read from --format json (2 files, 0 errors, 0 warnings), and the config declares no parserOptions.project — type-aware linting is off, so this diff cannot move the verdict on any file it does not touch.


Generated by Claude Code

os-project-manager and others added 2 commits August 22, 2026 16:32
…ing the whole registry

`SchemaRegistry.resolveObjectKey` answered the short-name direction by walking
every `objectContributors` key and calling `parseFQN` on each. Reached from
seven call sites including `getObject`, so a kernel boot that registers N
objects and resolves O(N) names did O(N^2) string work — the mechanism behind a
hosted environment whose bootstrap outgrew its request waiter and became
permanently unservable with no error anywhere.

Maintain a short-name -> FQN index Map beside `objectContributors`, mutated
only through two private choke points (`openObjectEntry` / `closeObjectEntry`)
so the two containers cannot drift. Resolution is unchanged: the index array is
the scan's `matches` list with the same members in the same order, so an
ambiguous short name still resolves to the first key registered under it and
the ambiguity warning still names every match.

Refs #10945

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RfyXxZ2WPjcjhuXpiQQc3y
…ions, add changeset

Ablation on this container: healthy ratio 5.7-11.1, full-registry scan 62.5.
Threshold placed between them (30x) rather than close to either, and PASSES
raised so the small window clears timer noise.

Refs #10945

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

This PR changes 1 package(s): @objectstack/objectql, touching 8 documentable anchor(s).

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx (via SchemaRegistry (symbol))
  • content/docs/kernel/services-checklist.mdx (via SchemaRegistry (symbol))
  • content/docs/plugins/adding-a-metadata-type.mdx (via SchemaRegistry (symbol))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx (via SchemaRegistry (symbol))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • 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 ab47f6974a275586355f06abd83a0975b8637d12packageMentionDocs.

Which tree this was computed on

This run read content/docs from f27a54338fd892051b534161f22d8af611de75e3 — the merge of head 686d0c24832a50d57de885bf860ab5a82e99ea13 into base ab47f6974a275586355f06abd83a0975b8637d12, 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 f27a54338fd892051b534161f22d8af611de75e3 && git checkout f27a54338fd892051b534161f22d8af611de75e3
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ab47f6974a275586355f06abd83a0975b8637d12 686d0c24832a50d57de885bf860ab5a82e99ea13 && git checkout -B drift-repro ab47f6974a275586355f06abd83a0975b8637d12 && git merge --no-ff 686d0c24832a50d57de885bf860ab5a82e99ea13

node scripts/docs-audit/affected-docs.mjs --json ab47f6974a275586355f06abd83a0975b8637d12

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs ab47f6974a275586355f06abd83a0975b8637d12 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions

Copy link
Copy Markdown
Contributor

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

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

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

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

    ✗ Build failed in 5.67s
    

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

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

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

历史信号:

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

分诊清单:

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

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

Merged via the queue into main with commit d29e271 Aug 22, 2026
32 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-10945-registry-shortname-index branch August 22, 2026 17:24

Copy link
Copy Markdown
Contributor Author

Queue-failure disposition: ⛔ NOT re-queuing — known deterministic red, not this PR's

Reviewer of record / dispatching seat, session_01RfyXxZ2WPjcjhuXpiQQc3y. Recording the disposition on the PR rather than leaving it in chat, so the next reader does not re-queue on a hunch.

Signature (queue build 32586913630): job Console Pin Gate, step Build the Console SPA at the pinned objectui SHA, ✗ Build failed in 5.67s. No test ran — this is a bundler failure, so the triage bot's timeout-vs-assertion reading does not apply and a re-run cannot change the answer.

Triage checklist item 2 — unrelated to this PR. This diff is packages/objectql/src/registry.ts plus a test and a changeset. It cannot affect a Console SPA build against a pinned objectui SHA.

Known, and deterministic rather than flaky. #10857 established the mechanism: #10695 retired the @objectstack/spec/ui theme schemas, and objectui still imports five of them in packages/types/src/zod/theme.zod.tsat the pinned SHA and at objectui's own tip, so bumping the pin reproduces it identically. objectui must land the counterpart first. ⛔ Nothing in this repo can fix it, and re-queuing burns a full-queue rebuild for every PR behind this one.

Not a required check. #11099 carried the identical signature in its queue build (32586810427) at 17:20Z and merged anyway — now 82cb6e849 on main. That is the direct evidence that this red does not gate the queue, rather than an inference from the gate's name.

Current state, verified rather than assumed: this PR is still open, not draft, not ejected (no removal notice), mergeable_state: unknown — which is what GitHub reports while it recomputes after main advanced under it when #11099 merged. Expected; no action owed.

What would change this disposition: an ejection notice, or a different failure signature. Either returns this PR to the drive-to-green loop. A repeat of this exact signature does not.

⚠️ Standing note, not actioned here: an advisory gate sitting red on main means every queue build carries a failure that the triage bot attributes to whichever innocent PR is in the batch. #10857 was closed as a duplicate, so a canonical card exists; this seat has not re-verified which one, and is not filing another.


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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SchemaRegistry.resolveObjectKey scans the whole registry per lookup, making kernel boot quadratic in stored metadata

2 participants