Repository navigation
fix(app-shell): Studio's new rules, hooks and actions start from safe defaults (objectui#11820) - #11918
Merged
objectstack-fleet[bot] merged 1 commit intoOct 8, 2026
Conversation
… defaults (objectui#11820) - Validation rules: a new Script / Cross-field / Conditional rule stays out of the object draft until it has a condition (a Conditional, its Then rule's too), listed and marked not saved with the hold's line and input hint; every new rule starts on Create + Update. An existing rule's type switch still seeds the valid 'false' placeholder. - Hooks: the target picker splits this package's objects (the package's own list, published and draft, provided by the hooks panel) from every other object, which sits under "Other objects" with a reach warning; "All objects" carries one too. A new hook still targets the object it is made from. - Actions: a new action is a declarative field update (operation 'update', empty patch, type left to the spec default) instead of a sandboxed-JS script. Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8 Co-authored-by: Claude <noreply@anthropic.com>
Contributor
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
objectstack-fleet
Bot
deleted the
claude/issue-11820-studio-safe-defaults
branch
October 8, 2026 07:02
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #11820
Clause-②: no
Studio's create flows for validation rules, hooks and actions start from safe defaults, as the triage grade's direction (option A) asks. Only what Studio offers and saves first changes; editing an existing rule, hook or action is unchanged.
What changes
ObjectValidationsPanel.tsx). A new Script, Cross-field or Conditional rule is no longer written to the object draft with the guard'false'. It is listed and open, marked "Not saved — needs a condition", with the line the Data pillar uses for an edit it holds (engine.studio.held.line, objectui#11786) and that hold's input hint under the guard. The first edit that gives it a condition writes it (a Conditional also waits for its Then rule's condition; its seeded Then rule shows an emptyconditionwhere it goes). Every new rule starts on Create + Update (events: ['insert', 'update']). A type with no guard (State machine, Format, JSON schema) is written at once, as before. A type switch on an EXISTING rule still seeds the valid'false'placeholder, because that rule is already in the draft.HookDefaultInspector.tsx,ObjectHooksPanel.tsx). A new hook already targeted the object it is made from (addHookseedsobject: objectNameon5e446ab); that is now pinned. The target picker is told which objects belong to this package (new module-internalHookTargetScopeContext, provided by the hooks panel fromloadPackageSurfaces(client, 'object', packageId), the published-plus-draft merge the pillars' rails use). It lists this package's objects under "This package" and every other object, other packages' and the platform's ownsys_*tables alike, under "Other objects" with a one-line reach warning. "All objects (*)" shows a reach warning when chosen. Choosing either deliberately still writes what it wrote before. The metadata admin's hook editor, which knows no package, keeps its flat list.ObjectActionsPanel.tsx). A new action is "Update fields on this record — no code":operation: 'update'with an emptypatch,typeleft to the spec default, no sandboxed-JS body.metadata-admin/i18n.ts(en and zh): 5 keys added (engine.studio.held.needsCondition,engine.studio.held.needsThenCondition,engine.studio.rules.notSaved,engine.inspector.hook.outsideReach,engine.inspector.hook.allObjectsReach; the group headings reuseengine.inspector.objectPicker.groupPackage/groupOther),engine.studio.rules.celMidreworded andengine.studio.rules.celPostremoved (the guard caption no longer says new rules default tofalse)..changeset/11820-studio-safe-defaults.md,patchon@object-ui/app-shell.No export, prop, type member or language-pack key reaches the package entry: after
pnpm --filter @object-ui/app-shell build,HookTargetScopeappears only indist/views/metadata-admin/inspectors/HookDefaultInspector.d.ts, and no other.d.tsindist/imports that file.Measurements behind the route (the dispatch's hypotheses)
@objectstack/spec17.7.0 declaresevents: z.array(z.enum(['insert', 'update'])).default(['insert', 'update']). Measured: a rule parsed withouteventscomes back with['insert', 'update'], so the card's "no Runs on event checked" rule did run on both; it was a no-op only because ofcondition: 'false'. The panel shares the Data pillar'suseDraftAutoSavethroughonPatch, but the objectui#11786 hold lives off this card's surface: its predicate isobjectHeldEditinmetadataError.ts, which holds a draft the object write guard refuses, and its "Show me" routes a field selection inStudioDesignSurface.tsx(a file this seat's objectui#11823 step 2 holds). A rule that has never been written is not an edit of that draft, so the panel holds it itself and reuses the hold's wording and input hint rather than adding a second vocabulary. Known cost against the pillar's hold, stated in the panel's header and the changeset: leaving the Rules view, or the object, before the rule has a condition drops it, and Publish does not wait for it.ObjectSchema.isSystem), but on objectstackmain3 of the 83sys_*objects in*.object.tsfiles do not declare it (sys_inbox_message,sys_file,sys_upload_session). The grouping therefore keys on package membership (the server's package filter on_packageId, plus the package's drafts), not on the flag and not on thesys_name prefix; everything outside the package, platform objects included, lands in the warned group. Raw camelCase event names (beforeInsert,afterUpdateMany): the designer string table has no label pattern for hook events, so no vocabulary is invented here (named under Acceptance notes).update_fieldis not anActionType; the declarative single-record write isoperation: 'update'+patchon the defaultscriptroute (objectstack issue 14092). Measured through the spec'sActionSchema:url,flow,modal,apiandformwithout atargetare each refused attarget;operation: 'update'withoutpatchis refused ("nothing to write");operation: 'update'withpatch: {}parses, withtypematerialised asscript. It is the only declarative skeleton valid without further input. The console'sActionRunneralready dispatchesoperationbeforetype.Tests
All runs on
54f36e2(the branch head), from the worktree root, throughos-verify-lock.sh:pnpm exec vitest run --maxWorkers=2 packages/app-shell/src/views/studio-design/—Test Files 122 passed (122),Tests 762 passed (762).pnpm exec vitest run --maxWorkers=2 packages/app-shell/src/views/metadata-admin/inspectors/plus the 12 test files outsidestudio-design/andmetadata-admin/inspectors/thatgit grepfinds naming a changed module or a changed string (ConditionWidget.conditionSubjects,ResourceEditPage.conditionScope,src/__tests__/select-placeholder-literal-11252, twodata-objectstackenvelope tests, threepackages/i18nkey tests, fourscripts/__tests__i18n / dts tests) —Test Files 155 passed (155),Tests 2394 passed | 1 skipped (2395); the skip is the pre-existing spec-gatedskipIfinflow-node-config.spec-reconciliation.test.ts.pnpm --filter @object-ui/app-shell type-check(tsc --noEmit && tsc -p tsconfig.test.json) — exit 0;--listFilesOnlyon the test project lists all six changed test files. Run on the working tree committed unchanged as54f36e2.pnpm exec eslinton the 11 changed.ts/.tsxfiles — 11 files judged (from--format json), 0 errors, 4 warnings: one new (react-refresh/only-export-componentson the exported context, the shapestudio-canvas-preview.tsxalready has) and three pre-existingreact-hooks/set-state-in-effect.check:control-bytes,check:test-path-roots,check:changeset-claims,check:pending-changeset-literals,check:i18n-keys,check:i18n-drift("Every changed designer-table en value was followed by its zh row."),check:i18n-dead-keys(no new key listed),check:i18n-designer-parity("Every en row has a zh row, and every shared row carries the same placeholders."),check:new-line-citations(0 new),check:action-forward-parity,check:designer-field-key-parity, andscripts/check-changeset-{presence,no-major,fixed,overwrite}.mjs.New pins:
ObjectValidationsPanel.newRuleWaits-11820.test.tsx,ObjectActionsPanel.newActionDefault-11820.test.tsx(newActionPin),HookDefaultInspector.reach-11820.test.tsx,ObjectHooksPanel.newHookTarget-11820.test.tsx. Rewritten: theObjectValidationsPanel.test.tsxcase that pinned the'false'default (it pinned exactly the branch removed here), andwhenKeyPin's New-menu cases, which now parse the rule the panel finally writes. Controls inside the pins: an existing rule edits and type-switches as before; a type with no guard is written at once; "All objects" chosen deliberately writes*; no package known means the flat list with no warning.Ablation (
node scripts/ablation-replace.mjsfrom objectstack, WRAP mode, run on54f36e2):addRule's wait was replaced by the old immediate write with the'false'guard (anchor 1 to 0, blob21757a5a4e64toda230345381a, marker count 1 on disk before the run). The three validation test files went red as expected:Tests 14 failed | 15 passed (29), every failure an assertion, none a crash (13AssertionError, 1 jest-domtoBeChecked; for exampleexpected "vi.fn()" to not be called at all, but actually been called 1 times). Restore proven by the tool: blob after restore21757a5a4e64equals the HEAD blob,git diff HEADempty.Acceptance notes
Security carrier — how the server binds an authored hook on a platform object or
*(objectstackmainatc6fe02d7, read only; triage routes it):packages/objectql/src/hook-binder.ts,bindHooksToEngine: every hook is registered on each objectnormalizeObjects(hook.object)names,'*'included, throughengine.registerHook(event, wrapped, { object, packageId, meta }). The only target it refuses is an empty one.packages/objectql/src/plugin.ts,ObjectQLPlugin.readAuthoredHookRowsandresyncAuthoredHooksNow: Studio-authored hook rows are read fromsys_metadataacross all organizations (its own comment: engine hooks fire process-wide) and bound under the package idmetadata-service.packages/spec/src/data/hook.zod.ts,refuseBodyOnStoredMetadataTarget, mirrored at bind bypackages/runtime/src/sandbox/body-runner.ts,hookBodyRunnerFactory(throughstoredMetadataBodyHookBindingRefusal): a hook carrying abodythat namessys_metadataorsys_metadata_historyis refused;'*'binds and its body is skipped for those two tables' events.sys_*object, anisSystemobject or another package's object as a target.hook-wrappers.tshas zero hits for organization / tenant /_packageId/isSystem(a grep zero; the same search hitshook-run-as.ts).Found on the way, not changed here (reported to the seat with evidence):
ActionDefaultInspector'scommitOperationclearsoperationandpatchand seeds no body, andpatchBodywrites{ source }withoutlanguage. Measured throughActionSchema: the switched action is refused atbody(a script needs a body or a target), and after typing a body it is refused atbody.languageuntil a language is picked explicitly. Pre-existing for any update action; this PR makes it the path for every author who wants a script action.Delete, which the spec'seventsenum refuses (ObjectSchema.safeParseanswersinvalid_valueatvalidations.0.events.N).eventsshows no "Runs on" box checked though the spec runs it on Create + Update, and ticking "Create" narrows it to Create only.ConditionBuilder) right above the "Required" hint on a new rule.type: 'script', so the Actions list shows a SCRIPT chip for it, while the new default (type left off) shows none; and the inspector's Type control still reads "Script — run an expression / sandboxed JS" (disabled, pinned) on an update action, by the objectui#7551 design.NOT MEASURED: browser verification of the three panels (not run this round; the pins drive the real components with the real spec); the rest of
@object-ui/app-shell's 1133 test files beyond the two directories and the grep-derived files above (pnpm testin CI runs them);check:readme-exports(exit 1 on 73 README imports whose packages'dist/index.d.tsis not built in this worktree, none of themapp-shell; the package's exports are unchanged).Authored in session
https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8(seatdomain:ui#3, claim comment 6053007704).Generated by Claude Code