feat(spec): curated activityMilestones, publicSharing, userActions and inlineColumns form rows (#19332, flight G2b) - #20485
Conversation
…d inlineColumns form rows
Flight G2b of ruling record 5861442317: the object form gains the
activityMilestones repeater (4 sub-rows), the publicSharing composite
(6 sub-rows) and the userActions composite (5 sub-rows); the field form
gains the inlineColumns repeater over a curated subset (4 of 20 keys),
with its nested `subset` row in the reconciliation ledger. The two new
repeaters' row schemas carry a JSON Schema title on every property
(24 `.meta({ title })` calls, nothing else in either *.zod.ts).
Claude-Session: https://claude.ai/code/session_01ARcDurZ5j34RdqsGgc4jgH
Co-authored-by: Claude <noreply@anthropic.com>
The extractor regenerated the 46 en leaves (23 rows, a label and a help text each); the 138 zh-CN / ja-JP / es-ES leaves are authored, and a second --write kept them and left no source-hash row. Two help texts reworded before translation. Claude-Session: https://claude.ai/code/session_01ARcDurZ5j34RdqsGgc4jgH Co-authored-by: Claude <noreply@anthropic.com>
object collapsed-section leaves 69 -> 105 (advanced 60 -> 96: three new rows with fifteen sub-rows, 36 leaves); the translated-label control 634 -> 657 per locale (23 new row labels); the field form's repeater row properties 6 -> 10 with `inlineColumns` a second walked parent. Claude-Session: https://claude.ai/code/session_01ARcDurZ5j34RdqsGgc4jgH Co-authored-by: Claude <noreply@anthropic.com>
… rows Claude-Session: https://claude.ai/code/session_01ARcDurZ5j34RdqsGgc4jgH Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 27 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 9 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 137 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 58678954985b174c164ac4991d32d13681b2f2eb && git checkout 58678954985b174c164ac4991d32d13681b2f2eb
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3062e500150c7a230be9344f9c9e17cc173bc095 2bcad436f3efb86e4390a565efd72e814cd942e9 && git checkout -B drift-repro 3062e500150c7a230be9344f9c9e17cc173bc095 && git merge --no-ff 2bcad436f3efb86e4390a565efd72e814cd942e9
node scripts/docs-audit/affected-docs.mjs --json 3062e500150c7a230be9344f9c9e17cc173bc095
|
82 predicates (81 + the field form's inlineColumns gate, data.type == 'master_detail') and 57 quoted-literal comparisons. The corpus was differenced against the merge base e956924 by form::field::source: one added, none removed. Claude-Session: https://claude.ai/code/session_01ARcDurZ5j34RdqsGgc4jgH Co-authored-by: Claude <noreply@anthropic.com>
…b-remaining-g2-rows
…dded Since object-field-ref-unknown judges indexes[].fields, publishing and os validate refuse an index column that names no field of the object; a draft save (the schema parse) still does not check. A real field that is not a stored column (a formula) is still skipped whole by the SQL driver with a warning. The help text, its comment and its four catalogue leaves now say exactly that. Claude-Session: https://claude.ai/code/session_01ARcDurZ5j34RdqsGgc4jgH Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Inputs: card #19332 (body + all 25 comments: ruling Check-runs on the head (the gate verdicts): 42 completed — 37
① Derived judgments
② Semver level
Clause-②: no ③ Boundary flagsDev flags, in report order:
The brief's five claims:
Escalated for the landing seat, not a defect of this diff: Implemented-by: VERDICT: PASS |
… help that shipped as English copies (objectstack-ai#20490) Fixes objectstack-ai#20462 Clause-②: no ## What changed A zh-CN console showed English on Setup surfaces: the Invite user dialog listed the membership roles as 所有者 / 管理员 / **Delegated Admin** / 成员, and a team record read **MEMBER COUNT**. The keys were present in `packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts`, but their values were byte copies of the `en` source, as `os i18n extract --fill=default` seeds them. - **320 copied leaves translated** in the zh-CN objects bundle. Only leaf values changed; the structure is untouched. The two reproduced strings now read 受托管理员 (on both `sys_member` and `sys_invitation`) and 成员数. - **42 leaves kept in English by design**, each declared with its reason in a decision ledger, `objects-zh-cn-echo-decisions.test.ts`. The list is below. - **The provenance companion** `zh-CN.source-hashes.generated.ts` was regenerated by `pnpm i18n:extract`, never by hand. It records a leaf only while the leaf is a byte copy of its source, so 320 rows dropped and 42 remain. The 42 are set-equal to the declared echoes. - **Pin:** the reproduced leaves `sys_member.fields.role.options.delegated_admin`, `sys_invitation.fields.role.options.delegated_admin` and `sys_team.fields.member_count.label` are pinned as translated under zh-CN. The role options must read one word per role on both objects. - `ja-JP` and `es-ES` are not touched. They carry the same copy class; their counts are under *Out of scope*. **The coverage-ratchet half is out of scope, per triage (5873680175).** Teaching `i18n-coverage-baseline` to count a copied source string as untranslated widens an existing gate. If the maintainer wants that ratchet, it is a separate gate card. The new ledger deliberately does not assert that no other zh-CN leaf is a copy, for the same reason. ## Copy count, before and after, by key class A zh-CN string leaf counts as a copy when it is byte-equal to the `en` leaf at the same path, over every leaf of `en.objects.generated.ts` / `zh-CN.objects.generated.ts`. Both bundles have 1529 leaves. | key class | before (b810ddb) | after | |---|---:|---:| | `label` (object, field, view, action param) | 196 | 27 | | `options.*` | 20 | 9 | | `pluralLabel` | 11 | 0 | | `help` | 114 | 0 | | `description` | 11 | 0 | | `placeholder` | 4 | 4 | | `emptyState.title` / `.message` | 4 | 0 | | result-dialog field (`client.client_id`, `client.client_secret`) | 2 | 2 | | **total** | **362** | **42** | | `label` + `options.*` | 216 | 36 | The card reads 338 in total, 192 of them `label` / `options.*`, at 3cf6449. The zh-CN and en object bundles are byte-identical between 3cf6449 and b810ddb, and this walk reads 362 / 216 at 3cf6449 too. So the difference of 24 comes from the counting method, and the card does not state its method. `help` / `description` / `placeholder` / `emptyState` are decided as the same class as `label`. The extractor files each of them as an `ExpectedEntry` under the same `source` kind as its element's label (`field`, `object`, `view`, `action`; `packages/cli/src/utils/i18n-extract.ts`), and the coverage detector consumes them identically. So they were decided as the same class: `help`, `description` and `emptyState` were translated, and all four placeholders are declared echoes, because each shows a value the admin types (see the list below). ## Kept in English by design (42), with reasons | leaves | value(s) | reason | |---|---|---| | `sys_account._actions.link_social.params.provider.options.{google, github, microsoft, apple, facebook, gitlab, discord}` (7) | Google, GitHub, Microsoft, Apple, Facebook, GitLab, Discord | Sign-in provider brands, shown on the link button as the name a user recognises. The bundle renders no brand name: the authored SSO empty state keeps Okta, Entra and Auth0 verbatim. | | `fields.id.label` on `sys_oauth_application`, `sys_oauth_access_token`, `sys_oauth_refresh_token`, `sys_oauth_consent`, `sys_oauth_resource`, `sys_oauth_client_resource`, `sys_oauth_client_assertion`, `sys_sso_provider`, the eight `sys_scim_*` objects, `sys_email_template`, `sys_metadata`, `sys_metadata_history`, `sys_view_definition`, `sys_metadata_audit`, `sys_secret`, `sys_setting_audit` (23) | ID | The bundle keeps the initialism verbatim in every label that carries it (用户 ID, 团队 ID, 客户端 ID). A bare `ID` has no noun to render, and adding one would invent content. | | `sys_oauth_application.fields.jwks.label`, `.jwks_uri.label` (2) | JWKS, JWKS URI | Protocol names the bundle keeps verbatim: `sys_jwks` is authored 签名密钥 (JWKS), and URIs stay URI (重定向 URI). Their `help` leaves are translated. | | `sys_oauth_application._actions.create_oauth_application.resultDialog.fields.client.client_id` / `.client_secret` (2) | Client ID, Client Secret | The credential names an admin copies out of the one-time dialog. The same dialog family authored its zh-CN text with the names verbatim (我已保存 Client Secret, 新的 Client Secret). | | `sys_sso_provider._actions.register_saml_provider.params.entryPoint.label` (1) | IdP SSO URL | Nothing but protocol initialisms the bundle keeps verbatim. The sibling SAML params are authored IdP 实体 ID and IdP 签名证书. | | `sys_sso_provider._actions.register_sso_provider.params.{scopes, mapEmail, mapName}.placeholder` (3) | openid email profile, email, name | A placeholder shows the literal value the admin types: OIDC scope and claim names the IdP matches byte for byte. The authored help on the same params keeps them verbatim (默认为 “email”). | | `sys_sso_provider._actions.register_saml_provider.params.identifierFormat.placeholder` (1) | urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress | The SAML NameID format URN the admin types: an identifier, not prose. | | `sys_email.fields.message_id.label` (1) | Message-ID | The RFC 5322 header name. The authored help on the same field keeps it (传输层分配的 RFC-5322 Message-ID). | | `sys_setting_audit.fields.source.options.{ui, api}` (2) | UI, API | Initialisms the bundle keeps wherever they appear (API 密钥, 在 UI 中暴露). | **Where the declaration lives.** Neither the extract config nor `os i18n extract` / `os i18n check` has a keep-source mechanism: no allowlist, no flag, no provenance mark. `--filter` only narrows emission. The existing mechanism that declares a leaf "English by design" is this package's echo-decision ledger. The shape is `type Verdict = 'translate' | 'echo'`, with a reason on every row and a departure reason required for every `echo` (`undeclaredEchoes`). It lives in `packages/platform-objects/src/apps/translations/*-echo-decisions.test.ts`, one file per family. `report-dataset-panel-echo-decisions.test.ts` is the first: it has `Verdict`, a reason per row and the per-locale departure check written inline. The named `undeclaredEchoes` predicate arrives in the later rounds, first in `object-field-editor-panel-echo-decisions.test.ts`. `objects-zh-cn-echo-decisions.test.ts` is that shape applied to the objects bundle. No new mechanism was added. ## Terminology The terms follow the zh-CN leaves that were already authored, so one term is not translated two ways: - `delegated_admin` is 受托管理员. That is the console's own word for this role value (objectui `organization.roles.delegatedAdmin`). The other three roles already read the same in both places (所有者 / 管理员 / 成员). The alternative is 委派管理员, the in-repo word for the ADR-0090 concept (委派管理范围). One line changes it if the maintainer prefers it. - SCIM provisioning is 预配, following `sys_scim_group_member.display_title` (预配的用户及其所在组). Decommission is 下线. - The notification leaves take the service-messaging bundle's words: 主题, 严重程度 (信息 / 警告 / 严重), 负载, 去重键 and 通道. - `managed_by` options take `sys_metadata`'s words: 管理方, 平台 / 包 / 管理员. - OIDC claims stay `claim`, and help text keeps `scopes`, both as the bundle already writes them (映射:邮箱 claim; 客户端可申请的 scopes …). - "My Memberships" is 我的组织, the account nav's label for the same screen. `Web` is Web 应用, like the sibling `sys_oauth_application.fields.type.options.web`. - `sys_migration` keeps its section's ASCII punctuation. ## Collateral: nine existing ledgers encoded the old copy debt Nine panel ledgers in the same directory asserted floors on the zh-CN provenance table: more than 100 rows, more than 300, and more than 50 echoing objects leaves "to sample". Those floors held only while about 360 zh-CN objects copies existed, so any card that translated them would go red. Each floor's own message states its intent, "provenance table is empty" or "has no echoing objects leaf to sample". Each now asks for greater than 0, with a one-line comment saying why. One more test pinned `sys_oauth_resource.fields.access_token_ttl.label` as an unauthored fill in all three locales. Its own message says "if a translator authored it, this ledger note is stale". It now records that zh-CN is authored (访问令牌 TTL keeps the `TTL` token, the treatment the lifecycle rows give it), while ja-JP and es-ES stay fills. Files: `action-body-panel-`, `bare-type-display-`, `dataset-panel-`, `field-panel-`, `hook-execution-panel-`, `object-collapsed-sections-`, `object-lifecycle-panel-`, `page-interface-panel-` and `report-form-echo-decisions.test.ts`. PR objectstack-ai#20485 edits three of these files (`field-panel-`, `object-collapsed-sections-`, `object-lifecycle-panel-`) in different hunks. A local merge-tree run was not taken, so whether the two merge cleanly is not measured. ## Verification Measured at head `08ebcd01d`. That is a true merge of `origin/main` 851af0c, and the CLI closure was rebuilt after the merge. - **`pnpm check:i18n` (H3):** - exit 0 at base; - exit 1 after the leaf edits alone, reported as `platform-objects DRIFTED (1)`. The drifted file was the provenance companion; - exit 0 after `pnpm i18n:extract`. The extract changed only `zh-CN.source-hashes.generated.ts`: −320 rows, 0 added, and the bundle file stayed byte-identical. - **`pnpm --filter @objectstack/platform-objects exec vitest run --maxWorkers=2`:** 56 files and 921 tests passed. Before the collateral edit, the same run had 15 failures across 9 files, all of them the floors above. - **`pnpm --filter @objectstack/platform-objects typecheck`:** exit 0. `tsconfig.test.json` includes the new ledger (`--listFilesOnly`: 1 hit). - **Reverse verification.** Three mutation legs, each through `scripts/ablation-replace.mjs` on `zh-CN.objects.generated.ts`, each red as predicted, and each restored to blob `24cfb9355b89` with `git diff HEAD` empty: 1. `label: "成员数"` changed back to `"Member Count"`: 1 failed / 9 passed, "sys_team.fields.member_count.label reads its en source again". 2. Both `delegated_admin: "受托管理员"` changed back to `"Delegated Admin"`: 2 failed / 8 passed. The verdict row failed, and so did the role-word test ("expected 'Delegated Admin' to match /\p{Script=Han}/u"). 3. The declared echo `google: "Google"` changed to `"谷歌"`: 1 failed / 9 passed, "is a declared echo and must stay the en source". - **Derived gates:** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at `08ebcd01d` printed 61 commands. All 61 were run, and each exit code was written to a file before any pipe. - 60 exited 0 on their first run. - `pnpm check:dual-build-cjs-loads` first answered PREREQUISITE NOT MET (exit 3: workspace `dist/` missing for packages outside the CLI closure). That is not a measurement. Rerun once the `dist/` existed: exit 0. - `--ran` reconciliation: 61 derived, 61 run, 0 NOT-MEASURED, 0 UNRUN (exit 0). - `pnpm check:i18n-coverage`: exit 0, "621 baselined untranslated string(s), none new". The ratchet is unchanged, as expected. - NOT MEASURED locally, declared to CI: the 6 workflow-valued families, the 5 path-scheduled CI jobs and the 4 type-check lanes the derivation names. ## Patch round 1 The contract review 5876239898 (PASS at `08ebcd01d`) named one false word and two comment slips. All three are corrected here, as text only; no translated value, assertion or floor changed. - **Changeset:** the translated-class list no longer names placeholders. All four placeholders were decided as echoes, and none was translated. - **Ledger header** (`objects-zh-cn-echo-decisions.test.ts`, comment only): it no longer says the provenance table "kept exactly the 42" as an assertion. It now names what is asserted: every declared echo still has its provenance row, and no pinned translation has one. That the table holds nothing beyond the echoes is measured, and deliberately not asserted. - **This body:** the H4 sentence now says placeholders were decided as the same class and are all declared echoes. The ledger-history sentence now credits `undeclaredEchoes` to the later rounds rather than to `report-dataset-panel-`. - **Merge and readings:** `origin/main` fc0db22 was merged with a true merge commit. At head `927957df4`, `pnpm check:i18n` exited 0 and `pnpm --filter @objectstack/platform-objects exec vitest run src/apps/translations/objects-zh-cn-echo-decisions.test.ts` passed 10 of 10 (lock verdict: command-exit 0). The round-0 gate readings above were taken at `08ebcd01d` and were not re-run. ## Out of scope - The coverage ratchet (see above). - `ja-JP` and `es-ES` carry the same copy class, measured at b810ddb and not edited: - `ja-JP`: 383 copies (`label` 210 · `options.*` 20 · `pluralLabel` 12 · `help` 121 · `description` 12 · `placeholder` 4 · `emptyState` 4); `label` + `options.*` = 230. - `es-ES`: 392 copies (`label` 217 · `options.*` 22 · `pluralLabel` 12 · `help` 121 · `description` 12 · `placeholder` 4 · `emptyState` 4); `label` + `options.*` = 239. ## Acceptance notes - The existing zh-CN bundle renders `Locked Until` two ways, 锁定至 on `sys_user` and 锁定到 on `sys_job_queue`. This card uses 锁定至 for `sys_two_factor` and leaves the pre-existing pair alone. --- _Generated by [Claude Code](https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…objectstack-ai#20494) (objectstack-ai#20521) Fixes objectstack-ai#20494 Clause-②: no ## What changed - `packages/spec/src/data/object.zod.ts` (~:2123): rewrote the `activityMilestones[].type` `.describe()` to state the real default — an unset `type` keeps the update row's kind, `updated`. No schema shape change. - Regenerated `content/docs/references/data/object.mdx` with `gen:docs` (never hand-edited). - `packages/plugins/plugin-audit/src/activity-type-vocabulary-enforcement.test.ts`: corrected the pin's title and docblock only — they stop describing a divergence and stop saying it was "filed separately" (this card is where it was filed). Its assertions are byte-for-byte unchanged. - `.changeset/20494-milestone-type-default-describe.md` (`@objectstack/spec` patch, `Clause-②: no`). ## Why `audit-writers.ts` starts `activityType` from `activityTypeFor(action)`, and a milestone can only fire on the UPDATE branch (`create` / `delete` return their own summary before the milestone match runs), so an unset `type` has always emitted `updated`, never `completed`. `milestone.type` overrides it only when the author actually sets it — that half of the old describe was correct and is unchanged. The plugin-audit pin (`activity-type-vocabulary-enforcement.test.ts`) already measured this real answer; only its title and docblock claimed a divergence and said the finding was filed separately. ## Dedupe search for other stale copies A repo-wide grep for the old wording, and for every `activityMilestones` mention, found no other hand-written copy of the "completed" default. `object.form.ts`'s `activityMilestones.type` help text (shipped with PR objectstack-ai#20485 — "Unset: updated.") already states the real default and is unchanged. ## Verification record Commands run in this worktree, foreground, through `scripts/pm/os-verify-lock.sh` where heavy: - `pnpm --filter @objectstack/spec build` — pass. - `pnpm --filter @objectstack/spec run check:generated` — all 15 generated artifacts green after `pnpm --filter @objectstack/spec run gen:docs` (only `check:docs` was stale, for exactly this describe change). - `pnpm --filter @objectstack/spec test` — 572 test files / 16796 tests passed, 1 todo. - `pnpm --filter @objectstack/spec typecheck` — pass. - `pnpm --filter '@objectstack/plugin-audit^...' build` (dependency closure) — pass. - `pnpm --filter @objectstack/plugin-audit test` — 25 test files / 363 tests passed, including the renamed pin. - `pnpm --filter @objectstack/plugin-audit typecheck` — pass. - `pnpm check:doc-authoring` (dispatch-named — no tracker number in the describe) — pass. - `node scripts/check-nul-bytes.mjs` — pass. - `node scripts/check-adr-0087-registration.mjs`, `check-changeset-no-major.mjs`, `check-empty-changeset.mjs`, `check-issue-citations.mjs` (`--self-test` and `--base origin/main`, matched by the new changeset file) — all pass. `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran <record>` reconciliation: 112 families derived from this diff's paths, 102 UNRUN locally. Those are whole-tree/whole-package scans this describe-text-only change (no export, no authorable key, no schema shape change) does not plausibly touch — `check:generated`'s own 15-gate sweep already covers the ones that read the schema/docs pipeline this change lands in (`check:docs`, `check:api-surface`, `check:authorable-surface`, `check:export-origins`, `check:declaration-map`, `check:strictness-ledger`, `check:liveness`, `check:test-typecheck`). The remaining UNRUN families are deferred to CI's farm per this repo's local-verification scope. ## Acceptance notes Nothing found outside this card's file surface. --- _Generated by [Claude Code](https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx)_ Co-authored-by: Claude <noreply@anthropic.com>
Part of #19332
Flight G2b of ruling 5861442317.
Clause-②: no
Status: draft, no open gap
The first round stopped at one red test outside the claim's surface,
packages/lint/src/validate-predicate-path-refs.test.ts, the lint census of every predicate the shipped metadata forms carry. The claim both admitted mechanically moved population pins and forbadepackages/lint/**. The seat answered A and amended claim5873857698in place (its "Amended 2026-09-28T17:27Z" line): that one file joined the surface for its census pin only. The patch round landed it in16037890(Pins moved below), and the file reads 54 of 54.Under the claim's second amendment ("Amended 2026-09-28T17:51Z"),
2bcad436corrects one G2a text: theindexes.fieldssub-row's help text (and its code comment and four catalogue leaves) now reads "Saving does not check them; publishing and os validate refuse a name that is not a field of this object. A field that is not a stored column (a formula, say) makes the SQL driver skip the whole index, with a warning in the server log." Each clause was measured on this tree after4b2d9041(#20479):ObjectSchema.safeParseaccepts a misspelt column;os validate's rules and the runtime publish gate (runRuntimeAuthoringRules, typeobject) both returnobject-field-ref-unknownaterroron it; and an in-memory SQLiteSqlDriversync skips an index on a formula field (and one on a misspelt name) with[sql-driver] skipping declared index … column(s) not materializedatwarn, while the index on a stored column is created.What
Four live keys had no form row, so an author could reach them only through the Source tab. Each is now a row with hand-written sub-rows, as the ruling says: 「G2 (…) — hand-written curated sub-rows, plus … one nested
subsetrow forinlineColumns」. The four-locale catalogue rows are in this PR.object.activityMilestonesobject.form.ts, Advanced, aftervalidationstype: 'repeater':field(widget: 'text', required),value,summary(text, required),type(text)fieldGroupsrepeater face (declared, labelled sub-rows); the text sub-rows copy the plain text rows in Basicsobject.publicSharingobject.form.ts, Advanced, afterrequiredPermissionstype: 'composite':enabled(switch),allowedAudiencesandallowedPermissions(widget: 'multiselect'with inline options),maxExpiryDays(number,min: 1),redactFields(widget: 'string-tags'),eligibility(type: 'code',language: 'expression')access/lifecyclecomposite face;enabledtheenabletoggles; the two lists the multiselect objectui derives for an array of enum (the derivedappearance.allowedVisualizationson the view and page forms);redactFieldsthehighlightFieldsrow;eligibilitythefields.visibleWhenpredicate rowsobject.userActionsobject.form.ts, Advanced, undermanagedBytype: 'composite':create,import,edit,delete(widget: 'json'),exportCsv(switch)requiredPermissionsrow (jsonon a union);exportCsvtheenabletogglesfield.inlineColumnsfield.form.ts, Configuration, betweeninlineTitleandinlineAmountField, gateddata.type == 'master_detail'like bothtype: 'repeater'over a curated subset:name(text, required),label(text),width(number),defaultHidden(switch)fieldGroupsrepeater face; the gate copies its two sibling rowsShapes (dispatch assumption 2), confirmed on this base:
activityMilestonesisz.array(strictObject(…))atobject.zod.ts:2093, four keys;publicSharingis astrictObjectat:2286, six keys;userActionsastrictObjectat:1769, five keys;inlineColumnsisz.array(InlineGridColumnSchema)atfield.zod.ts:1460, the item schema at:890, twenty keys. The gate's ownkeysOfread the same sets.Ledger: one nested
subsetrow atfield/inlineColumnsinmetadata-form-zod-reconciliation.test.ts. Its shape is theobject/fieldssubset row at the top of the ledger (and its two depth-two childrenfields.options,fields.summaryOperations), which is the ledger'ssubsetprecedent. The other three keys need no row: every key they declare is offered.Row titles: the two new repeaters' row schemas carry a JSON Schema
titleon every property (Row titles below).Faces that needed a reason
activityMilestones.fieldpinswidget: 'text'. Read at the.objectui-shapin onmain,dd3f7e1b: with nowidget,SchemaForm'sresolveFieldWidgetruns its name conventions, anddetectFieldRefWidgetturns a string property namedfieldinto thefield-refpicker wheneverwidgetContext.objectFieldsis present.ResourceEditPagealways hands that over as a load state, and on an object draft it isidle(the draft names noobject/objectName/data.object/interfaceConfig.source).FieldRefWidgetthen renders a select offering only "None", so a new milestone could not name its field. An explicitwidgetskips the conventions, andtextis a passthrough hint, so the face is a plain input.redactFieldspinswidget: 'string-tags'for the same reason: a string list named...Fieldsbecomesfield-multiby the same convention.userActions.create/import/edit/deletetakewidget: 'json', the ruling's union rule (「Union-typed values takejson」). Each is a boolean or a strict{ enabled, visibleWhen, disabledWhen }object. At the pin,jsonis a passthrough hint:resolveFieldFacepicks the stored value's union branch. A new entry or a stored boolean renders the switch (the first arm), and a stored object renders its three keys as a nested form, whosesetFieldmerges each edit into it. No face writes one arm over the other. The object arm is written in source and edited here once stored.allowedAudiences/allowedPermissionstakewidget: 'multiselect'with inline options. Every member is a spellable option value.MultiSelectWidgetwritesundefinedwhen every choice is cleared, so the form cannot store the empty listgetPolicyreads as "any audience".dd3f7e1b(G2a read the repeater face atf8a9d0fb): the declared composite (CompositeField,pickSubSchemareadingproperties[NAME]afterinlineSchemaRefs), the multiselect widget, the switch and number branches of the scalar chain, and thejsonhint on a union sub-row. Code readings only, no browser run.inlineColumns: the curated subsetname, plus the three keys that apply to a column of any type,label,widthanddefaultHidden. An entry that names only a field is what the key's own describe recommends, because objectui'shydrateColumnscompletes it from the child field.subsetrow):type: declaring it opts the column out of that hydration.options,reference,displayField,idField,autofill,multiple,accept,prefix,step,scale,computed,expr: each applies to one cell type only. A column takes its type from the child field at render, and no sub-rowvisibleWhenhere can see it, so each would be offered on every column.required,readonlyWhen,requiredWhen: hydration copies them from the child field, where the rule the server enforces lives.zodOnlyforfield.inlineColumnsis exactly those sixteen keys.Where a misspelt field name is refused, read from the code
The ruling's 「a misspelling is refused loudly at parse」 does not hold for three of this flight's four name positions. Each help text claims only what is measured.
os validatepublicSharing.redactFields[]validate-object-field-refsowns it aterror(runtimeTypesincludesobject); probe belowactivityMilestones[].fieldvalidate-object-field-refsleaves it out by name; probe belowmatchMilestonecomparesafter[field] === value, so the milestone never fires{token}inactivityMilestones[].summaryrenderMilestoneSummaryrenders it emptyinlineColumns[].namehydrateColumnsleaves an unknown name unhydrated, a plain text columnSeat 2's #20479 (for #20432), which landed on
mainas4b2d9041during this flight, extendsvalidate-object-field-refsto four field-level lists andindexes[].fields. Read onorigin/main, its list positions still do not includeactivityMilestones[].fieldorinlineColumns[].name, so these texts stay true (Out-of-scope finding below).Other help-text claims, each read from its consumer
activityMilestones: an update that moves the field into the value writes the summary in place of the field-change entry, and the first match wins (audit-writers.tsmatchMilestone). The comparison is strict, andvalueis a string, so a milestone on a number or boolean field never fires. A lookup, master-detail or user token shows the referenced title (REFERENCE_FIELD_TYPES). An unsettypeisupdated: the update branch starts fromactivityTypeFor('update')and a milestone replaces it only when it names one. (The schema's describe says the default is "completed"; see Acceptance notes.)publicSharing(share-link-service.ts):enabledis re-read on every redemption; unset audiences default to['link_only']and permissions to['view'];createLinkrefuses any other with 422. Every audience still needs the token:resolveTokenadds a signed-in check forsigned_inand an allowlist check foremail.maxExpiryDaysdefaults to 365, and a link created without an expiry is stored with none (expiresAt ?? null), so the cap does not force one.eligibilitybindsrecord, is checked at mint and at every redemption, and a predicate that does not compile or faults refuses.userActions(resolveCrudAffordances): the per-bucket defaults in the help text areCRUD_AFFORDANCE_DEFAULTSverbatim. On anengine-ownedorappend-onlyobject, turning a verb on also passes plugin-security'sassertEngineOwnedWriteAllowed, so users can make that write through the data API.inlineColumns: read only when the field setsinlineEdit(attachInlineSubforms). Unset,deriveColumnscurates past six columns into the column chooser.defaultHiddennever hides a required column (GridField:c.defaultHidden && !c.required).Row titles (admitted by the claim from the start)
repeater-item-titles.test.ts(spec: every repeater item schema except dashboard header.actions still has no JSON Schema title, so 21 property-panel tables render machine keys in every locale #17232) requires a JSON Schematitleon every authorable property of every repeater's row schema, and forbids a ledger entry. Both new repeaters are new carriers:object:activityMilestonesandfield:inlineColumns..meta({ title })calls, and nothing else in either file:object.zod.ts, theactivityMilestonesentry:field'Field',value'Value',summary'Summary',type'Type'.field.zod.ts,InlineGridColumnSchema, all twenty properties: Name, Label, Type, Width, Required, Options, Prefix, Step, Reference, Display Field, ID Field, Multiple, Accept, Default Hidden, Computed, Expression, Scale, Autofill, Read-only When, Required When.object.zod.tssha256 prefix8979b5feea7ed0ffboth ways (4 removed),field.zod.ts24713de3b50d5f71both ways (20 removed).scripts/ablation-replace.mjsin wrap mode, on the committed state. Deleting theSummarytitle readsobject:activityMilestones … expected [ 'summary' ] to deeply equal [], 1 failed of 29. Deleting theDefault Hiddentitle reads the same forfield:inlineColumnswith[ 'defaultHidden' ]. The tool proved each mutation landed (anchor 1 → 0, blob changed) and each restore (blob equals HEAD,git diff HEADempty).check:generatedreads all 15 artifacts up to date on this head,check:authorable-surfaceandcheck:api-surfaceincluded.Residue of the reconciliation gate (dispatch assumption 1)
The test file's own helper block was copied verbatim into a probe that was never committed, and run with the gate's own functions. At base it is lines 1-838, sha256 prefix
5e04d44fc5c5edb3, the prefix G2a read. On this branch it is lines 1-851, and it differs from the base block only by the 13 inserted ledger lines. Residue = offerable root keys − offered − rootomitrows, per type, withviewapart.Controls, asserted inside the probe: lit,
nameis offered by 17 of 17 forms; dark,object.zzFabricated19332G2bandobject.nameare in no residue.e956924e82c5b111, forms and ledger as on the head)Removed:
object.activityMilestones,object.publicSharing,object.userActions,field.inlineColumns. Added: none.Nested reading on the branch, through the gate's own
reconcileNestedLists:field.inlineColumnsreadszodOnly = []with thesubsetrow, and without itzodOnly=accept, autofill, computed, displayField, expr, idField, multiple, options, prefix, readonlyWhen, reference, required, requiredWhen, scale, step, type.object.activityMilestones,object.publicSharingandobject.userActionsreadformOnly = [] · retired = [] · zodOnly = []with or without any row of their own.Pins moved (measured, mechanical)
object-collapsed-sections-echo-decisions.test.tsadvancedobject-lifecycle-panel-echo-decisions.test.ts.labelcontrol, per localefield-panel-echo-decisions.test.ts['options']→['options', 'inlineColumns']inlineColumnsrepeater and its four children, all translatedpackages/lint/src/validate-predicate-path-refs.test.ts(admitted by the claim's 17:27Z amendment)field :: inlineColumnsondata.type == 'master_detail'. Measured, not inferred: the shipped corpus, keyedFORM::FIELD::SOURCE, was enumerated at the merge basee956924e(81 predicates, 56 comparisons) and on this branch (82, 57), and the difference is exactly that one entry added and none removedVerification
Test runs went through
scripts/pm/os-verify-lock.sh. The table is the first round's, at66b73be5, and the lint row is the patch round's, at16037890. After the merge and the G2a text correction, these re-ran at the final head2bcad436:pnpm --filter @objectstack/spec testTest Files 572 passed (572)·Tests 16791 passed \| 1 todo (16792);pnpm --filter @objectstack/platform-objects testTest Files 55 passed (55)·Tests 911 passed (911)(the text change moves no pin); lintvalidate-predicate-path-refs.test.ts+validate-object-field-refs.test.ts2 files, 114 passed;pnpm check:i18nOK (9 packages in sync) after the three translated leaves were authored and a second--writeleft no source-hash row;pnpm --filter @objectstack/spec check:generatedAll 15 generated artifacts are up to date.pnpm --filter @objectstack/spec testTest Files 569 passed (569)·Tests 16698 passed | 1 todo (16699)pnpm --filter @objectstack/spec test:repoTest Files 38 passed (38)·Tests 690 passed (690)repeater-item-titles.test.ts+metadata-form-zod-reconciliation.test.tsrepeater-item-titles.test.ts29 +metadata-form-zod-reconciliation.test.ts57:Test Files 2 passed (2)·Tests 86 passed (86)pnpm --filter @objectstack/platform-objects testTest Files 55 passed (55)·Tests 911 passed (911)(before the pin moves: 4 failed, the three pins above)pnpm --filter @objectstack/spec typecheck/ platform-objectstypecheckcheck:test-typecheck: OK(53 file(s) / 251 error(s) / 138 pinned; 1 / 3 / 2)pnpm check:i18ncheck-i18n-bundles: OK (9 package(s) — all bundles in sync, no undeclared authoring keys)pnpm --filter @objectstack/spec check:generatedAll 15 generated artifacts are up to datesrc/protocol.meta-types-*.test.tstest/i18n-coverage.test.ts,test/i18n-duplicate-demand.test.tssrc/validate-predicate-path-refs.test.tsat16037890Test Files 1 passed (1)·Tests 54 passed (54)(2 failed before the pin move, the two pins above)Catalogues:
node scripts/check-i18n-bundles.mjs --writeregenerated the 46enleaves (23 rows, a label and a help text each). The 138 translated leaves were then authored in zh-CN, ja-JP and es-ES, with noenecho. A second--writekept every translated value and left no source-hash row.Gates:
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 87 commands at the final head2bcad436(change set vs merge base9801da12, after the mergee809f0bd: the 14 files of this diff). That is the first round's 86 pluscheck:docs-transcript-drift, which the lint test file brings in. All 87 ran on that head, and each exit code went to disk before it was read. In every roundpnpm check:dual-build-cjs-loadsfirst exited 3 (PREREQUISITE NOT MET: nine packages had nodist/in the fresh worktree); later gates in the same run built them, and the rerun exited 0 (104 published require entry point(s) across 66 package(s) load), which is the coderan.listrecords.--ranreports:87 derived famil(ies) accounted for — 87 run, 0 NOT-MEASURED (a DERIVED zero — all 87 recorded an exit code and none of them is 3).Reach probe (built
@objectstack/specand@objectstack/lintof this tree, never committed): an object withactivityMilestones: [{ field: 'statsu', … summary: 'Done: {titel}' }]andpublicSharing.redactFields: ['titel'], and a childmaster_detailfield withinlineColumns: [{ name: 'quantiy' }].ObjectSchema.safeParseandFieldSchema.safeParseboth succeed. The reference-integrity suite, which the publish door andos validaterun, returns exactly one finding,object-field-ref-unknown @ objects[0].publicSharing.redactFields[0](the lit control), and none for the milestone field, the token or the column.Acceptance notes
activityMilestones[].type's describe says the default is "completed". The runtime writesupdated: the update branch'sactivityTypeFor('update'), replaced only by a milestone that names a type. The help text states the runtime. The showcase milestone namestype: 'completed'explicitly, so no measured author relies on the describe. Carrier: none.publicaudience's TSDoc (object.zod.ts) says "search engines may index; no token check".resolveTokenhas no branch forpublic: it redeems likelink_only, token required. The option label says only "Public", and the help text says every audience needs the link. Carrier: none.maxExpiryDaysdoes not force an expiry. A link created without one never expires. That matches the key's describe ("Reject links with expiry beyond this many days"), and the help text says it outright. Whether a capped object should require an expiry is a product question. Carrier: none.userActionsswitch reads off even where themanagedBydefault offers the entry, a switch having no unset state. The composite's help text names the defaults. Carrier: none.fieldmeet the samefield-refconvention.lifecycle.ttl.fieldhastype: 'text'and nowidget, so by the reading above it renders the "None"-only picker on an object draft. This is a code reading atdd3f7e1b, not browser-run, and it is outside this flight's rows. (fields.summaryOperations.fieldsits inside thefieldsrow, which the Studio object page hides as canvas-owned.) Carrier: none.origin/mainwas merged once, withscripts/pm/os-regen-merge.sh, at9801da12(e809f0bd), because spec(ui)+objectui: declare the console's round-trip keys on the stored view overlay (#20051 stage ii, ruling 甲) #20456'se967cbd2edited threeviewwhytexts in the reconciliation ledger. It merged without conflict,main's side was taken for every generated artifact it moved, andcheck:generatedthen read all 15 up to date.origin/mainhas moved since (to3062e500), not onto a file of this diff. Seat 2's fix(spec): hook condition row declares expression, not javascript #20475 regeneratesen.metadata-forms.generated.tstoo and is not onmainyet: ordinary concurrency.indexes.fieldshelp text went stale when fix(lint)!: object-field-ref-unknown judges a field's relatedListColumns, lookupColumns, lookupFilters and dependsOn, and indexes[].fields #20479 landed, and is corrected here (Status, second paragraph), under the claim's 17:51Z amendment. No other G2a row changes.Out-of-scope finding (folded into #20432 by the seat; not filed by this run)
activityMilestones[].fieldandinlineColumns[].namename fields of the owning object, and no authoring door judges them. A misspelt milestone field silently never fires, and a misspelt column renders as plain text.validate-object-field-refsleaves the first out by name. Its extension fix(lint)!: object-field-ref-unknown judges a field's relatedListColumns, lookupColumns, lookupFilters and dependsOn, and indexes[].fields #20479, landed as4b2d9041, reaches four field-level lists andindexes[].fields, but neither of these.activityMilestones field unknown·inlineColumns name unknown field·milestone never fires misspelt field·inline grid column reference integrity.Generated by Claude Code