Skip to content

docs(kernel): teach the dot-qualified view item name in metadata-service.mdx - #13133

Merged
os-trump merged 1 commit into
mainfrom
claude/issue-12384-view-item-name-grammar
Aug 29, 2026
Merged

docs(kernel): teach the dot-qualified view item name in metadata-service.mdx#13133
os-trump merged 1 commit into
mainfrom
claude/issue-12384-view-item-name-grammar

Conversation

@os-trump

Copy link
Copy Markdown
Collaborator

Fixes#12384

Branch 2 of the maintainer ruling (comment 5459698917, verbatim 「同意」), executed after the census it made a precondition.

The ruling

  1. Census first, with a positive control: enumerate the view item names actually present in-tree (seeds, examples, tests, any stored fixtures) against QUALIFIED_ITEM_NAME_PATTERN.
  2. All dot-qualified ⇒ docs fix: metadata-service.mdx's two teaching sites move to the true form (object.view_name style) — lands directly, no return to the inbox.
  3. Live flat-named rows exist ⇒ STOP: that is a published-contract conflict; it comes back as its own contract card under the batch-Convert to monorepo with scoped packages #8 platform stance (finding: "stored pre-fold rows are live data" is decided per-consumer — the REST lookup route keeps a legacy-spelling chain while objectui consumers just removed theirs #12920: the fold/schema is the contract, stored-row censuses gate every narrowing).

ViewItemNameSchema is untouched, as the dispatch requires on every branch.

Census — method

A window scan, then hand adjudication. A view item row is the shape { name, object, viewKind, config } (ViewItemSchema / ViewItemWireSchema, ADR-0017 §3.1), so the discriminator viewKind is the anchor: for every occurrence of viewKind in a tracked file, the plus/minus 8 lines around it were harvested for every name: string literal, plus the explicit authoring door (defineViewItem). CHANGELOGs, the strictness-ledger audit and release notes were excluded as narrative rather than stored rows. The net over-collects deliberately — every hit was then read and adjudicated.

Result: 90 distinct names, 37 dot-qualified, 51 flat, 2 neither.

Positive control: crm_lead.pipeline was FOUND by the same scan (20 sites) — a zero-hit scan would not have been a reading.

Census — adjudication

The 51 flat names are not counter-examples. Read individually they fall into four classes, none of them a flat-named view item:

  1. Not view names at all — object names (crm_lead, crm_case, crm_task, iojn_customer), field names (name, amount, count), app/package names (sales, crm, Repair), a dashboard (sales_overview), hooks and services (probe_hook, probe_item, whatever), a locale (zh_cn). Caught by the wide net, discarded on reading.
  2. Deliberately invalid negative fixturesbroken (config: null, whose own test comment says it "matches no ViewMetadataSchema member"), mystery (no viewKind, no container slot — the walker must not invent a view), garbage_view (the view 的 spec 校验闸门形同虚设:saveMetaItem({ item: { nope: 1 } }) 返回 success 并把 {"nope":1} 存成一个 active view #5599 write-path probe), x, qa_dead_view.
  3. Container rowssaveMetaItem({ type: 'view', name: 'crm_lead', item: leadContainer }). Flat by design: ADR-0017's dual-read registers the aggregated container under the bare object key for backward-compatible reads.
  4. Flattened runtime overlay rowscase_grid, cases, shared_grid, org_grid, overdue_grid, myapp_case_grid. Bodies of the form { name, type: 'grid', columns, object, viewKind } — a raw ListView config at the top level plus its binding.

Every name on a genuine ViewItem record is dot-qualified: crm_lead.all, crm_lead.pipeline, crm_lead.edit, crm_lead.mine, crm_lead.tasks, crm_lead.hot, crm_lead.intake, crm_lead.all_leads, crm_lead.default, crm_account.pipeline, crm_account.custom, crm_account.edit, crm_case.pipeline, crm_case.mine, crm_case.custom, crm_case.edit, account.default, account.form, account.hot, account.junk, showcase_task.default, showcase_task.open, showcase_task.all, showcase_task.in_progress, lead.contact, lead.contact_us, lead.list, expense.all, iojn_repair_ticket.all, adhoc.view, other_object.all_accounts, o.default, a.b, and the seed PLACEHOLDER_OBJECT.new_view.

One fixture is worth naming: packages/spec/src/conversions/view-spelling-walk.test.ts:429 carries { name: 'clean', object: 'crm_lead', viewKind: 'list', config: {...} } — record-shaped with a flat name. It is a copy-on-write identity probe for the conversion walker and never reaches the parse door, so it is not a live row; noted as fixture hygiene, not fixed here.

Why this is branch 2 and not branch 3

Branch 3 fires on a live flat-named row that constitutes a published-contract conflict. Measured against the built schema (packages/spec/dist, not read off source), the flat-named rows that do exist parse clean — so they are not a conflict and no narrowing is implied:

=== A. the two NAME grammars, on the docs' literal name ===
ViewItemNameSchema("account_list") [dot-REQUIRED] -> REJECT
invalid_format @ []: View item name must be a dotted snake_case qualified name, e.g. "crm_lead.pipeline".
ViewItemNameSchema("account.list") [positive control] -> ACCEPT
=== B. the four stored `view` body spellings, each with a FLAT name ===
RECORD flat name {name,object,viewKind,config} -> REJECT
invalid_format @ ["name"]: View item name must be a dotted snake_case qualified name, e.g. "crm_lead.pipeline".
RECORD dotted {name,object,viewKind,config} [positive control] -> ACCEPT
OVERLAY flat name {name,object,viewKind,...rawConfig} -> ACCEPT
OVERLAY dotted {name,object,viewKind,...rawConfig} -> ACCEPT
CONTAINER flat name {name,list} -> ACCEPT
CONTAINER bare object key {name,list} [ADR-0017 dual-read] -> ACCEPT

This is the answer to the card's title question. A stored view row's name is governed by the body's spelling, not by one grammar:

spellingname declared asflat name
standalone ViewItem recordViewItemNameSchema (dot REQUIRED)rejected, by name
flattened runtime overlayz.string().optional() — no grammaraccepted
defineView containernot in the container's declared shapeaccepted (bare object key)

So ViewItemNameSchema is not too tight, and the docs were not describing a legal-but-undocumented spelling: the page taught a name that the canonical authoring path never produces.

The change

Four sites, one file, all addressing the same logical row. Moving only the two the card named would have left the page overlaying a row it does not read back, so all four move together (same defect, same page, same pass).

 // Get a view definition
-const view = await metadataService.get('view', 'account_list');+const view = await metadataService.get('view', 'account.default');
baseType: 'view',
- baseName: 'account_list',+ baseName: 'account.default',
scope: 'platform',
baseType: 'view',
- baseName: 'account_list',+ baseName: 'account.default',
scope: 'user',
// Resolve effective view for a specific user
-const effectiveView = await metadataService.getEffective('view', 'account_list', {+const effectiveView = await metadataService.getEffective('view', 'account.default', {

The replacement name is derived, not invented. The page's object is account (the next line reads listViews('account')). expandViewContainer('account', { list, form }) emits account.default and account.formpackages/spec/src/ui/assembled-views.test.ts states it in its own fixture comment ("a default list and a default form → expands to account.default + account.form"), and account.default appears in-tree at assembled-views.test.ts:66/:145 and engine-nested-plugin-view-expansion.test.ts:255.

Left alone deliberately: listViews('account') (an object name, correct) and get('dashboard', 'sales_overview') (type dashboard, governed by METADATA_ITEM_NAME_PATTERN, where the qualifier is optional — flat is legal).

Other pages

None found. Sweeping content/docs/ for view-name teaching outside this page turns up one sibling site, content/docs/api/client-sdk.mdx:225, which already teaches the dot-qualified form (getItem('view', 'crm_lead.pipeline')). metadata-service.mdx was the only page carrying the defect.

Verification

Gate family derived from the real diff by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (no paths passed — the script takes its own change set from the merge base), which matched 25 families and printed "Once a changeset exists, 9 more families apply — write one unless this card is docs-only".

Union re-run after the final commit, at d6e72f149: 25 pass, 1 not-measured. Exit codes captured before any pipe. Verdict lines quoted from the gates themselves:

✅ check-doc-anchors: 279 internal #fragment link(s) across 410 source file(s) all resolve to a real heading
✅ import examples resolve against api-surface/ (62 accepted gap(s) in the baseline)
✅ 231 generated files in sync with packages/spec
✅ 260 prose examples type-check across 3 surface(s) — every marked block parsed, so tsc ran the SEMANTIC pass on all of them
✓ check:doc-formula-expressions: 22 record-scoped formula example(s) across 425 files / 1453 TS blocks judged clean by @objectstack/formula.
✅ 26 ObjectSchema.create example(s) in 230 marked block(s) across 237 prose file(s) in 2 root(s) carry an os validate-clean security posture
check-nul-bytes: OK (scanned 7287 text file(s) -- 7287 tracked, 0 untracked-not-ignored; skipped 7 binary; no raw ASCII control bytes).

Three of these (check:skill-examples, check:doc-formula-expressions, check:doc-security-posture) first returned PREREQUISITE NOT MET because @objectstack/client-react, @objectstack/formula and @objectstack/lint were unbuilt. Those gates say so themselves — "Nothing was measured: this gate exited before running a single check" — so they were NOT recorded as failures; the three packages were built and the gates re-run, and the readings above are from the real run.

The one non-pass is scripts/check-test-completeness.mjs, which printed a usage line demanding a TURBO_TEST_LOG positional argument. It is a CI-invoked gate needing a turbo test log argument; run bare it measures nothing. Recorded as NOT MEASURED, not as a failure.

No typecheck leg is owed — the diff is four prose lines in one .mdx file and touches no TypeScript program. No ablation is owed — there is no guard to delete and no runtime behaviour to mutate; the change is documentation prose.

No changeset. The changeset gate is path-blind (zero changesets plus no skip-changeset label is red), so this is a label decision, not an omission: apps/docs is "private": true and no package manifest lists content/docs, so this diff publishes nothing from any package. Precedent agrees — the five docs-only commits in the last 300 on main each landed with no changeset. The skip-changeset label is applied to this PR.


Generated by Claude Code

`metadata-service.mdx` taught `account_list` as a view name at four sites —
the `get('view', ...)` read, both `saveOverlay` `baseName`s, and the
`getEffective(...)` resolve. A view row written as a standalone ViewItem
record is governed by `ViewItemNameSchema`, which pins identity on
`QUALIFIED_ITEM_NAME_PATTERN` (dot REQUIRED), so a reader following the page
gets a schema rejection naming the very field the page told them to fill.
The replacement name is derived, not invented: `expandViewContainer('account',
{ list, form })` emits `account.default` + `account.form`, which is what the
in-tree fixtures carry. All four sites move together — they address one
logical row, so moving two would leave the page overlaying a row it does not
read back.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LpRNHxWZgSUgVnFT9mQQo4
@github-actions

Copy link
Copy Markdown
Contributor

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

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

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

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL test/serve-publishes-bound-port.e2e.test.ts > #13062 the non-zero half — nothing an ordinary boot publishes may move > follows the DEV AUTO-SHIFT onto the port it really ↳ 失败原因: @objectstack/cli:test: Error: ENOENT: no such file or directory, open '/tmp/os-bound-port-home-tryJWG/runtime.env_local.json'
    

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

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

历史信号:

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

分诊清单:

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

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

@github-merge-queue
github-merge-queueBot removed this pull request from the merge queue due to failed status checks Aug 29, 2026
@os-trumpClaude

Copy link
Copy Markdown
CollaboratorAuthor

Queue ejection triaged (domain:spec seat, session_01LpRNHxWZgSUgVnFT9mQQo4) — standing down on the failure, not the PR.


Generated by Claude Code

@os-trump
os-trump added this pull request to the merge queueAug 29, 2026
Merged via the queue into main with commit 39d5d8aAug 29, 2026
35 checks passed
@os-trump
os-trump deleted the claude/issue-12384-view-item-name-grammar branch August 29, 2026 08:24
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/xsskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@os-trump@claude