Repository navigation
Commit 408ca2e
feat(spec): give the 45 declared-but-unoffered scalar metadata keys a form row each (#19673)
Fixes #19331
Clause-②: no
Bucket B8 of the #19188 census: keys an **object-rooted** metadata
schema declares, graded **`live`** by the liveness ledger, whose schema
node is a **scalar**, and that **no** form in `METADATA_FORM_REGISTRY`
offered. Each gets exactly one form row. **45 rows landed; 4 keys are
deliberately still unoffered and named below.**
## The population was re-derived, and it is 49 — not 47
⛔ No number was inherited. The reconciliation gate's **own helper
block** was sliced verbatim —
`packages/spec/src/system/metadata-form-zod-reconciliation.test.ts`
lines 104-469, sha256 `25ef6c218394d1ea…` — into a throwaway probe
beside it, run through `vitest` against the live registry, and deleted
again. It is not in the diff.
**Controls asserted inside the probe, not beside it:**
| control | reading |
|---|---|
| LIT — `name` | offered by **17 of 17** forms |
| DARK — a fabricated key, form side | **0** of 17 forms offer it |
| DARK — the same key, schema side | **0** of 17 schemas declare it |
**The query, and what it read** — for each of the 17 registered types:
`offerableKeysAt(getMetadataTypeSchema(type), ROOT_PATH)` minus
`topLevelFields(METADATA_FORM_REGISTRY[type])`, then filtered on three
predicates in turn — root node is `object` (not a union), liveness
`status === 'live'` read from `packages/spec/liveness/TYPE.json`, and
the property node peeled of wrappers ONLY (never of `array` / `record`,
which would make a list read scalar) resolving to `string` / `number` /
`boolean` / `enum`.
| stage | reading |
|---|---|
| top-level zod-only, ADR-0010 overlay skipped | **142** |
| … object-rooted | **94** (`view` is union-rooted and contributes the
other **48**) |
| … graded `live` | **87** |
| … resolving to a scalar node | **49** |
| the same probe, after this change | **4** — exactly the four left out
|
**142 is the card's 274 with the overlay taken off** (274 − 132 = 142),
so the census's own arithmetic reproduces; **49 is not 47**, and that is
the finding the re-derivation was asked for. ⛔ No `view` key is in the
population, as the card required.
**Three of the five examples the card names are not in it**, which is
the same drift measured from the other side:
- `object.nameField` ✅ and `object.ownership` ✅ — both landed here.
- `field.precision` — **already offered** (`field.form.ts`), so it is
not zod-only today.
- `action.shortcutKey` — **no such key anywhere in the repository.** The
nearest thing is `action.shortcut`, removed with its form input by the
2026-07 #3896 audit close-out in `@objectstack/spec` 17.0.0 — the
tombstone at `action.zod.ts:1424-1438` cites no ADR. ADR-0049 is the
doctrine; #3896 is the act.
- `permission.license` — **not declared** by `PermissionSetSchema` and
not a row in `packages/spec/liveness/permission.json`.
## The four keys deliberately left out
Each would be a trap rather than an offer. All four are **recorded
reasons**, which is sibling card #19333's bucket — ⛔ nothing here writes
a ledger row for them.
| key | why no control |
|---|---|
| `object.displayNameField` | `[DEPRECATED → nameField]`. Its canonical
replacement lands here; offering the alias beside it teaches the retired
spelling. |
| `app._unpublished` | the schema's own text says `Never authored` — a
machine-managed publish gate written by AI materialization and cleared
by publish-drafts. |
| `field.system` | the auto-injected-column marker the platform stamps
(`applySystemFields`, the search companion). It is read on the **write**
path — the record validator skips required and multi-value checks for a
flagged column — so a control lets an author assert a false provenance
that silently disables validation for that field. |
| `field.format` | ONE `z.string()` key carrying THREE value
vocabularies. The engine reads it as an autonumber pattern
(`resolveAutonumberFormat`, `autonumber-format.ts:196-202`, called from
`engine.ts:5043-5051`); objectui reads it as a date display style,
`short` / `relative`; the published `describe` names a third, `Format
string (e.g. email, phone)`, that nothing measured honours. No help text
can be written for a key whose vocabulary is not yet ruled. |
## The judgement in each row
The control follows the scalar type; the copy states **what the runtime
enforces, including what ABSENCE resolves to** — the half an author
cannot read off an enum. `object.sharingModel` says a custom object that
omits it resolves to `private`; `field.step` says the write path does
not reject a value off the step grid; `action.undoable` says an action
with no `operation` has no write set to capture;
`action.requiresFeature` says the key is lowered into `visible` at parse
time and stripped, so no downstream consumer sees it.
**Nineteen rows carry a `visibleWhen` MEANINGFULNESS gate** — not a
parse gate. `FieldSchema` accepts every one of these keys whatever the
sibling value is, but only some field types, page kinds or action
operations ever read them, and offering a knob the runtime does not
deliver is what Prime Directive #10 forbids. Fifteen are mirrored from
the same key's row in the object designer's quick-add grid, so the two
authoring surfaces cannot disagree about when a knob applies.
**Three enums carry no inline `options` list, and that is a constraint
rather than a preference.** `FormSelectOptionSchema.value` is a system
identifier (`^[a-z][a-z0-9_.]*$`), so `object.managedBy`'s four
hyphenated members, `action.openIn`'s `new-tab` and `action.execution`'s
`perRecord` **cannot be spelled as option values at all** — `defineForm`
threw `invalid_format` at module load when they were. Those three derive
their enum from the served JSON Schema, which carries every member
verbatim, and the meanings ride the help text. See Acceptance notes.
## The i18n half — an EXPANSION of the declared file surface, stated
here
The claim declared `packages/spec/src/**/*.form.ts`. Six further files
changed, and the expansion is mechanically forced: `os i18n extract`
walks `METADATA_FORM_REGISTRY`, so a new row moves the generated
catalogs and `pnpm check:i18n` reds without them.
- **90 new leaves** (45 rows × label + helpText) in the four
`metadata-forms.generated.ts` bundles, regenerated with `pnpm
i18n:extract`.
- **All 90 are AUTHORED in `zh-CN`, `ja-JP` and `es-ES`, not left as
extractor fills.** Those three catalogs are ratcheted: a leaf that reads
its `en` source and is neither decided nor deferred is red on the day it
lands. Verified from the provenance side too — after re-extracting, **0
of the 90** keys remains in any locale's `source-hashes.generated.ts`,
which is the table that holds an entry exactly while a leaf is still a
byte copy of its source.
- Machine tokens are kept verbatim in every locale: enum members,
`params._selectedIds`, the `{0000}` and `{recordId}` placeholders,
`sys_permission_set.description`.
- **Three measured pins moved with the forms, each re-taken rather than
adjusted to fit:** the object form's collapsed-section population (45 →
55, `advanced` 36 → 46), its open-section population (94 → 96), and the
catalog-wide translated-label control (538 → 583, which is +45 in every
locale — exactly the rows that landed).
- One more in `packages/lint`: the shipped-form predicate census (53 →
72) and its `==`/`!=` literal half (41 → 52). **Measured, not inferred
from the delta** — the `*.form.ts` corpus was differenced against the
merge base `5ce3705052` by form/field/source: **19 entries added, 0
removed**, and the eight that do not reach the second census are seven
`in`-list gates plus one comparison against a bare `true`.
## Verification
Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` on a tree at `origin/main` (the stale-tree
warning was cleared by merging `8f30c188a` first), every command run
with its exit code captured **before any pipe**, then reconciled:
```
Run reconciliation — 85 derived, 85 run, 0 NOT-MEASURED, 0 UNRUN.
✓ dispatch-gates --ran: 85 derived famil(ies) accounted for — 85 run,
0 NOT-MEASURED (a DERIVED zero — all 85 recorded an exit code and none of them is 3).
```
Two of the 85 exited **3** on first pass — `check:dual-build-cjs-loads`
and `check:type-check-debt`, both `PREREQUISITE NOT MET` with no `dist`
— which measures nothing and is neither a pass nor a finding. Both were
re-run after the prerequisite build and both are green; the recorded
codes are those readings.
Suites, all at `3e453d8da`:
| package | reading |
|---|---|
| `@objectstack/spec` | 512 files / **14 953 passed**, 1 todo |
| `@objectstack/platform-objects` | 54 files / **883 passed** |
| `@objectstack/lint` | 108 files / **4 111 passed** |
| `@objectstack/objectql` (consumer — `getMetaTypes` serves this
registry) | 303 files / **5 050 passed** |
| `@objectstack/rest` (consumer — the `/meta/types` payload) | 194 files
/ **3 265 passed**, 1 skipped |
| `@objectstack/cli` unit tier (consumer — the i18n extractor walks this
registry) | 222 files / **3 141 passed** |
`turbo run typecheck` over spec + platform-objects + lint: 10/10 tasks
green. `pnpm --filter @objectstack/spec run check:generated`: all 15
generated artifacts up to date, working tree clean afterwards. Repo-wide
`pnpm lint` (`eslint . --no-inline-config`) run **whole, not narrowed**:
exit 0.
The reconciliation gate itself and
`metadata-form-declared-rows.pin.test.ts` are green throughout, and the
`field.relatedListFilter` / `object.validations` rows they pin are
untouched.
## What this deliberately does NOT land
⛔ **The gate that would notice a missing row.** The top-level `zodOnly`
direction stays unwired. Turning it on today would turn the remaining
absences into red lines with no offers behind them, which is the shape
the census round refused in as many words. This card lands offers; the
assertion is a separate card, and landing it without the rows is
precisely what was refused.
## Acceptance notes
Findings met on the way, ⛔ none fixed here, ⛔ none filed by this seat:
1. **`object.form.ts` offers two formula return types the schema
refuses.** The quick-add grid's `fields.returnType` row lists `text`,
`number`, `boolean`, `date`, `datetime`, `currency`;
`FieldSchema.returnType` is
`z.enum(['number','text','boolean','date'])`. Authoring `datetime` or
`currency` from the object designer writes a value the parse rejects.
The `field.form.ts` row added here lists the four declared members only.
`Seam: spec:FieldSchema.returnType → renderer:object.form.ts
fields.returnType`. Dedupe words: `returnType option datetime currency`
· `form option refused enum member` · `quick-add grid returnType drift`.
2. **The form face cannot express an enum whose members carry a hyphen
or a capital.** `FormSelectOptionSchema.value` is a system identifier
(`^[a-z][a-z0-9_.]*$`), so `system-data`, `engine-owned`, `append-only`,
`better-auth`, `new-tab` and `perRecord` are unspellable as option
values — measured, not inferred: `defineForm` threw `invalid_format` on
`options[2].value` at module load in this worktree. The workaround used
here (derive the enum, put the meanings in help text) works, but a form
author who does not know the bound writes an options list that fails at
import. `Seam: spec:FormSelectOptionSchema.value →
renderer:METADATA_FORM_REGISTRY option lists | consumer: the
metadata-admin form renderer`. Dedupe words: `FormSelectOptionSchema
value system identifier` · `option value hyphen refused` · `enum member
unspellable form option`.
3. **`field.format` is one key with three value vocabularies, and its
`describe` names the one with no consumer.** The ledger row is
`{"status":"live","evidence":"packages/objectql/src/engine.ts"}` and
that evidence **carries**: `engine.ts:5043-5051` resolves every `type:
'autonumber'` field through `resolveAutonumberFormat`
(`packages/spec/src/data/autonumber-format.ts:196-202`), which reads
`field.format` as the shorthand for the autonumber pattern when
`autonumberFormat` is absent. A second consumer reads the same key
differently: objectui's date/datetime cell honours `short` / `relative`
(`packages/core/src/utils/dataset-format.ts`, pinned by
`datetimeCell.formatVocabulary-8853.test.tsx` and
`DetailSection.dueLikeReachesTheCell-9729.test.tsx`). The declared
`Format string (e.g. email, phone)` is honoured by neither, so an author
who follows the describe on an autonumber field renders the literal
`email` as their pattern. This is why the key gets no row here. Dedupe
words: `field.format three vocabularies` · `autonumber format shorthand`
· `describe names a vocabulary no consumer honours`.
4. `page.form.ts`'s existing `kind` row says `full or slotted` while the
derived enum offers five members (`full`, `slotted`, `html`, `react`,
`jsx`) — an under-description of an existing row, noted only. No card:
an omitted member is not a reproducible defect.
5. `object.tenancy`, `app.contextSelectors` and `action.onSuccess` carry
a liveness row with **no `status` field** at all. None is a scalar, so
none is in this bucket; noted because the probe had to classify them and
a status-less row resolves to no verdict.
Items 1-3 are handed to the filing seat with their dedupe words; ⛔ this
seat opened no issue. `field.system`, `app._unpublished` and
`object.displayNameField` are ⛔ not new cards — they are inside sibling
#19333's scope (recorded reasons) and are named there.
## 维护者速读(草稿)
**改了什么。** 45 个元数据字段原本在 schema 里已声明、运行时也确实在读,却没有任何表单控件,作者只能去 Source 标签页手写
JSON。这次给每个字段补上一行表单控件,并把配套的四语文案一次写全。
**为什么改。** 手写 JSON 没有任何校验提示:写错一个相邻键名,要等运行时拒收才知道。补上控件等于把这批能力从"藏着"变成"能点",也让
AI 代写元数据时读到的是准确的字段说明,而不是自己猜。
**风险与代价(含回滚)。** 不动任何 schema 的接受集合,写进去的数据形状一个字节都没变,因此风险面只在"界面上多了几行"。有 4
个键故意没做控件:两个是平台自己盖章、作者不该碰的,一个是已废弃的旧别名,还有一个的官方说明和唯一能查证的消费方互相矛盾——与其猜一个说法,不如先不给入口。回滚就是回退本
PR,界面回到今天的样子,没有数据迁移。
**席位意见。** 本席(`domain:spec` 执行席位)已逐条复核,⛔ 不是按开发的报告核的,是按 diff 和
`origin/main` 核的:
- **45 行是数出来的**。十个 `*.form.ts` 里按 `field:` 键计数:1+1+2+19+6+1+4+9+1+1 =
45。
- **90 条文案也是数出来的**。每个语言包各 45 条 `label:` 与 45 条 `helpText:`,+180 行 =
45×4,四个语言包完全一致;`source-hashes.generated.ts` 按声明不在 diff 里。
- **文件面的扩张对得上**。18 个文件 = 10 个申报面内的 `*.form.ts` + 1 个 changeset + 7
个被机械强制的(四个语言包、两个 platform-objects 铉子、一个 lint 铉子)。
- **卡面举的五个例子里有三个不成立,本席逐个复测过**:`field.precision`
今天已经有控件(`field.form.ts:73`);`shortcutKey` 全仓 0 处;`permission.license`
既不在 `permission.zod.ts`,也不在 `liveness/permission.json`。
-
**文案口径抽核过**。`ownership`、`sharingModel`、`undoable`、`app.hidden`、`nameField`
五处,逐条对回声明它的 `.describe()`,一致;其中 `sharingModel` 的「缺省 =
private」带了「自定义对象」这个限定词,是对的 —— 系统对象缺省解为 public。
- **退回过一处**。`object-collapsed-sections-echo-decisions.test.ts` 的注释把 2
条文案错记在本卡名下(本卡实际带来 10 条,不是 12 条;那 2 条的缺口在本卡之前就已存在)。已退回修正,断言值一个没动。
- **一处由本席改写**。上面 `field.format` 一行与验收笔记第 3 条,原先写「台账里引的证据文件根本没读这个键」——
本席实测下这句是错的:引擎确实读它(当作 autonumber
的格式简写)。不给控件的结论不变,而且更站得住:同一个键今天背着三套取值词汇,而官方说明写的那一套没有任何消费方。changeset
里的同一句话正由开发改(它会发到 npm)。
对你要确认的两点,本席的建议是两点都可以收:① 不做控件的四个键里,`field.system` 那一个尤其值得保留 ——
给它控件等于让作者自称“这是系统列”,而记录校验会对系统列跳过必填与多值检查,等于多一个静默关掉校验的开关;②
文案口径写“运行时到底怎么做”、并且把“缺省会解成什么”写进去,是作者从枚举值上读不到的那一半,值得定为默认风格。
席位复核写于 2026-09-22T06:39Z;此前的文档漂移提醒已单独回执。
**你要做的。** 确认两点:① 4 个不做控件的键,理由是否认可;②
这批控件的文案口径(写"运行时到底会怎么做",而不是复述字段名)是否就是你要的默认风格。确认后按常规流程合入即可。
---
_Generated by [Claude Code](https://claude.ai/code)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 2b80461 commit 408ca2e
18 files changed
Lines changed: 965 additions & 7 deletions
File tree
- .changeset
- packages
- lint/src
- platform-objects/src/apps/translations
- spec/src
- ai
- automation
- data
- identity
- security
- ui
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
571 | 571 | | |
572 | 572 | | |
573 | 573 | | |
574 | | - | |
| 574 | + | |
| 575 | + | |
| 576 | + | |
| 577 | + | |
| 578 | + | |
| 579 | + | |
| 580 | + | |
| 581 | + | |
| 582 | + | |
| 583 | + | |
| 584 | + | |
| 585 | + | |
| 586 | + | |
| 587 | + | |
| 588 | + | |
| 589 | + | |
| 590 | + | |
| 591 | + | |
| 592 | + | |
| 593 | + | |
| 594 | + | |
575 | 595 | | |
576 | 596 | | |
577 | 597 | | |
| |||
651 | 671 | | |
652 | 672 | | |
653 | 673 | | |
654 | | - | |
| 674 | + | |
| 675 | + | |
| 676 | + | |
| 677 | + | |
| 678 | + | |
| 679 | + | |
| 680 | + | |
| 681 | + | |
| 682 | + | |
| 683 | + | |
655 | 684 | | |
656 | 685 | | |
657 | 686 | | |
| |||
0 commit comments