Skip to content

feat(spec): give the 45 declared-but-unoffered scalar metadata keys a form row each - #19673

Merged
os-justin merged 8 commits into
mainfrom
claude/issue-19331-scalar-form-rows
Sep 22, 2026
Merged

os-justin merged 8 commits into
mainfrom
claude/issue-19331-scalar-form-rows

Conversation

@os-justin

@os-justin os-justin commented Sep 22, 2026 •

Copy link
Copy Markdown
Collaborator

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:

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

Bucket B8 of the top-level zod-only census: keys an object-rooted metadata
schema declares, whose liveness verdict is `live` and whose schema node is a
scalar, that no form in METADATA_FORM_REGISTRY offered. Re-derived on this
tree with the reconciliation gate's own helper block: 49, not the 47 the card
relayed. Four are deliberately left out (a deprecated alias, a
machine-managed publish gate, a platform-stamped marker, and one key whose
describe and its only measured consumer disagree about the value vocabulary).

Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1
Co-authored-by: Claude <noreply@anthropic.com>
…dles

`pnpm i18n:extract` after the 45 new form rows. The three translated locales
arrive as extractor fills (the source string verbatim); they are authored in
the next commit.

Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1
Co-authored-by: Claude <noreply@anthropic.com>
… three locales

The three translated catalogs are ratcheted: a leaf that reads its `en` source
and is not decided or deferred is red on the day it lands
(`*-echo-decisions.test.ts`). So the 45 new form rows arrive translated rather
than filled. Machine tokens — enum members, `params._selectedIds`, the
`{0000}` / `{recordId}` placeholders, `sys_permission_set.description` — are
kept verbatim in every locale; an author retypes them.

Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1
Co-authored-by: Claude <noreply@anthropic.com>
Two decision-ledger counts in platform-objects (the object form's collapsed
and open leaf populations, and the catalog-wide translated-label control) and
two corpus counts in the lint predicate census. Every one is a measurement of
the shipped forms, re-taken here with the delta enumerated rather than
inferred: nineteen predicates added, none removed.

Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1
Co-authored-by: Claude <noreply@anthropic.com>
…adata form

Judged against `packages/spec/package.json`'s `files[]` rather than by
category: `dist` is published and the new help text is in it (measured by
grep, with a lit and a dark control), so spec takes a minor. The
platform-objects translation bundles reach `dist/metadata-translations`
(non-ASCII escaped, which is why a CJK grep over dist reads zero), so that
package takes a patch. `@objectstack/lint` publishes `dist` only and its
change here is a test file, so it takes none.

Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/platform-objects, @objectstack/spec, touching 36 documentable anchor(s).

66 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 97f4f8c8282ebb78de0815cec24eddb8b8335eb0.

⛔ 13 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: sharingModel (literal, 42 pages)
  • 10 name(s) were too generic to anchor anything (single lowercase words)
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 136 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 97f4f8c8282ebb78de0815cec24eddb8b8335eb0 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 97f4f8c8282ebb78de0815cec24eddb8b8335eb0

⚠️ 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 97f4f8c8282ebb78de0815cec24eddb8b8335eb0 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

Review catch on the collapsed-section ledger. The card contributed ten leaves
— five `advanced` rows, a label and a help text apiece — which is the +10 both
counts below already read. Writing 12 closed a sum that was short before this
card ever touched the file (9 + 2 + 32 = 43 against an asserted 45), which
would have absorbed a two-leaf gap into this card's provenance in the one file
whose job is to be the measured record. That gap is named and left standing.

Comment only: no assertion value moves, and `git diff` carries no `expect(`
line.

Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1
Co-authored-by: Claude <noreply@anthropic.com>

Copy link
Copy Markdown
Collaborator Author

Docs drift — receipted by the seat, with the reading that says no page moves. 2026-09-22T06:29Z

domain:spec execution seat, on docs-drift-check's comment 5772019296. ⛔ Not a dismissal: the run's 36 anchors and 66 hand-written pages were taken as a work list and read.

The verdict is that nothing here can falsify a page, and the reason is structural rather than a spot check: this change moves no schema, no accept set and no export. It adds form ROWS to METADATA_FORM_REGISTRY and the translated copy those rows force. Every anchor the run matched is a key NAME, and the pages under content/docs/references/** that carry those names are GENERATED from the schemas — unchanged here, which is also why pnpm --filter @objectstack/spec run check:generated reads all 15 artefacts up to date on this head.

The one hand-written page that names the subject was read in full rather than grepped: content/docs/protocol/kernel/i18n-standard.mdx:413-427 (「Metadata authoring forms (metadataForms)」). It states the MECHANISM — the type's authoring form lays out the panel, a bundle localises it under metadataForms.<type>.fields.<key> — and carries ⛔ no count, ⛔ no key list, ⛔ no claim about which keys have a control. A row landing under metadataForms.object.fields.nameField is the shape that page documents, so this change confirms it. Nothing to edit.

⛔ The 13 release-owned pages are read-only per AGENTS.md Documentation Guardrails and are untouched by this diff.

⚠️ The run's own two blind spots, answered rather than inherited: ① the emitter-vs-inputs gap — the rule this diff carries is 「a declared live scalar key gets one form row」, and no page states that rule by its inputs, because no page states it at all (the card exists precisely because the population was never written down); ② the name-is-not-a-key hazard — none of the 45 keys is one of the seven ambiguous spellings the #19093 census names (active, aria, joins, objects, template, tools, version).


Generated by Claude Code

os-justin commented Sep 22, 2026 •

Copy link
Copy Markdown
Collaborator Author

Contract review

9e6e78dd1e871028f1969863e87b20ceffd5e0a4

Reviewed-by: domain:spec execution seat 5 (session_01Sfe5YjBLwB9J3y8fvm2xq1, seat post #19357)
Served-tier: CONTRACT_REVIEW_TIER — at tier, so the clause-② judgement is made in-seat.

⚠️ This comment was edited in place at 2026-09-22T07:07Z, for two reasons, both stated rather than quietly fixed.

  1. ⛔ It named a model identifier. AGENTS.md: 「no model identifier lands in a PR title or body, a comment, a changeset, a doc or a code comment」 — and a review of record IS a comment. The tier is named by its CONSTANT above, which is what the queue guard's reader wants and what the rule permits.
  2. It was anchored to 047f2917e, and the head moved. The delta between the two heads is one commit, one file: .changeset/19331-scalar-form-rows.md, whose field.format bullet was corrected (see 「What moved after the first record」 below). ⛔ No source file, no test, no assertion and no count moved, so every reading below stands on this head as taken.

⛔ Reviewed against the diff and origin/main, ⛔ not against the dev's report. The C6 record is owed on every round this lane delivers, Clause-② yes or no alike; this one reads no.

Clause-② — NO, and the reason is structural

Nothing in this diff can change the accept/reject verdict on any input. Measured: the 18 changed files contain zero *.zod.ts. No schema, no .describe(), no export, no package.json files[] entry moves. METADATA_FORM_REGISTRY is declared Readonly<Record<string, FormView>> — an opaque map whose row CONTENTS were never part of the declared surface, so a row landing inside it is not a surface widening under any reading of the clause.

What does move is the form payload getMetaTypes() serves and the translation keys os i18n extract walks — a published-bytes change, not an accept-set change. That is the pair the changeset grades: @objectstack/spec minor, @objectstack/platform-objects patch, @objectstack/lint none (its only change is a test). skip-changeset is ⛔ not available here.

What this seat verified by counting, ⛔ not by reading the report

claim this seat's reading
45 rows landed 45 — counted by field: key across the ten *.form.ts patches: 1+1+2+19+6+1+4+9+1+1
90 i18n leaves 90 — 45 label: + 45 helpText: per bundle; +180 lines = 45 × 4, identically in all four catalogs
the declared file surface expanded by 7 7 — 18 files − 10 *.form.ts − 1 changeset; source-hashes.generated.ts correctly absent, which is the provenance claim's own negative side
the lint pin moved by a measured 19 19 = 15 field + 4 others; 11 quoted-literal comparisons + 7 in gates + 1 bare true = 19; 19 + 26 = 45 — reconciles in both directions
the object-form pins moved by 10 and 2 advanced 36 → 46 and open 94 → 96, matching object.form.ts's 5 advanced rows + nameField in basics

Three of the card's five example keys do not hold, re-measured here on origin/main: field.precision is already offered (field.form.ts:73); shortcutKey has 0 occurrences repo-wide; permission.license is declared neither by PermissionSetSchema nor by packages/spec/liveness/permission.json. The population is 49, of which 45 land and 4 are named.

Copy口径 spot-checked against the schema that owns each key, because a help text stating runtime behaviour is a claim: ownership (「absent → user」), sharingModel (「a CUSTOM object that omits it resolves to private」 — the qualifier is load-bearing: a system object defaults its org-wide sharing to public), undoable (action.zod.ts:1320), app.hidden, nameField (ADR-0079). All five hold.

Two items were sent back before this record, ⛔ neither waved through

  1. A comment mis-attributed two leaves to this card. object-collapsed-sections-echo-decisions.test.ts said 12 where the card brought 10, absorbing a 2-leaf shortfall that predates it. Fixed comment-only; git diff --unified=0 filtered to expect( returns 0 changed assertions.
  2. What moved after the first record — a false claim on its way to npm. The changeset said field.format's liveness evidence 「contains no read of the key」. Measured false by this seat: packages/spec/src/data/autonumber-format.ts:196-202 resolves autonumberFormat → format → the default, and packages/objectql/src/engine.ts:5043-5051 calls it for every autonumber field (the SQL driver too). The read is TRANSITIVE, so grepping the cited file for the key could not find it whatever the truth was. The DECISION — no control for this key — is unchanged and stronger: it is one z.string() carrying an autonumber pattern, a date display style and a describe vocabulary (email / phone) nothing honours. Corrected in the changeset before it ships, and in this PR body.

Main-branch risk, read rather than assumed

The class this seat caught on a sibling PR — a .describe() the console pins verbatim moving under a gate that is skipped on the PR and live on main — has no analogue here: the Console Pin Gate asks 「does the PINNED SHA still BUILD」 against this tree's @objectstack/client, and this diff touches no schema, no exported type and no describe. Zero *.zod.ts files is the whole argument.

Filed out of this round, ⛔ by the seat, ⛔ not by the dev

#19677 (the object designer offers two formula return types FieldSchema.returnType refuses) · #19678 (FormSelectOptionSchema.value is a system identifier, so hyphenated and capitalised enum members are unspellable as option values) · #19679 (field.format's vocabularies).

Verdict

ACCEPT, on this head. CI reads 32 success / 3 skipped / 0 incomplete, latest-per-name. Not a governed surface, so no maintainer merge is owed; the two confirmations the 速读 asks for are the author's call at review, ⛔ not a gate on landing.


Generated by Claude Code

The published bullet said the liveness row's evidence pointer carries no read
of the key. That is false and the method behind it was wrong: the read is
transitive, so grepping the cited file for a literal `.format` could not see
it. `resolveAutonumberFormat` falls back from `autonumberFormat` to `format`,
and the engine calls that resolver for every autonumber field — as does the
SQL driver.

The decision does not move: the key still gets no control, and for a stronger
reason. One `z.string()` carries three vocabularies — an autonumber pattern on
the engine arm, a date display style on the renderer arm, and a third in the
published `describe` that nothing measured honours. No help text can be
written until one of them is ruled.

Re-verified on this tree, and the renderer arm at the sha this repo pins
rather than at whatever the sibling checkout happens to be.

Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1
Co-authored-by: Claude <noreply@anthropic.com>
@os-justin
os-justin marked this pull request as ready for review September 22, 2026 07:04
@os-justin
os-justin enabled auto-merge September 22, 2026 07:04
@os-justin
os-justin added this pull request to the merge queue Sep 22, 2026
Merged via the queue into main with commit 408ca2e Sep 22, 2026
37 checks passed
@os-justin
os-justin deleted the claude/issue-19331-scalar-form-rows branch September 22, 2026 07:26
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
… metadata form (objectstack-ai#20064)

Part of objectstack-ai#19333
Clause-②: no

This PR gives the top-level keys that no metadata form may offer a
recorded reason. The reasons live in the metadata-form reconciliation
ledger, at the root coordinate PR objectstack-ai#19639 added. It does **not** switch
on the top-level `zodOnly` assertion. After this PR, 40 top-level keys
on the 16 object-rooted types still have neither a form row nor a
recorded reason, so the population does not close.

What stays open under objectstack-ai#19333: the one residue key that fits none of the
card's buckets (`field.format`), and the open questions on the seven
`view` keys folded in from objectstack-ai#19334, which is no longer open. The other 39
residue keys are the structured-control bucket, and they are carded on
objectstack-ai#19332.

One file changes:
`packages/spec/src/system/metadata-form-zod-reconciliation.test.ts`. It
gets 14 ledger rows and a comment block. No schema, form, `describe()`,
liveness row or generated artifact changes.

## The population, re-derived on this tree

⛔ No number is carried over from the card or the thread. I copied the
reconciliation gate's own helper block byte for byte, from the top of
the file down to the first `describe(`, into a throwaway probe test next
to it. The probe runs the same `resolveCoordinate(form, root,
ROOT_PATH)` / `offerableKeysAt` / `omittedAt(LEDGER, …)` calls the gate
runs. It was deleted afterwards and is not in the diff. Final slice:
bytes 0..34234, sha256 `7b97432d8408…`, prefix verified byte-identical
on disk. The controls are asserted inside the probe:

| control | reading |
|:--|:--|
| LIT: `name` | offered by **17 of 17** forms |
| DARK: a fabricated key, form side | offered by **0** forms |
| DARK: the same key, schema side | declared by **0** schemas |

| stage (17 registered forms) | before (base `7e6ca1787a`) | after (head
`39590d226c`, merged with main `980bc05e5b`) |
|:--|--:|--:|
| top-level zod-only keys, overlay included | 229 | 229 |
| of those, the ADR-0010 overlay (skipped at the root since PR objectstack-ai#19639) |
132 | 132 |
| non-overlay keys with no offer and no recorded reason | **97** |
**83** |

**Arithmetic.** 229 = 274 − 45. The census round measured 274 at
`596090efbe7`, and PR objectstack-ai#19673 has since landed 45 scalar form rows. 97 =
229 − 132, which is also 142 − 45. 83 = 97 − 14, and 14 is the number of
rows this PR adds.

**The card's 145 and the thread's 142 count two different sets.**
Neither is a misreading.
- 145 = 132 overlay + 13 keys in the card's own three other sub-buckets
(4 + 4 + 5). The card's table adds up to it.
- 142 = 274 − 132, every non-overlay key, including the 47 scalar, 39
structured and 36 + 7 `view` keys that belong to other cards.
- On this tree the matching figures are **146** (132 + 14; the extra key
is `field.system`, see below) and **97**.

## The 229, by sub-bucket, measured

I assigned each bucket from the key's own `describe()` and its row in
`packages/spec/liveness/TYPE.json`. None was decided here:

| sub-bucket | criterion | keys | disposition |
|:--|:--|--:|:--|
| ADR-0010 provenance / lock overlay | in `FRAMEWORK_FIELDS` (the 7
`MetadataProtectionFields` keys on all 17 forms, 119, plus `protection`
on 13) | 132 | **one reason, already in place**: the `FRAMEWORK_FIELDS`
skip. No rows, and a root row naming an overlay key is refused by the
resolve test |
| platform-written, never authored | describe says not authored / never
authored / machine-managed / auto-injected | 5 | a root `omit` row each
|
| deprecated or legacy alias | describe carries `[DEPRECATED …]` or
`[LEGACY ALIAS …]` | 4 | a root `omit` row each, the shape of the
`page.interfaceConfig.sourceView` precedent |
| declared, not enforced yet | liveness verdict `planned` /
`experimental`, or every child of the row is | 5 | a root `omit` row
each, saying the key is out of this gate until enforced |
| the seven `view` keys from objectstack-ai#19334 | no liveness verdict at any
coordinate | 7 | measured, **no row** (see below) |
| residue, object-rooted | fits no bucket above | 40 | 39 structured
(objectstack-ai#19332) + `field.format` |
| residue, `view` | per-arm keys | 36 | outside the top-level direction
until the per-arm forms exist, per the objectstack-ai#19330 ruling (letter A) |

132 + 5 + 4 + 5 + 7 + 40 + 36 = **229**.

### The 14 rows

- **Platform-written:** `app._unpublished`, `field.system`,
`view.columnState`, `view.isPinned`, `view.sortOrder`.
- **Deprecated / legacy alias:** `object.displayNameField`,
`object.titleFormat`, `view.drawerWidth`, `view.groups`.
- **Not enforced yet:** `object.externalSharingModel` (planned),
`field.useGrouping` (planned), `page.requires` (planned),
`agent.structuredOutput` (experimental), `action.onSuccess` (both
children `navigate` and `openIn` planned).

Why the five `view` rows are safe while `view` is outside the direction:
each of these reasons holds on every arm. None of the rows can excuse a
key that a future per-arm form ought to offer.

`field.system` is the one key not in the card's 145. PR objectstack-ai#19673 held it
out of the scalar bucket, and the census round routed it here. It meets
the platform-written criterion on its own `describe()` (`Auto-injected
system/audit field`, set against `author-declared business fields`).
Every writer is platform code:
`packages/spec/src/data/injected-system-column-provenance.ts`,
`packages/objectql/src/search-companion.ts`,
`packages/metadata-core/src/audit-field-governance.ts`. The record
validator skips its required and multi-value checks for a flagged column
(`packages/objectql/src/validation/record-validator.ts`), so a control
would let an author turn those checks off by claiming a false
provenance.

### `app._unpublished`: what it is

It is the ADR-0045 §3 publish gate, amended to its own key. The AI
materialization path writes it, `POST /packages/:id/publish-drafts`
clears it, and `filterAppForUser` reads it. It shares the ADR-0010
envelope's `_` naming convention but is **not** a member of that
envelope: it is not in `MetadataProtectionFields`, and only `AppSchema`
declares it. So its absence from `FRAMEWORK_FIELDS` is correct, not a
gap, and nothing here widens that set. It now has its own root row,
which gives a machine a place to read its machine-written status.

## The seven `view` keys

**The planned mechanism does not work.** The plan was to give them
verdicts in `packages/spec/liveness/view.json`. That ledger's walk stops
at the `container` arm of the `view` union: the gate's `shapeOf` takes
the first OBJECT member, and the `viewItem` arm is a discriminated
union, so it is passed over. `--dump view` walks only name, label,
object, list, form, listViews, formViews and the overlay. A row for any
of the seven is therefore an ORPHAN. Measured by planting a `config`
row: `check:liveness` exited 1 with `✗ 1 ORPHAN ledger row(s) …
view/config`. The file was then restored, blob `21c15486454c` equal to
HEAD.

**What was measured instead.** Readings at framework `7e6ca1787a` and
objectui `62597c588`, re-read at framework `980bc05e5b` and objectui
`f8a9d0fb0596` (the console pin on that main), with the same results:

| key | writers | readers | reading |
|:--|:--|:--|:--|
| `config` | `defineViewItem`; the console's `viewEnvelope` (Save as
view); `expandViewContainer` | `MetadataManager.getViewsByObject` serves
it; the console's View editor edits `draft.config` | **authored**, live
|
| `viewKind` | `defineViewItem`; `viewEnvelope`; `expandViewContainer`,
the server's `viewIdentityPatch` and the console's
`buildPersistedViewBody` also stamp it | `getViewsByObject` filters on
it; the console's `listViews` drops the form family on it | **authored**
discriminator, live |
| `order` | `expandViewContainer`; the authoring door's own guidance
names it "the authored default" beside the per-user `sortOrder` |
`getViewsByObject` sorts on it | **authored**, live |
| `isDefault` | declared on the strict authoring door; the console's
set-default (`setDefaultViewPatches`) | the console's switcher |
**authored**, and also console-written as shared state |
| `scope` | `expandViewContainer` stamps `package`; nothing writes
`shared` or `personal` | the generic metadata list's `scope` filter |
platform-stamped; no runtime writer |
| `owner` | none in either repo | none in either repo | inert |
| `hidden` | none in either repo | none in either repo | inert |

None of the seven gets a row. The four authored keys would be excused by
a row, and whether an arm form should offer them is a question for the
first per-arm form, not for this ledger. The other three have no live
writer, so there is no platform-written state to give as the reason. The
readings are kept as a comment at the end of the ledger. They are not in
`view.json`, because `liveness/` is in `@objectstack/spec`'s published
`files[]` and a note there would change the tarball.

## Why the `zodOnly` check is not wired

The direction objectstack-ai#19188 needs covers the 16 object-rooted types (per the
objectstack-ai#19330 ruling, letter A). 40 of their keys still carry neither an offer
nor a reason. Wiring the check now would turn those 40 into red lines,
which is the shape the census round refused. `view` stays outside the
direction until its first arm form is registered.

## Ablation

Every leg went through `scripts/ablation-replace.mjs`. The anchor had to
hit, the mutation was verified on disk by marker count and blob change,
and the restore was proven by blob hash plus an empty `git diff HEAD`.
Every leg restored to `efc4637425b6` (the file at `9c63b38770`). The
subject is imported from `src` by relative path, so no build or `dist/`
sits between the mutation and the run.

1. **Remove the overlay reason.** `offerableKeysAt`'s root filter became
`return keys;`. The gate went red: 2 failed of 54, and `_lock is overlay
and must not be offerable at the root` names the keys. The census went
83 → **215**, exactly +132, all overlay keys.
2. **Delete one recorded reason** (the `app._unpublished` row). The
census went 83 → **84** and names `app._unpublished`. The gate stayed
**green**, 54 of 54. That is the measured statement of what "unwired"
means: while the `zodOnly` direction has no assertion, no gate notices a
row going missing.
3. **Point a row at a key the form offers** (`displayNameField` →
`nameField`). The resolve test went red: `object.(root).nameField: the
form offers it now — drop the ledger entry`. The new rows cannot outlive
the omission they excuse.

An earlier attempt at the `view.json` probe was a no-op: the replacement
contained its own anchor, so the tool refused before running anything.
It was re-run with a non-self-matching anchor, and that run is the
reading quoted above.

## Verification (head `39590d226c`)

- `@objectstack/spec` build: exit 0. Tree clean afterwards (no
`authorable-surface.base.json` or other artifact movement).
- `pnpm --filter @objectstack/spec exec vitest run --project local
--maxWorkers=2`: **534 files, 15708 passed, 2 todo**. Pre-merge at
`9c63b38770`: 533 files, 15681 passed.
- `pnpm --filter @objectstack/spec typecheck` (`tsc --noEmit` +
`check:scripts-typecheck` + `check:test-typecheck`): exit 0. The test
layer holds its identity-pinned debt with no new signature.
- The reconciliation gate plus the probe: 2 files, 54 tests passed.
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`: 77 commands, each run with its exit code
captured before any pipe. `--ran` first reported `77 derived, 73 run, 4
NOT-MEASURED, 0 UNRUN`: the four exit-3 families
`check:doc-formula-expressions`, `check:dual-build-cjs-loads`,
`check:lean-entry-closure` and `check:type-check-debt` were
`PREREQUISITE NOT MET` on other packages' `dist/` while the
whole-closure build waited for the shared lock. After that build ran
under the lock (turbo 72/72, verdict 0), all four were re-run and exit
0, and `--ran` reports `77 derived famil(ies) accounted for — 77 run, 0
NOT-MEASURED`. (Updated by the seat from the dev's report `5825062779`.)
- `check:liveness`: green, and `state-counts.md is current`.
- Lint, narrowed and measured: `eslint --no-inline-config --format json`
on the one changed file gives 1 file, 0 errors, 0 warnings. The
population comes from eslint's own `--print-config` (the file is linted,
not ignored). The config has no `parserOptions.project`, no
`projectService` and 0 typed rules. It is therefore not type-aware, and
a test-file edit cannot move any other file's verdict.

## Changeset

None. The only changed path is not published: `npm pack --dry-run` of
`@objectstack/spec` lists 2034 files, with 0 `*.test.ts` among them,
while the positive controls `src/ui/view.zod.ts` and
`liveness/view.json` are present. The documented no-release route is the
`skip-changeset` label (AGENTS.md, Post-Task Checklist step 3). This
PR's author does not write labels, so the seat applies it.

## Acceptance notes

- **The five not-enforced rows hold only while the verdict does.** No
leg re-reads the liveness verdict, so when one of these keys becomes
enforced its row goes stale silently. The ledger comment says to delete
the row at that point. Carrier: whichever PR enforces each key.
- **`object.actions` sits on the edge of the platform-written
criterion.** Its describe says "auto-populated from top-level actions
via objectName", but 8+ platform objects author `actions: […]` inline,
so it stays with the structured bucket (objectstack-ai#19332).
- **The liveness ledger covers only one arm of the `view` union**
(`container`). The keys of the other arms can hold no verdict there at
all. This was noted by the census round, and it goes to whoever
registers the first per-arm form. The two inert keys above are the part
of it that is a finding, handed to the filing seat.
- The `274 … 132 of them this overlay` figures in the file's existing
comments are dated readings from the census tree and were left as
written.

---
_Generated by [Claude
Code](https://claude.ai/code/session_019c3Hi6ZMU1p6m6aA6Bz45d)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…meField (objectstack-ai#21854)

Fixes objectstack-ai#21765

Clause-②: no

Item 3 of the card, under the director seat's ruling A (`5989738766`):
Studio's object form now offers `imageField`, the record's picture, as a
plain `text` row beside `nameField`. Items 1 and 2 landed in PR objectstack-ai#21824.
With this PR, the card's last open item is done.

## What changed

- **The form row** (`packages/spec/src/data/object.form.ts`): one `{
field: 'imageField', type: 'text', colSpan: 1, helpText }` row directly
after `nameField`. It has the same face as `nameField`, for the same
reason: the value names one of the object's own fields, and the registry
has no own-field picker. The row adds no picker and no validator.
`refuseNonPictureImageField` at parse stays the one judge, and its
refusal at save is what an author sees. The helpText says what the parse
accepts: a field of this object whose type is `image` or `avatar`. Left
empty, there is no record picture and no placeholder. The metadata
form's text input writes no key when cleared (objectui `SchemaForm.tsx`
at the pin `0abd4f9f8769`: `onChange(e.target.value || undefined)`), so
"empty" in the helpText means the key is unset, which the parse reads as
no picture.
- **The stale ledger row** (`metadata-form-zod-reconciliation.test.ts`):
the `imageField` `omit` row in the declared-not-enforced group is
deleted. No other row moves.
- **The catalogs**: the four
`packages/platform-objects/src/apps/translations/*.metadata-forms.generated.ts`
files were regenerated with `node scripts/check-i18n-bundles.mjs
--write`. The zh-CN / ja-JP / es-ES `label` and `helpText` leaves were
then written in place, since translated-locale values are hand-written
in those files (AGENTS.md, Documentation Guardrails). A second `--write`
kept them and dropped the six provenance entries the first extract had
added to the three `*.source-hashes.generated.ts` companions. Those
companions are byte-unchanged in this diff.
- **The pin** (`metadata-form-declared-rows.pin.test.ts`): three tests.
A lit and dark control for the locator. The key is offered once, as a
`text` row with no widget. It sits directly after `nameField`, in the
same section, at the same `colSpan`. The reconciliation test proves only
that the key is offered somewhere on the form; it reads neither the
control nor the position, so this pin covers what it misses. No test
title or string carries a tracker id.
- **Two measured counts in `platform-objects` were re-taken**, both
forced by the new row:
- the object form's open-section leaf population: 114 → 116
(`object-collapsed-sections-echo-decisions.test.ts`);
- the catalog-wide translated-label control: 659 → 660 per locale
(`object-lifecycle-panel-echo-decisions.test.ts`).

  The `openEchoes` reading stays at zero.
- **Changeset**: `@objectstack/spec` patch and
`@objectstack/platform-objects` patch (below).

⛔ No Zod change, no liveness change, no new gate or ledger class.

## The reconciliation test: predicted, then measured

Predicted before writing: red without the form row, green with it.
Measured on the committed state, with `scripts/ablation-replace.mjs` in
wrap mode and a `trap` restore proven by blob equal to HEAD:

| state | reading |
|---|---|
| row deleted, form row present (this PR, `c730a8c598`) | 2 files, **87
passed (87)** |
| row deleted, form row removed (anchor 1 → 0, blob `d867e358e5e5` →
`a67cdaed2b22`) | **3 failed / 84 passed**: `object.(root): accepted by
the Zod but unauthorable in the form … expected [ 'imageField' ] to
deeply equal []`, plus the two new pin tests. Restored: blob
`d867e358e5e5` == HEAD, `git diff HEAD` empty |
| control: the stale row planted back beside the form row | **1 failed /
75 passed**: `object.(root).imageField: the form offers it now — drop
the ledger entry`. Restored: blob `2a7c49dca958` == HEAD |

The first attempt at the control leg was a no-op. The tool refused it
before running anything, because the replacement text contained its own
anchor. It was redone with a non-overlapping replacement, which is the
reading above.

## What ships

Each package was built and its `dist/` grepped, with a positive control:

- `@objectstack/spec`: the new helpText is in 8 dist files; the control,
`nameField`'s helpText, is in 8.
- `@objectstack/platform-objects`: the new en helpText is in 6 dist
files, the control in 6. The zh-CN, ja-JP and es-ES labels are in 6
each, matched on literal or unicode-escaped text.

Both packages publish the change, so both get `patch`. `skip-changeset`
does not apply.

## Verification (final head `9f6b177875`)

- `pnpm --filter @objectstack/spec exec vitest run --project local
--maxWorkers=2`: Test Files 614 passed | 1 skipped (615), Tests 18385
passed | 1 skipped | 1 todo. The skipped file is
`scripts/root-entry-type-nameability.pin.test.ts`, which is gated by
`OS_EXPECT_ROOT_NAMEABILITY`.
- `pnpm --filter @objectstack/spec run typecheck`: exit 0. `tsc -p
tsconfig.test.json --listFilesOnly` compiles `object.form.ts` and both
changed spec tests.
- `pnpm --filter @objectstack/platform-objects exec vitest run
--maxWorkers=2`: 59 files, **949 passed**. Run before the two counts
were re-taken, it gave exactly two failures: `expected 116 to be 114`
and `zh-CN positive control: expected 660 to be 659`. Typecheck: exit 0.
- `metadata-forms-vocabulary.test.ts`: 5 passed. `pnpm check:i18n`: exit
0, `platform-objects in sync (11 bundle(s))`. `pnpm
check:i18n-coverage`: exit 0, `13 config(s), 621 baselined untranslated
string(s), none new`.
- `pnpm --filter @objectstack/spec run check:generated`: exit 0 (15
artifacts).
- Gates: `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 87 commands from 10 paths against
the merge base `8832655af`. All 87 were run on `9f6b177875`. `--ran`
reconciliation: `87 derived famil(ies) accounted for — 87 run, 0
NOT-MEASURED`. Four spec gates first exited 3 (`PREREQUISITE NOT MET`:
the dist predated a test-title edit). They exited 0 after `pnpm --filter
@objectstack/spec build`, and the record keeps that last reading.
- eslint `--no-inline-config --format json` on the 9 changed TS files: 9
files, 0 errors, 0 warnings. The population is `eslint.config.mjs:971`
(`**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}`). Linting is not type-aware (no
`parserOptions.project`), so untouched files' verdicts cannot move.

## Acceptance notes

- **Surface, stated.** The claim named `object.form.ts`, the
reconciliation test, the four catalogs and a changeset. Three more files
changed:
- `metadata-form-declared-rows.pin.test.ts` holds the pin the dispatch
asked for (item 4).
- The two `platform-objects` echo-decision tests carry the counts the
row moves. Those count lines are the ones PR objectstack-ai#19673 moved when it added
rows to the same section. A reviewer who reads them as outside the
surface can say so.
- No `platform-objects` source or object file moved, and no other
catalog key changed.
- The authored leaves live in the generated catalogs themselves, so no
further file joins the surface for them.
- `origin/main` gained one commit after the branch point (`2799155678`,
test titles in `packages/spec/src/data/`). It touches none of this PR's
paths.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

#19188 split: 47 top-level zod-only keys are scalar controls needing one form row each

2 participants