Repository navigation
feat(spec): offer object.imageField in Studio's object form beside nameField - #21854
Conversation
The record picture pointer is live, so the reconciliation ledger's declared-not-enforced omit row for it is stale. The row goes, and the object form offers the key as a plain text row beside nameField, the same face nameField has. The parse (refuseNonPictureImageField) stays the one judge of the value. Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude <noreply@anthropic.com>
…e imageField row Regenerated with `node scripts/check-i18n-bundles.mjs --write`; the zh-CN, ja-JP and es-ES label and helpText leaves are authored, and a second extract dropped their source-hash provenance entries, so the source-hashes companions are byte-unchanged. Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude <noreply@anthropic.com>
… row moves The object form's open-section leaf population reads 114 -> 116 and the catalog-wide translated-label control 659 -> 660, both measured, with no echo introduced. Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude <noreply@anthropic.com>
…form row Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude <noreply@anthropic.com>
… tracker id Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 138 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 f4557641cd356d55a85d83df7a06356af149ad51 && git checkout f4557641cd356d55a85d83df7a06356af149ad51
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5b2d189e28d5563cbcaa84ac912219017692ea97 9f6b17787539e545969e24b2871a2e1af72a9988 && git checkout -B drift-repro 5b2d189e28d5563cbcaa84ac912219017692ea97 && git merge --no-ff 9f6b17787539e545969e24b2871a2e1af72a9988
node scripts/docs-audit/affected-docs.mjs --json 5b2d189e28d5563cbcaa84ac912219017692ea97
|
…rm; field.picklist joins the own-editor class (objectstack-ai#21901) Fixes objectstack-ai#21863 Clause-②: no This lands the director seat's ruling 1A / 2B on the card (record `5995552118`, maintainer 「同意」). Studio's action form now offers `onSuccess` and `outcomeMessages`, using the controls ruling `5861442317` already gave their shapes. The `field.picklist` ledger row moves into the ruled class "authored through its own editor". No Zod key, liveness row or gate logic changes. ## What changed - **The form rows** (`packages/spec/src/ui/action.form.ts`), in the Behavior section after `successMessage` / `errorMessage`: - `outcomeMessages` is one `widget: 'json'` row, spelled like the file's `patch` and `bodyExtra` rows (G1: a map-shaped value takes `json`). Its helpText says each key is a snake_case `outcome` value the handler returns, and each value is the label shown for that outcome. It also says what happens with no entry, and that a key the handler never returns is never shown. - `onSuccess` is one `composite` row (G2: an object-shaped value takes curated sub-rows), the face the `body` row above already uses. It has two sub-rows: - `navigate` is `type: 'text'`, `required: true`, because the Zod requires it inside the block. Its helpText names the route or URL template and its three interpolation scopes (`${param.*}`, `${ctx.*}`, `${result.*}`), taken from the key's describe. - `openIn` is `type: 'select'` with no inline `options`. The member `newTab` has a capital letter, and `FormSelectOptionSchema.value` is a lowercase system identifier, so an inline list cannot spell it. The select reads its two members (`self`, `newTab`) off the served schema instead. Their meanings are in the helpText, and nothing is retyped. - Both rows carry `visibleWhen: "data.type == 'api' || data.type == 'script'"`. These are the two types the parse accepts them on (the `onSuccess` type refinement and `refuseInertOutcomeMessages` in `action.zod.ts`), so the control is shown where the value is accepted. - No designed control and no new widget. - **The ledger** (`packages/spec/src/system/metadata-form-zod-reconciliation.test.ts`): - The `action.onSuccess` and `action.outcomeMessages` `omit` rows are deleted from the "Declared, not enforced yet" group. - **2B:** `field.picklist` is added to the "authored through its own editor" key list in `RULED_ROOT_REASONS`, and `RULED_EDITORS` gains `'field.picklist': { surface: "the object designer's shared-picklist picker" }`. - The picklist row moves into that class's group and its `why` is rewritten. It states the `live` verdict (the server resolves the named picklist onto the served field's `options` and judges a write against that set). It names the surface, and gives the class's citation with the placement ruling inside the same parenthetical: `(ruling record 5861442317, objectstack-ai#19332; the picker placed there by ruling record 5755653853, objectstack-ai#18164 batch objectstack-ai#209 item 1 A)`. - Comments only: the four comment blocks that described the class as holding exactly ruling `5861442317`'s keys now name `field.picklist` and the ruling that admitted it (`5995552118`). - `object.externalSharingModel` and `view.groups` are untouched. - **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 --filter=platform-objects`, following PR objectstack-ai#21854: - The first `--write` added the four new keys (`outcomeMessages`, `onSuccess`, `onSuccess.navigate`, `onSuccess.openIn`). The zh-CN / ja-JP / es-ES `label` and `helpText` leaves were then written in place. - A second `--write` kept those leaves and dropped the eight provenance entries per locale that the first extract had added to the `*.source-hashes.generated.ts` companions. - The three companions are byte-identical to `BASE`: blob `2decb02bf6f1` (es-ES), `a96e8eb503d5` (ja-JP) and `31882e433b83` (zh-CN), before and after. - **Three count pins in `platform-objects`, each re-taken with one comment line naming the rows:** - The translated row-label positive control in `object-lifecycle-panel-echo-decisions.test.ts`: 661 → 665 per locale (four new row labels). - In `action-body-panel-echo-decisions.test.ts`, the count of composite children the repeater pin does not walk: 5 → 7 (`onSuccess.navigate`, `onSuccess.openIn`). - In the same file, the set of their parents: `['body']` → `['body', 'onSuccess']`. The same file's echo rule ("no leaf on this panel reads its `en` source unless the ledger decided it is an echo") stays green, so all eight new leaves are authored in all three locales. - **Changeset**: `@objectstack/spec` patch and `@objectstack/platform-objects` patch, in one file (see What ships). ## Controls (reverse verification, from the committed state) Both legs ran against HEAD `a5106c1c78`, in a throwaway detached worktree at that commit, through `scripts/ablation-replace.mjs` in wrap mode. Each leg also had its own `trap` that restores from `HEAD`. Command: `pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/system/metadata-form-zod-reconciliation.test.ts`, through `scripts/pm/os-verify-lock.sh`. | state | reading | |---|---| | baseline (unmutated) | 1 file, **76 passed (76)** | | the `onSuccess` form row deleted (anchor x1 → x0, blob `da0842c71614` → `0c90358ff49f`) | **1 failed / 75 passed**: `action.(root): accepted by the Zod but unauthorable in the form — offer it, or add a root ledger entry that records why it is not offered: expected [ 'onSuccess' ] to deeply equal []`. Restored: blob `da0842c71614` == HEAD, `git diff HEAD` empty | | `field.picklist` removed from the class's key list (anchor x1 → x0, injected text x0 → x1 read on disk during the run, blob `c3bc2007ee93` → `aa5ba299af81`) | **1 failed / 75 passed**: `"authored through its own editor": the root rows giving this reason are not the keys ruling record 5861442317 put in it. A key the ruling did not name needs a ruling of its own; …: expected [ 'field.picklist', …(4) ] to deeply equal [ 'object.actions', …(3) ]`. Restored: blob `c3bc2007ee93` == HEAD, `git diff HEAD` empty, `git status --porcelain` 0 lines | The first run of leg 1 timed out in the lock queue (exit 99, never acquired). The tool restored the file then (blob == HEAD), and the leg was re-run once the lock freed; the reading above is the re-run. The test imports only relative `src` paths and the spec package has no workspace dependencies, so no `dist/` is involved in either leg. ## What ships Measured with `npm pack --dry-run --json --ignore-scripts`, with a positive control: - `@objectstack/spec`: `src/ui/action.form.ts` is not in `files[]` (0 entries; `src/ui/action.zod.ts`, the control, is 1). The form ships compiled, though. The new `onSuccess` and `outcomeMessages` helpTexts are each in 6 packed `dist/` files, and the control, `successMessage`'s helpText, is in 6. - `@objectstack/platform-objects`: the new en helpText is in 6 packed files, against 6 for the control. The zh-CN and ja-JP labels are in 6 each, matched as unicode-escaped text, and the es-ES label is in 6. The control, zh-CN `successMessage`, is in 6. Both packages publish the change, so `skip-changeset` does not apply. ## Verification (head `a5106c1c78`) All suites ran through `scripts/pm/os-verify-lock.sh`. - `pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2`: **618 files passed, 18450 tests passed, 1 todo**. - `pnpm --filter @objectstack/spec exec vitest run --project repo --maxWorkers=2`: **53 files passed, 902 tests passed**. - `pnpm --filter @objectstack/platform-objects exec vitest run --maxWorkers=2`: **59 files, 949 passed**. Run before the pins were re-taken, it gave exactly the three failures above (`expected 7 to be 5`, `expected [ 'body', 'onSuccess' ] to deeply equal [ 'body' ]`, `zh-CN positive control: expected 665 to be 661`). - `pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2`: **215 files passed, 3 skipped; 27906 tests passed, 19 skipped**. No served-schema count moved: the change adds no Zod key. - `pnpm --filter @objectstack/spec run typecheck`: exit 0. `tsc -p tsconfig.test.json --listFilesOnly` compiles both changed spec files. - `pnpm --filter @objectstack/platform-objects run typecheck`: exit 0. Its `tsconfig.test.json` compiles both changed test files. - `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`. The first attempt exited 3 (PREREQUISITE NOT MET, the example closure unbuilt), and the reading here is from after the build. - `pnpm --filter @objectstack/spec check:generated`: exit 0 (15 artifacts up to date). - **Gates:** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 87 commands from 9 paths against merge base `607463d73`. All 87 ran on `a5106c1c78`. `--ran` reconciliation: `87 derived famil(ies) accounted for — 87 run, 0 NOT-MEASURED`. `pnpm check:dual-build-cjs-loads` first exited 3 (PREREQUISITE NOT MET, no `dist/` for nine packages), and exited 0 after a full workspace build. The record keeps that last reading. - **eslint:** `--no-inline-config --format json` on the 8 changed TS files gave 8 files, 0 errors, 0 warnings. The population is `eslint.config.mjs:971` (`**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}`). The config never enables type-aware linting (`eslint.config.mjs:326-328`: no `parserOptions.project`), so the verdict on an untouched file cannot move. - **Patch round 1, at `bff24d6190`** (claim revisions `5999807822` and `6000249318`; one commit on `a5106c1c78`): the two red shards on `a5106c1c78` were this PR's own pins. - `Test Core (4/6)`: `packages/lint/src/validate-predicate-path-refs.test.ts` re-takes the shipped-corpus pins 83 → 85 (predicates) and 58 → 62 (`==` / `!=` literal comparisons), each with a history comment. Measured against merge base `607463d736` by `<form>::<field>::<source>`: added exactly `action :: outcomeMessages` and `action :: onSuccess`, both `data.type == 'api' || data.type == 'script'`; none removed. - `Test Core (2/6)`: `packages/cli/test/i18n-extract-outcome-messages.test.ts` leaves the `metadataForms.` subtree out of its `.outcomeMessages.` selection. The new row's `metadataForms.action.fields.outcomeMessages.{label,helpText,placeholder}` paths are form-row text, so the equality stays over the four action-outcome keys. Ablation control: with the clause made a no-op, the file reddens exactly as CI did (`expected [ …(7) ] to deeply equal [ …(4) ]`). - Local runs after a full build: `@objectstack/cli` 352 files, 4683 passed, 2 skipped; `@objectstack/lint` 119 files, 5629 passed; plus `objectql`, `service-automation`, `plugin-approvals`, `cloud-connection`, `http-conformance`, `plugin-dev`, `example-embed-objectql`, `connector-rest`, `connector-slack` and `rest`, all green. `dispatch-gates` derived 89 commands from 11 paths, and all 89 exited 0. ESLint 0 / 0 on the 2 edited files. No source line moves in `packages/lint` or `packages/cli`. ## Acceptance notes - **The picklist citation.** The gate requires every row of a ruled class to contain `5861442317` ("cite the ruling record that decided the reason"). The dispatch asked that the row name the placement ruling (`5755653853`). So the row carries both, in the one parenthetical the class's citation form uses. The two-ruling spelling follows the `object.listViews` row in the same class (`ruling record 5861442317, objectstack-ai#19332; per-arm view forms under the objectstack-ai#19330 ruling, letter A`). The ruling that admitted the key to the class (`5995552118`) is cited in code comments only. - **The picker does not exist yet.** At the objectui pin `0abd4f9f87`, `ObjectFieldInspector` has the inline option editor and no shared-picklist picker; the picker is objectui#10202 phase 2. The row's `why` therefore says the offer "was decided as that picker", not that the picker is there. Until it lands, `field.picklist` is authorable only in source. - **Renderer reading, not a browser run.** Read at the objectui pin, not run in a browser. `SchemaForm.tsx` resolves a composite sub-row's schema from the parent's `properties`. `select` is a passthrough widget, and with no `fieldSpec.options` the select falls back to the sub-schema's `enum`, so `openIn` should render `self` / `newTab`. The JSON Schema this head emits (`packages/spec/json-schema/ui/Action.json`) carries `onSuccess` as a plain object, with `navigate` required and `openIn.enum` equal to `self` and `newTab`, which is the shape that fallback reads. `json` is a passthrough widget too, so `outcomeMessages` takes the same face as `patch` and `bodyExtra`. NOT MEASURED: a browser run of either face. - **Surface, stated.** The claim named `action.form.ts`, the four catalogs, the reconciliation test, any `platform-objects` count pin, and a changeset. `action-body-panel-echo-decisions.test.ts` is the second `platform-objects` pin the new composite moves. It falls under the claim's "any `platform-objects` count pin" and under the cross-lane declaration `5998582591` on objectstack-ai#6367. No metadata-protocol pin moved. - `origin/main` gained one commit after the branch point (`87712ab823`, global search in `metadata-protocol`). It touches none of this PR's paths, so it was not merged. --- _Generated by [Claude Code](https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #21765
Clause-②: no
Item 3 of the card, under the director seat's ruling A (
5989738766): Studio's object form now offersimageField, the record's picture, as a plaintextrow besidenameField. Items 1 and 2 landed in PR #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 afternameField. It has the same face asnameField, 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.refuseNonPictureImageFieldat 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 isimageoravatar. Left empty, there is no record picture and no placeholder. The metadata form's text input writes no key when cleared (objectuiSchemaForm.tsxat the pin0abd4f9f8769: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): theimageFieldomitrow 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.tsfiles were regenerated withnode scripts/check-i18n-bundles.mjs --write. The zh-CN / ja-JP / es-ESlabelandhelpTextleaves were then written in place, since translated-locale values are hand-written in those files (AGENTS.md, Documentation Guardrails). A second--writekept them and dropped the six provenance entries the first extract had added to the three*.source-hashes.generated.tscompanions. 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 atextrow with no widget. It sits directly afternameField, in the same section, at the samecolSpan. 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-objectswere re-taken, both forced by the new row:object-collapsed-sections-echo-decisions.test.ts);object-lifecycle-panel-echo-decisions.test.ts).The
openEchoesreading stays at zero.Changeset:
@objectstack/specpatch and@objectstack/platform-objectspatch (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.mjsin wrap mode and atraprestore proven by blob equal to HEAD:c730a8c598)d867e358e5e5→a67cdaed2b22)object.(root): accepted by the Zod but unauthorable in the form … expected [ 'imageField' ] to deeply equal [], plus the two new pin tests. Restored: blobd867e358e5e5== HEAD,git diff HEADemptyobject.(root).imageField: the form offers it now — drop the ledger entry. Restored: blob2a7c49dca958== HEADThe 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-changesetdoes 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 isscripts/root-entry-type-nameability.pin.test.ts, which is gated byOS_EXPECT_ROOT_NAMEABILITY.pnpm --filter @objectstack/spec run typecheck: exit 0.tsc -p tsconfig.test.json --listFilesOnlycompilesobject.form.tsand 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 114andzh-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).node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 87 commands from 10 paths against the merge base8832655af. All 87 were run on9f6b177875.--ranreconciliation: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 afterpnpm --filter @objectstack/spec build, and the record keeps that last reading.--no-inline-config --format jsonon the 9 changed TS files: 9 files, 0 errors, 0 warnings. The population iseslint.config.mjs:971(**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}). Linting is not type-aware (noparserOptions.project), so untouched files' verdicts cannot move.Acceptance notes
object.form.ts, the reconciliation test, the four catalogs and a changeset. Three more files changed:metadata-form-declared-rows.pin.test.tsholds the pin the dispatch asked for (item 4).platform-objectsecho-decision tests carry the counts the row moves. Those count lines are the ones PR feat(spec): give the 45 declared-but-unoffered scalar metadata keys a form row each #19673 moved when it added rows to the same section. A reviewer who reads them as outside the surface can say so.platform-objectssource or object file moved, and no other catalog key changed.origin/maingained one commit after the branch point (2799155678, test titles inpackages/spec/src/data/). It touches none of this PR's paths.Generated by Claude Code