Skip to content

feat(spec): refuse the confirmText + params pair on ActionSchema, scoped by schema boundary - #7972

Merged
os-zhuang merged 5 commits into
mainfrom
claude/issue-7428-confirmtext-params-guard
Aug 12, 2026
Merged

os-zhuang merged 5 commits into
mainfrom
claude/issue-7428-confirmtext-params-guard

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Fixes #7428

An action that declares confirmText beside a non-empty params opens two sequential dialogs for one decision — the console runner awaits the confirm, then the param prompt, so the first dialog already reads as "the action ran" while nothing has been sent. #7278 (PR #7592) and #7309 (PR #7827) repaired the sites that shipped it. Repairing instances does not stop the next one being written; this PR is the structural half.

The guard

A .refine appended to ActionSchema's chain in packages/spec/src/ui/action.zod.ts, refusing confirmText + a non-empty params array at path: ['confirmText'] — the key that has to go, since pointing at params would tell the author to delete the inputs they need. The message names both keys, states the user-visible consequence, points at the remedy (description), warns off the remedy's trap (ai.description), and names the two shapes that stay legal.

Census evidence (the premise, re-measured — not assumed)

Re-ran the card's brace-balanced scan on origin/main @ 8d80e12 before writing a line of the guard, and again on the final merged tree. Predicate per the #7309 notes recorded on the card: object literals declaring both keys at their own depth, with params: [ non-empty — which is what separates real sites from the schema definition files (action.zod.ts, bulk-action.zod.ts, translation.zod.ts declare both keys) and the generated translation bundles (params is a record there, not an array).

scope result
ActionSchema sites, non-test 0
ActionSchema sites, including test files and every package 0
BulkActionDefSchema sites 4 — the examples/app-showcase defs, correct as-is

So the PM's mechanism premise holds: the ActionSchema census is already 0 after #7592 and #7827, and the refusal breaks nothing that ships today. The only hits anywhere in the repo are the four showcase bulkActionDefs (plus one bulk-action.test.ts fixture), all on the surface the guard is scoped away from.

The census had one blind spot, found after opening this PR — see "Docs" below.

Ruling provenance

The card left three design questions open. They were answered by the PM's claim comment of 2026-08-12T08:24Z and its correction minutes later, which in turn defers to the measured correction of 2026-08-11T17:29Z on the same thread. Quoted rather than paraphrased:

From the 17:29Z measured correction — the finding that scoped this guard:

Measured answer: there is a legitimate shape, and it is the bulk-action surface. The distinction is not "params all optional" as the card guessed — it is which schema renders the pair into how many dialogs. Encode the guard on the schema boundary, not on a heuristic about param optionality.

and its reasoning, which this PR's tests pin verbatim in substance:

(b) They are not the defect. Per the schema's own contract a bulk def renders one dialog: params are "inputs collected once before the run", confirmText is "shown above the affected-record summary", and decisively, a required param "blocks the Confirm button until a value is present." A param cannot block a button in a dialog that has not opened yet

From the PM's correction comment:

  • The 4 showcase sites are bulkActionDefs (BulkActionDefSchema) … they are correct, struck from the target set, and MUST NOT be migrated.
  • Acceptance is the ActionSchema census returns 0
  • Ruling 2 stands in its corrected form … the guard is encoded on the schema boundary — a refusal on ActionSchema's pair that structurally cannot fire on BulkActionDefSchema — plus a pinning test that the bulk pairing stays accepted (so a future "helpful" widening of the guard goes red). Not a param-optionality heuristic.
  • Ruling 1 (refuse severity) and 3 (no description requirement) unchanged.

All four are implemented as ruled: refuse severity, schema-boundary scoping with the bulk pinning test, no added description requirement, and no migration of the showcase sites.

Scoping is structural, not conditional

The refusal is on ActionSchema's refine chain. BulkActionDefSchema is a separate strictObject in bulk-action.zod.ts with its own superRefine, so no refinement here can reach it. InlineActionSchema .pick()s from the shared field factory rather than deriving from this chain, so it is out of reach too — and it deliberately does not pick description, so the #7278 remedy has no slot on that surface to move a question into. That is the same argument the 17:29Z correction used to strike the bulk defs, which is why inline is recorded as an open question rather than quietly swept in.

action-confirm-params-guard.test.ts (12 tests) pins both directions:

  • Refusal — the pair; the localized-map form of confirmText (a truthiness check written against a string would miss it); the path; and the message substance, asserted phrase by phrase rather than as success === false.
  • Non-refusal — a param-LESS action; params: []; params + description (the shape approvals: sys_approval_request.approval_reject / approval_recall declare both confirmText and params, so one decision opens two sequential dialogs #7278 migrated to — if this goes red the guard has swallowed its own remedy); and params with no description, pinning ruling 3.
  • BoundaryBulkActionDefSchema still accepts confirmText + non-empty params, with the showcase's own set_labels literal; that the two schemas are independent; and InlineActionSchema's current acceptance, with the reason recorded.

One test changed shape against a measurement. I had assumed aliases (confirmconfirmText, inputsparams) were folded before the refinements, and wrote a test asserting the guard fires on the aliased pair. It does not — this repo rejects a near-miss with a rename arrow rather than folding it (Prime Directive #12), so the aliased pair is refused one layer earlier by key recognition and never reaches the refinement. The test now pins that actual mechanism, since the guard's coverage claim depends on it.

Reverse verification

Guard committed first, then reverted alone (tests kept), per the discipline that a restore point must actually exist.

  • Predicted: 5 red — the 4 guard-dependent assertions plus the defineAction throw.
  • Measured: 4 redrefuses the pair, says WHY, localized-map form, throws from defineAction. All 8 acceptance/boundary tests stayed green, as they must: they assert success === true, which a schema with no guard satisfies trivially.
  • The miss is informative, not noise. The alias test stayed green because the alias rejection is upstream of the guard and does not depend on it — which is exactly the mechanism the corrected test now documents. My prediction had miscounted it as guard-dependent.
  • Restored via git checkout <branch> -- <path> from the commit; git diff HEAD empty (byte-identical, not a matching line count), 12/12 green again.

Docs — a stale example the census could not see

The docs-drift bot's list pointed at content/docs/protocol/objectui/actions.mdx, and two of its "Real-World Examples"disable_oauth_application and rotate_client_secret — declared confirmText beside params. Both were already stale against main: the real actions moved the question to description in #7827. This PR makes the documented shape fail to parse, so a doc that was merely out of date became a doc that teaches metadata the spec refuses.

Fixed in b5b4ff8: the two examples now mirror what sys-oauth-application.object.ts actually ships, and the confirmText bullet above them states the constraint (param-LESS only; question on description; not ai.description; bulk defs unaffected).

Why the census missed them: those examples are YAML, and the card's acceptance scan is brace-balanced — it can only see TS/TS-like object literals. A YAML-aware re-scan across all content/docs/** found exactly these two and nothing else. Worth knowing if the census is ever re-run as a gate: it measures source declarations, not documented ones.

Gates

All run locally, foreground, on the merged tree.

gate result
@objectstack/spec full suite ✅ 382 files / 10045 tests
@objectstack/spec typecheck (incl. check:test-typecheck)
check:generated (all 13 artifacts) ✅ all up to date
check:authorable-surface ✅ green (anchor untouched)
check:adr-anchors
check:changeset-gate-self-tests
@objectstack/lint check:doc-formula-expressions ✅ (re-run after the docs edit)
check:docs-audit-scope ✅ (re-run after the docs edit)
check:driver-conformance
check:i18n ✅ 9 packages in sync
check:merge-driver
check:release-body
check:spec-parsed-alias
check:stack-collection-maps
check:nul-bytes ✅ (re-run after the docs edit)

Downstream blast radius, since a refusal is only safe if consumers still parse: platform-objects 342 ✅, plugin-approvals 458 ✅, plugin-security 1001 ✅, example-showcase 193 ✅ — the last being the live proof the four bulk defs still validate.

Two gate results needed a prerequisite rather than a fix, both stale-build-state per the AGENTS.md §9 table, both cleared by building rather than asserted away: check:i18n needed the built CLI, and examples/app-showcase typecheck reported four Cannot find module '@objectstack/connector-*' errors until those packages were built (then clean).

⚠️ Regenerated baseline — this joins the spec serial relay

The diff regenerates content/docs/references/ui/action.mdx (via gen:docs), because the confirmText .describe() now names the constraint so an author meets it in the reference and not only in the error.

Worth a reviewer's eye on the wording: that one field factory renders both the ActionSchema table and the InlineAction table, where the guard does not apply. My first draft said "pairing it with a non-empty params is REFUSED" unqualified, which would have published a documented refusal that does not exist on the inline surface — a declared≠enforced inversion. It reads "on a registered action" for that reason.

No other generated artifact moved; authorable-surface is unchanged (a refinement adds no authorable key), and the anchor was never hand-edited.

Changeset

@objectstack/spec minor — an acceptance narrowing is user-visible. It carries the migration diff, the ai.description trap, and each of the four shapes deliberately left accepted.

Out of scope

packages/metadata-protocol/src/protocol.ts carries a hand-written JSON-Schema mirror of the action shape that declares confirmText and params with no equivalent constraint. Not touched here — a second, independently-maintained contract is a finding of its own rather than a rider on this guard.


Generated by Claude Code

claude added 3 commits August 12, 2026 08:37
An action declaring both opens TWO sequential dialogs for one decision —
the console runner awaits the confirm, then the param prompt, so the first
already reads as "the action ran" while nothing has been sent.

#7278 and #7309 repaired the 16 shipped sites (PRs #7592, #7827); neither
stops the next one being written. This is the structural half: a refusal on
ActionSchema whose message names both keys and points the confirm question
at the action's top-level `description`.

Scoped by schema boundary, not by a param-optionality heuristic. The pair is
INTENDED on `BulkActionDefSchema`, where params and confirm render one dialog
(a `required` param blocks that dialog's own Confirm button), so the four
`examples/app-showcase` bulk defs are correct as written. The refusal lives on
ActionSchema's refine chain and is structurally incapable of reaching either
`BulkActionDefSchema` or `InlineActionSchema`; both directions are pinned.
…geset

The `confirmText` describe() now names the refusal, so an author meets the
constraint in the generated reference rather than only in the error. Worded
"on a registered action" deliberately: the same field factory renders the
InlineAction reference table, where the guard does not apply — an unqualified
claim there would be a documented refusal that does not exist.

Regenerates content/docs/references/ui/action.mdx (gen:docs).
@vercel

vercel Bot commented Aug 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 12, 2026 11:11am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec.

106 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/tenancy-modes.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/spec)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/permissions/system-context.mdx (via packages/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/apps.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/field-grouping-and-order.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

7 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx (via @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests protocol:ui tooling labels Aug 12, 2026
claude added 2 commits August 12, 2026 09:16
`content/docs/protocol/objectui/actions.mdx` showed `disable_oauth_application`
and `rotate_client_secret` pairing `confirmText` with `params`. Both were
already stale — the real actions moved the question to `description` in #7827 —
and the guard in this PR makes the documented shape fail to parse.

They are labelled "Real-World Examples", so they now mirror what the platform
objects actually ship, and the `confirmText` bullet above them states the
constraint: param-LESS actions only, question on `description` otherwise, not
`ai.description`, and bulk defs unaffected.

Found via the PR's docs-drift list; the brace-balanced census could not see
these because the examples are YAML.
@os-zhuang
os-zhuang marked this pull request as ready for review August 12, 2026 12:43
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 12, 2026
Merged via the queue into main with commit 333a374 Aug 12, 2026
27 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-7428-confirmtext-params-guard branch August 12, 2026 12:59
os-zhuang pushed a commit that referenced this pull request Aug 12, 2026
…ged with batch B

Second turn of the os-regen relay. `#7985` (batch B — the memory driver's five
persistence sub-shapes) and `#7972` landed while this branch was in the queue, so
the shared counts artifact met two batches that each decremented it correctly and
independently.

That is the exact arithmetic #5107 built this artifact to prevent: the rows do
not overlap, git merges them without complaint, and the SUBTOTAL — which overlaps
nothing — merges clean and wrong. The artifact carries `merge=os-regen` so the
merge defers rather than splices, and the only correct resolution is to recompute
from the merged tree. Regenerated, never hand-edited:

    still-open (strip)          149 → 144   (batch B's five)
    files carrying at least one  26 → 25
    authorable — forced scope      9 → 4
    data/ strip                 107 → 102,  data/ strict 57 → 62
    total strict                283 → 288

Neither batch's own numbers moved; the combined ones did. `check:strictness-ledger`
agrees with the merged tree in both directions (25 open files / 144 strip sites,
no closed file still carrying a worklist row — batch B's `driver/memory.zod.ts`
row left with their side of the prose).

Merged tree verified: spec 382 files / 10109 tests, lint 71 / 1932,
check:generated 13/13.

Part of #4001

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012NLJ8PWLdwLSyi84LHzrAx
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 17, 2026
…keys (objectstack-ai#4001 batch A) (objectstack-ai#8008)

* feat(spec): close the 31 SDUI component-props shapes against unknown keys (objectstack-ai#4001 batch A)

`ComponentPropsMap` — the declared shape of every `page:*`, `record:*`,
`element:*`, `nav:*` and `ai:*` node a page can carry — stripped unknown keys
in silence. All 31 object sites in `ui/component.zod.ts` are `strictObject` now,
including the two union arms (`RecordHighlightsField`'s object arm and
`record:related_list`'s sort entry) and every nested block, since strictness
does not recurse.

objectstack-ai#5068 already REPORTED these keys, by walking a strip-mode object and
reconstructing what the parse would have dropped. Now the parse rejects the key
itself — same rule id, same warning tier, and three things the reconstruction
could not give: curated per-surface prescriptions, a rejection that holds for
every caller rather than only inside the gate, and coverage of union arms.

Five renderer-honoured props are DECLARED rather than rejected, on the rule this
file has applied three times before (objectstack-ai#5611/objectstack-ai#5775/objectstack-ai#6276 — the delivered,
authorized shape is the contract): `page:header` `maxVisible`/`mobileMaxVisible`,
`page:tabs.alwaysShowStrip`, `record:details` `inlineEdit`/`showHeader`. Each is
read by objectui through `schema?.X ?? schema?.properties?.X` with its own
comment inviting authors. All optional with no schema default — the defaults are
the renderer's, and declaring them would turn an unset key into an authored one.

Curated tables, each backed by a producer measured in the wild rather than
guessed: a tab item's `key` → `value` (objectui's Studio designer publishes
`key`; the renderer reads `it.value`), a header's `description` → `subtitle`
(the rename its own ADR-0087 conversion performs — the one path that had no
diagnostic at all), a container's `body` → `children`, and a wrong-layer family
for keys that belong on the component NODE.

`@objectstack/lint` gains one piece of wiring: zod 4 collapses union-arm
failures into a single `invalid_union`, so a lone arm's `unrecognized_keys` is
unpacked back onto the unknown-key rule id — and deliberately is not when two
arms could both have been meant.

Deliberately unchanged: the carrier is still `z.record(z.string(), z.unknown())`
(direction B stays declined, unregistered `record:*` types are still skipped),
the storage path still parses no props (objectstack-ai#4463), and the gate is still warning
level.

Verified with direct build-artifact `.parse()` probes over the example corpus —
`objectstack validate` never parses through `PageSchema` (objectstack-ai#5000), so "the
examples validate clean" would have been no evidence. 244 registered props bags
across the three example apps' build artifacts, the same three from source, and
the three published platform pages: zero undeclared keys, zero new refusals. The
probe's negative control moved from 0/244 injections caught to 244/244.

Filed out of scope: objectstack-ai#7973 (objectui's Studio designer publishes three page
component inputs no renderer reads).

Part of objectstack-ai#4001

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012NLJ8PWLdwLSyi84LHzrAx

* docs(ui): stop teaching `record:highlights.actions` — nothing has ever rendered it

Found by the objectstack-ai#3746-style corpus scan this batch owes: every `content/docs/**`
and `skills/**` code block carrying a registered component type was parsed and
its props judged against the now-closed `ComponentPropsMap`. One hit, in the
"Complete Example" of the pages guide — an `os:check` block, so it is an example
the docs gate compiles and an AI author copies.

`RecordHighlightsProps` has never declared `actions`, and objectui's
`record-highlights.tsx` reads `schema.fields` and nothing else. Under strip the
key was dropped in silence, which is precisely why a doc could teach it for this
long: the example parsed clean and rendered without the actions, and no layer
disagreed. With the shape closed it is a rejection, so the doc had to be right
before the batch could land.

The actions move to `record:quick_actions`, which is the component that renders
them (`actionNames`, per objectui's own designer config) and which the same page
already lists two sections up.

Scan after the fix: 392 mdx + 2299 markdown files, 8 registered-type props bags,
0 teaching a key these schemas reject. `check:skill-examples` — 209 prose
examples type-check.

Part of objectstack-ai#4001

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012NLJ8PWLdwLSyi84LHzrAx

* chore(spec): regenerate authorable-surface after merging main — the os-regen driver's second half

`authorable-surface/ui.json` carries `merge=os-regen`, so a merge resolves it by
taking a side and leaving it REGEN-PENDING rather than by textually splicing two
generated files. That is the driver working as designed; the half that must
follow is the regeneration, and it is not optional here — measured on the merged
tree before pushing, `check:authorable-surface` fails:

    ❌ authorable-surface/ is out of date (1 key(s) not recorded).
         + ui/GlobalFilter:object

The missing key is main's own, from objectstack-ai#7892 (`GlobalFilterSchema` gains an optional
`object` for i18n label resolution). Nothing of this branch's is involved: the
regenerated diff is exactly that one line, and batch A's five declared keys were
already recorded.

Worth stating because the failure mode is quiet: `pnpm build` REWRITES this
artifact, so a `check:generated` run that happens to follow a build reports all
13 green against the file the build just fixed. Only `check:authorable-surface`
on its own, against the merged bytes, shows the drift — which is how this was
caught before the merge queue caught it.

Merged tree verified: spec 381 files / 10091 tests, lint 71 / 1932,
check:generated 13/13, check:strictness-ledger current (438 sites / 149 strip /
9 authorable — unmoved by the merge). The ledger counts and liveness counts were
regenerated too and came back byte-identical.

Part of objectstack-ai#4001

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012NLJ8PWLdwLSyi84LHzrAx

* chore(spec): recompute the strictness ledger counts from the tree merged with batch B

Second turn of the os-regen relay. `objectstack-ai#7985` (batch B — the memory driver's five
persistence sub-shapes) and `objectstack-ai#7972` landed while this branch was in the queue, so
the shared counts artifact met two batches that each decremented it correctly and
independently.

That is the exact arithmetic objectstack-ai#5107 built this artifact to prevent: the rows do
not overlap, git merges them without complaint, and the SUBTOTAL — which overlaps
nothing — merges clean and wrong. The artifact carries `merge=os-regen` so the
merge defers rather than splices, and the only correct resolution is to recompute
from the merged tree. Regenerated, never hand-edited:

    still-open (strip)          149 → 144   (batch B's five)
    files carrying at least one  26 → 25
    authorable — forced scope      9 → 4
    data/ strip                 107 → 102,  data/ strict 57 → 62
    total strict                283 → 288

Neither batch's own numbers moved; the combined ones did. `check:strictness-ledger`
agrees with the merged tree in both directions (25 open files / 144 strip sites,
no closed file still carrying a worklist row — batch B's `driver/memory.zod.ts`
row left with their side of the prose).

Merged tree verified: spec 382 files / 10109 tests, lint 71 / 1932,
check:generated 13/13.

Part of objectstack-ai#4001

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012NLJ8PWLdwLSyi84LHzrAx

---------

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

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spec: guard the confirmText + params PAIR at authoring time, once the 20 shipped sites have migrated to action.description

2 participants