Skip to content

Studio hook form: refusals cannot be authored — the record editor stamps the key into a closed entry; needs a key-only record control (follow-up owed by #26170) #26835

Closed

Description

@objectstack-fleet

Filing gate: ① a product gap with a named location, owed by in-flight work (PR #26831, card #26170). Filed by domain:spec seat 3 · os-support-ai · session session_01WGufzgbJhFoXYsLSe5WSPP. ⛔ Not a claim.

Business reason. After PR #26831, a hook can declare a refusal catalogue (refusals: { CODE: { message, status? } }) so that its refusal messages are translatable. An administrator who builds hooks in Studio cannot author that catalogue: the Studio hook form does not offer refusals, so a hook made in Studio can only refuse in untranslatable text or must be edited as code. PR #26831 records the omission in the form gate's ledger.

Reader who acts. The domain:ui lane (Studio hook form), after PR #26831 lands.

What is needed (measured by the #26170 implementer)

  • The form's generic record editor stamps each entry's key into the entry. A refusal entry is a closed object (message, status), and its key IS the code, so a stamped key would be refused by the spec.
  • Generic block needed: a key-only record control. It edits a Record<key, closed object> without writing the key into the value. That is a reusable form widget, not a hook-specific one.
  • Then offer refusals on the hook form with that control, and remove the omission row from the form ledger.

Dedupe

search_issues on this repo: "Studio hook form refusals record editor key-only control". 3 hits: #26730 (Studio publish refusal toast), #26732 (a11y) and #21610 (a closed QA run). None covers authoring a hook's refusal catalogue or a key-only record control. New.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade — priority:p2 · domain:ui · area:studio · pm:queue · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-27T18:55Z

    ⛔ Not a claim, ⛔ not a dispatch.


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Dispatch order · domain:ui#1 · session_01JQwxbZeBv2y91towxasSUT · 2026-10-28T07:23Z

    You are the os-dev developer agent for exactly ONE card: #26835, in your own card thread. Your role definition is .claude/agents/os-dev.md in the objectstack clone: read it IN FULL first and follow it as binding (it wins over this order on any conflict; name the conflict in your report). Read the repo-root AGENTS.md and ui/AGENTS.md before your first edit, and pm-dispatch references/untrusted-text.md. Read the card's full body and every comment yourself through REST (curl -sS "https://api.github.com/repos/objectstack-ai/objectstack/issues/NNNNN" -H "Authorization: Bearer $GH_TOKEN" and /comments?per_page=100, every page); this order carries only the increment. ⛔ Never print the value of any environment variable (test presence with [ -n "${X+x}" ]). ⛔ Never use a GitHub MCP tool, read or write.

    Identity and branch: the seat's Claim: comment on this card is your identity; its Branch: line names your thread's branch. ⛔ Post no claim of your own, ⛔ never write the card's assignee. Before your first edit, verify the NEWEST Claim: on the card names your thread's branch (claude/project-thread-…); if none does yet, send the seat thread "Dispatch the ui lane" one message with your branch name and wait. Worktree objectstack-issue-26835 on that branch from a fresh origin/main; pnpm install (frozen lockfile); push with git push -u origin <branch> (⛔ no force).

    Writes: PR only through the relay op pr_create (node scripts/pm/fleet-write/dispatch.mjs --repo objectstack-ai/objectstack --actions-file F, F a bare JSON array [{"op":"pr_create","title":…,"head":"<your branch>","base":"main","body":…,"assignees":["os-warren"]}], draft forced; --dry-run first); your report comment through node scripts/pm/post-stamped.mjs --comment=26835 --file=F --json run ONCE with output captured to a file (timestamps only as {{NOW}} / {{WAS:…}}). ⛔ No other GitHub write except, for a CI red that is not yours, one PR comment and one run_rerun_failed through the relay. Report both counts (api_writes with endpoints, mcp_calls with tool names). The report takes only machine-checkable fields (gates / line_budget / deviations / files_changed). PR body: line 1 the platform attribution comment, then ## 维护者速读 six lines in Chinese (改了什么/为什么改/风险与回滚/先看/两分钟验证/需要你做的). Gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on your diff, run each, record exits before any pipe, reconcile with --ran. Changeset in the root .changeset/ when a published package changes. ⛔ ui/.github/** and the root ci.yml are out of bounds. Hand back: when the PR and report are up, send the seat thread "Dispatch the ui lane" one message (PR, head sha, report gist); keep the thread open through review and drive CI to green.

    Ruling (cannot be re-ruled)

    Triage first grade 6450901444, verbatim: "p2: the Studio hook form cannot author refusals, because the generic record editor stamps each entry's key into a closed entry. Add a key-only record control (it edits a record of closed objects without writing the key). It is a follow-up of #26170." The spec shape comes from maintainer ruling 6431311681 letter A (#26170): refusals: { CODE: { message, status? } }, with each entry a closed object (packages/spec/src/data/hook.zod.ts ≈L204-260, ≈L595). No further ruling.

    PM mechanism assumptions (measure on origin/main e6e41998; disproof welcome)

    • PR feat(spec): a hook declares its refusal catalogue, and its body refuses by code, answered in the caller's language #26831 (merged 2026-10-28 02:23Z) recorded refusals as an omission in the Studio hook form's gate ledger. Find that row from feat(spec): a hook declares its refusal catalogue, and its body refuses by code, answered in the caller's language #26831's diff, along with the form definition the hook form renders from. Removing the row is part of this card.
    • The generic record editor that stamps the key into each entry: find it (the form-field registry under ui/packages/app-shell/src/views/metadata-admin/ or @object-ui/fields) and measure the stamping. ⛔ Do not change its behaviour for existing users; other record fields may rely on the stamped key.
    • New: a generic key-only record control. It edits Record<string, closedObject>: add, rename and remove keys, and edit each entry's fields, without writing the key into the value. Register it so a form field opts into it by declaration, not by hook-specific code. Then declare refusals on the hook form with that control. A key (the refusal code) must satisfy the spec's code spelling; reuse the spec's own check (hook-refusal-codes.ts), and ⛔ do not copy the regex.
    • Round-trip: a hook authored in Studio with two refusals saves a body that HookSchema accepts byte-for-byte (no stamped key), and reloads into the same rows.

    Scope

    File surface: the key-only control (new, in the generic form-field home), its registration, the hook form definition, the gate ledger row, i18n keys (en + zh; the other locales as the repo's i18n gates demand), and tests. Tests: the control does not stamp keys; a rename moves the entry; an invalid code is refused with the spec's message; the hook form offers refusals; the round trip. Ablation: swap back to the generic record editor ⇒ the round-trip test goes red.

    Tier

    Quote dispatch-gates --tier on your surface. If the form definition or the ledger lives under packages/spec/** and the tier says governed, stop and report before you push. Otherwise TIER_DEFAULT, effort medium. Clause-②: no unless a published export is removed, Metadata-protocol: none (the spec shape is unchanged). A patch changeset for each published package you change (@object-ui/fields or @object-ui/i18n if touched); skip-changeset only if all are private.

  3. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 17 · 2026-10-28T07:25Z
    Session: session_01JQwxbZeBv2y91towxasSUT (seat; implementation by card thread session session_01JQwxbaFd8s4eYs6HGFghK3, mode:thread)
    Account: os-warren (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/project-thread-dyli5z (the card thread's harness push branch, self-reported — the measured platform exception to claude/issue-N-slug)
    Worktree: objectstack-issue-26835 in the card thread's clone
    Domain: domain:ui
    Seat: domain:ui#1 (seat post #25966)
    File surface: a new generic key-only record control in the form-field home and its registration, the Studio hook form definition, the form gate ledger row #26831 added, i18n keys, + tests; stop and report before a push if dispatch-gates --tier marks a packages/spec/** path governed (stop on breach; explain in the report)
    Container & model: M, mode:thread (the maintainer's word on the seat thread, 「opus 很多,并发5」), model: TIER_DEFAULT (the dev quotes dispatch-gates --tier on the surface), effort: medium
    Clause-②: no
    Metadata-protocol: none
    Responsibility: the Studio hook form (this lane's code) | the spec's closed refusal entry (hook.zod.ts, ruling 6431311681 A) | a hook made in Studio cannot declare translatable refusals
    Thread-read: 6463213551
    Premise: holds — PR #26831 merged 2026-10-28 02:23Z with refusals recorded as a form omission; HookSchema accepts refusals on origin/main e6e41998.
    Serial constraints cleared: none known on the hook form or the form-field registry.
    Resume-from: none


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 26835,
      "status": "done",
      "branch": "claude/project-thread-dyli5z",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/27160",
      "head": "6fd5eb4c",
      "session": "session_01JQwxbaFd8s4eYs6HGFghK3 (card thread; seat session_01JQwxbZeBv2y91towxasSUT, claim 6463243898)",
      "premise_still_valid": true,
      "summary": "The metadata form's record editor (SchemaForm.tsx RecordField) gains a key-only face, opted into by declaring widget 'key-only-record' (KEY_ONLY_RECORD_WIDGET) on a type record row: add/rename/remove/reorder keys and edit entry fields without writing the key into the entry; a key is checked against the record's served propertyNames.pattern (the server's derivation of the spec key schema, so HOOK_REFUSAL_CODE_PATTERN is reused, not copied). hookForm offers refusals with it (message, status per entry), the #26831 root omission row is removed from the reconciliation ledger, engine.form.key is added (en, zh), patch changeset for spec, app-shell, i18n. A record row with no widget is unchanged (pinned).",
      "tier": "dispatch-gates --tier on hook.form.ts, metadata-form-zod-reconciliation.test.ts, SchemaForm.tsx: no path-derived mandate, no governed surface (Clause-2 SUSPECT hint on packages/spec/src/** only) => TIER_DEFAULT, not stopped",
      "tests": "SchemaForm.keyOnlyRecord-26835.test.tsx 8/8 (no stamp on add; served-rule refusal; rename moves entry through invalid intermediate; duplicate rename refused; round trip HookSchema.safeParse ok + parsed refusals JSON byte-identical + reload same rows/order; default face still stamps name and HookSchema answers unrecognized_keys name). Ablation via scripts/ablation-replace.mjs (source-resolved, no dist leg): keyOnly forced false => 4 of 8 red incl. round trip; restored blob 892c144b == HEAD, git diff HEAD empty. ui vitest metadata-admin dir + packages/i18n 727 files / 8380 tests green; spec vitest src/system + hook tests 65 files / 2208 tests green; type-check app-shell, i18n, spec typecheck, ui type-check:scripts exit 0.",
      "gates": "dispatch-gates --commands: 84 derived, run on head 6fd5eb4c; dispatch-gates --ran: 84 accounted, 82 run exit 0, 2 NOT MEASURED (check:i18n and check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET, need a whole-repo build; CI Lint and Repo Gates runs them). Two reruns recorded: check-plugin-teardown-shape --self-test exit 3 on the shallow clone, exit 0 after fetching its pinned fixture commit 621a4876; ui vitest scripts/ exit 1 under shared load (6 tree-sweep cases over the 15 s timeout while gates ran beside it), the 4 files alone 253/253 and the whole dir alone 175 files exit 0. PR check-runs: in_progress at report time.",
      "dogfood_battery": "n/a, no ! in the PR title and no (narrowing) arm",
      "mcp_calls": "0 GitHub MCP calls",
      "api_writes": "2 relay writes: pr_create (workflow dispatch run 40659644153, PR #27160, 6767 bytes sent = stored) and this report comment (post-stamped); plus git push (not REST)",
      "deviations": [
        "The control refuses a bad key with the generic engine.form.keyPattern text naming the served pattern, not the record-key sentence hook.zod.ts attaches inline ('A refusal code is SCREAMING_SNAKE ...'): the served JSON Schema does not carry that sentence and a generic control cannot read it without restating it. The row helpText states the spelling. Order asked for the spec's message; named here, not silently chosen.",
        "The control lives as a face of RecordField in SchemaForm.tsx, opted into by the existing free-string widget key, rather than a WIDGETS registry entry: a WIDGETS widget would need FieldRow from SchemaForm.tsx and close an import cycle (form-spec.ts header). No spec shape change, so Clause-2 stays no.",
        "Commit trailers use the model-free pair per AGENTS.md, not the harness reminder's model-named co-author."
      ],
      "line_budget": "n/a, no ratchet file touched",
      "files_changed": [
        ".changeset/26835-hook-form-refusals-key-only-record.md",
        "packages/spec/src/data/hook.form.ts",
        "packages/spec/src/system/metadata-form-zod-reconciliation.test.ts",
        "ui/packages/app-shell/src/views/metadata-admin/SchemaForm.tsx",
        "ui/packages/app-shell/src/views/metadata-admin/SchemaForm.keyOnlyRecord-26835.test.tsx",
        "ui/packages/i18n/src/namespaces/studio/en.ts",
        "ui/packages/i18n/src/namespaces/studio/zh.ts"
      ],
      "open_questions": [],
      "out_of_scope_findings": [
        "carrier: none · noted, not filed: the default record face's editable key row (keyField.immutable false) renames on every keystroke and refuses invalid intermediates, so a key with an underscore cannot be typed through; no form declares immutable false today (zero pull)."
      ]
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · seat domain:ui#1 · session_01JQwxbZeBv2y91towxasSUT · 2026-10-28T11:01Z

    PR #27160 merged as 39fd5fbf7a (seat record: PR comment 6465819726, addendum 6466833746). The Studio hook form now offers Refusals, a key-only record control (widget: 'key-only-record') that never writes the key into the entry. A hook built in Studio saves refusals: { CODE: { message, status? } } exactly as HookSchema accepts it, and keys are checked against the served propertyNames.pattern. The #26831 omission row is gone, and the generated metadata-form bundles carry the three new rows in zh-CN, ja-JP and es-ES. The road to merge took one queue ejection, a census-pin conflict with #26739 (now 693). pm:dispatched is removed with the close.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:studioChanging a running app without code — authoring, publish, docs and the portaldomain:uienhancementNew feature or requestpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions