Skip to content

feat(types): arm record:approvals and record:attachments from their spec rows (objectui#12363) - #12382

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-12363-approvals-attachments-arms
Oct 14, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-12363-approvals-attachments-arms

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #12363
Clause-②: yes

What

@objectstack/spec 18 declares two page blocks, record:approvals and record:attachments. @object-ui/app-shell registers a renderer for each, and buildDefaultPageSchema (@object-ui/plugin-detail) emits both as the default record page's Approvals and Attachments tabs. But AnyComponentSchema had no arm for either. So objectui validate refused each one with invalid_union at type, and the record page objectui builds itself failed validation.

This PR reuses the construction of objectui#12342 (PR #12362, 52d6194):

Contract review: the accept set this widens (Clause-②)

Installed spec. @objectstack/spec 18.0.0-next.0, as this checkout resolves it (packages/types). Both types are PageComponentType members, and both have a ComponentPropsMap row. This was measured at runtime and is re-read by the new test on every run.

Type Spec row Row keys Strict? Arm properties Accepted keys
record:approvals RecordApprovalsProps (not exported under that name from @objectstack/spec/ui) none yes (catchall never) z.strictObject({}).optional() none: only {}
record:attachments emptyProps('record:attachments'), built inline and exported under no name none yes z.strictObject({}).optional() none: only {}

Key by key: neither row declares a key, so neither bag declares one. ⛔ No key the spec does not declare, and no passthrough / catchall. Both rows are unnamed exports, so they cannot cross the objectui#8317 import boundary by reference. They are restated, which is element:divider's and record:approval_decision's situation.

record-panels-arms-12363.test.ts pins three things against the installed row on every run:

  • the arm's bag key set equals the row's;
  • the bag accepts or refuses five probe bags exactly as the row does;
  • a CONTROL: the row itself accepts {} and refuses a key.

What is refused:

  • Any key inside properties (unrecognized_keys at properties). This includes approvals and currentUserId. The spec's RecordApprovalsProps guidance names both as the HOST's runtime channel, "not authorable surface".
  • A node-level children or body, refused by name (invalid_type at the key).
  • A flat undeclared key, refused on the strict face (unrecognized_keys).

What is accepted: the bare node { type }, and the node with id / className / an empty properties bag (the BaseSchema node keys).

Published exports added:

  • @object-ui/types/zod: RecordApprovalsBlockSchema, RecordAttachmentsBlockSchema.
  • @object-ui/types: the TS types of the same names, plus two new AnySchema members.
  • AnyComponentSchema / StrictAnyComponentSchema gain two discriminator literals.

Premise checks (dispatch section 2)

  • A1 holds. Both are PageComponentType members with ComponentPropsMap rows on the installed spec. Both rows are strict and declare no key (table above).
  • A2 holds, with one boundary. buildDefaultPageSchema emits { type: 'record:attachments' } for an enable.files: true object, and { type: 'record:approvals' } when the host passes approvals. That page now validates whole on both faces (pinned).
    • When the HOST also passes approvals.node, the synthesizer spreads it into the node's properties. RecordDetailView does this with its live useRecordApprovals read: approvals plus currentUserId. The arm refuses those two keys because the spec row does.
    • This is the same shape as record:history's host entries / loading, which the existing arm already refuses.
    • The spec's own guidance describes that runtime path ("the default record page passes the approval requests it fetched through it at runtime"). That page is rendered through SchemaRenderer and never reaches the validator. So I read it as a host runtime channel, not a producer defect, and pinned it as a refusal. The arm is not widened for it. It is reported below.
  • A3 holds. The ratchet is in packages/cli/src/__tests__/registered-types-validate-ratchet-10859.test.ts. It drops by exactly two, 308 to 306.
  • Serial note. objectui#12370 (the next.1 bump) had not landed when this was pushed. origin/main moved by one commit, fc211e817, which touches neither packages/types, the lockfile nor the ratchet. So the rows are measured on next.0.

Pins

  • packages/types/src/__tests__/record-panels-arms-12363.test.ts. Per type, it pins:

    • the installed row is present and keyless, and the bag equals it;
    • the bare node validates on every face;
    • the type is claimed at type;
    • an undeclared key in the bag is refused;
    • a flat undeclared key is refused on the strict face;
    • children / body are refused by name;
    • the union reaches the arm itself.

    It also pins that properties.approvals / properties.currentUserId are refused.

  • packages/plugin-detail/src/__tests__/buildDefaultPageSchema.validatesWhole-12363.test.ts:

    • the default record page carries both panels, as a non-vacuity check;
    • it passes safeValidateSchema and StrictAnyComponentSchema with zero issues;
    • the Attachments-only page passes;
    • the host-payload page is refused with exactly one issue, unrecognized_keys at the record:approvals bag naming approvals and currentUserId.
  • The cli ratchet row described above.

Verification (head 83d91d3)

  • vitest, full packages/types/ and packages/cli/, plus the plugin-detail pin. One run under the verify lock: Test Files 409 passed (409), Tests 10719 passed (10719). This includes zod-mirror-parity, any-component-emit-headroom-11573, record-approval-decision-arm-12342 and the ratchet.
  • type-check. @object-ui/types, @object-ui/cli and @object-ui/plugin-detail all exit 0. I built the dependency closure first.
  • Ablation (ablation-replace.mjs, run with --delete on the two union members in zod/index.zod.ts):
    • anchor went 1 to 0, and the blob changed cd2ce50 to 0f4b39f;
    • the three pin files went red: Test Files 3 failed (3), Tests 24 failed | 50 passed (74);
    • the ratchet pin (the refused count equals the pin), the new ratchet row, all four default-page rows and every union-path arm row failed;
    • the bag-vs-row rows stayed green, as predicted, because they read the arm directly;
    • restore was proven by blob == HEAD and an empty git diff HEAD.
  • Gates, each exit code read directly. These all exit 0: check:phantom-deps, self-import, unreferenced-sources, doc-example-readers, handler-key-reads, side-effects-array, element-data-source-declaration, spec-symbols, action-forward-parity, designer-field-key-parity, icon-record-names, i18n-keys, i18n-drift, i18n-designer-parity, registry-bare-names, control-bytes and new-line-citations. So do check-changeset-fixed.mjs, check-changeset-presence.mjs and check-changeset-no-major.mjs.
  • check:spec-floors exits 1. It says "The scan collapsed: 0 package(s) declare @objectstack/spec in a consumer-facing field". That is the known failure on main (objectui#12334), and this diff touches no dependency field.
  • Lint, narrowed. eslint --format json over the 9 changed .ts files gives 9 results, 0 errors and 0 warnings, the way the package lint scripts run it (eslint . for types and plugin-detail, eslint src for cli).
    • With --no-inline-config it reports 3 findings, all on lines this diff does not touch: 2 in zod-mirror-parity.test.ts, 1 in types/src/index.ts. Each is already covered by an inline disable.
    • eslint.config.js sets no project / projectService, so linting is not type-aware, and this diff cannot change the verdict on any untouched file.
    • The full pnpm lint run belongs to CI.

Acceptance notes

  • The file surface grew by one test file. The dispatch named packages/types/src/**, the cli ratchet and their tests. The ruled pin "buildDefaultPageSchema's page validates whole" cannot live in either package. @object-ui/types and @object-ui/cli do not depend on @object-ui/plugin-detail, and a reverse import is what check:phantom-deps refuses. So that pin is a new test file in packages/plugin-detail/src/__tests__/, a package that already depends on @object-ui/types. No plugin-detail source changed.
  • The restated record:approvals bag drops the spec's guidance text. The spec row's own unrecognized-key message explains why approvals and currentUserId are host channels. The restated z.strictObject({}) reports only a generic unrecognized_keys. The arm's .describe() carries the same guidance for readers of the schema. Carrying the row's message would need the row by reference, which the objectui#8317 boundary census does not admit for an unnamed export.
  • The arms do not spread NODE_ENVELOPE (responsiveStyles), as on app-shell: spec 18's record:approval_decision page block renders nothing — ApprovalDecisionRenderer is written but never registered (the objectui half of spec 18's three record blocks, after #12030) #12342's and the cloud: arms. Neither block is offered by Studio, and no producer writes that key on these nodes.

Generated by Claude Code

…pec rows (objectui#12363)

Both spec-18 page blocks were registered by app-shell and emitted by
buildDefaultPageSchema, but AnyComponentSchema had no arm for them, so
objectui validate refused the default record page at `type`. Each arm's
`properties` bag restates its empty strict ComponentPropsMap row; body and
children are refused by name. NAMESPACED_REFUSED_AT_TYPE falls 308 -> 306.

Claude-Session: https://claude.ai/code/session_01XVp19E1MYUTFM3dnkwz8gB
Co-authored-by: Claude <noreply@anthropic.com>
…hments arms (objectui#12363)

Claude-Session: https://claude.ai/code/session_01XVp19E1MYUTFM3dnkwz8gB
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 1 pending changeset(s) describe a file this change touches

Their bodies publish verbatim into the CHANGELOG at the next release, so this is a request to re-read them against your diff — addressed here because you are the one seat that can answer it without re-deriving anything.

⛔ Nothing here blocks, and nothing here is a verdict on your change. This gate exits 0, is not a required context, and judges name resolution, never meaning: it asked whether a pending body names a file you touched. "Is this sentence still true?" is the one question it will not answer, and the one you are being asked to answer.

.changeset/11573-any-component-emit-headroom.md

  • names packages/types/src/__tests__/any-component-emit-headroom-11573.test.ts → packages/types/src/__tests__/any-component-emit-headroom-11573.test.ts — edited by this change

    packages/types/src/__tests__/any-component-emit-headroom-11573.test.ts reads the declaration's size with the compiler's own counter, fails when it crosses a tenth of the ceiling below it, and lists every member as named or inline.

Read the paragraph, not the line: both false halves of the objectui#8617 claim sat in one paragraph, and correcting either alone would have left it asserting the same wrong thing.

If a claim did go false, correct the body. That is precedented and prose-only, frontmatter untouched; check-changeset-overwrite.mjs will report the correction as its own case 2 ("correcting a declaration on purpose … legitimate"), which is the intended shape — one gate asks for the read, the other records the write.

Not covered, stated so nobody reads this as more: a born-false claim that spells no line address at all (objectui#9495 coordinated one by ORDINAL — "a grep finds that member first" — and deciding that means reading what the sentence means), a claim spelled as a symbol or a package rather than a backticked file name, and a file named ambiguously.

Compared the checked-out tree with fc211e817 (merge-base with origin/main): 9 file(s) changed outside .changeset/, read against 443 pending declaration(s) that publish a body (459 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 69 chunks) 3052.7 KB 3119.4 KB
Main entry chunk (gzip) 73.8 KB 350 KB
Entry file index-T1h6m0gh.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 20.90KB 7.67KB
app-shell (runtime-config.js) 22.59KB 7.89KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.62KB 0.83KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 41.19KB 11.12KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.26KB 8.03KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (commentAuthorship.js) 1.39KB 0.78KB
collaboration (index.js) 1.93KB 0.85KB
collaboration (useCollaborationTranslation.js) 6.50KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 591.65KB 142.68KB
core (index.js) 10.18KB 4.04KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 248.41KB 69.48KB
fields (index.js) 283.00KB 72.12KB
i18n (LocalizationContext.js) 2.92KB 1.42KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 36.87KB 9.88KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 40.26KB 11.71KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 15.35KB 5.51KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.82KB 2.38KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.04KB 3.92KB
plugin-calendar (index.js) 54.94KB 16.05KB
plugin-charts (index.js) 84.72KB 23.27KB
plugin-chatbot (index.js) 201.72KB 48.07KB
plugin-dashboard (index.js) 143.63KB 38.79KB
plugin-designer (index.js) 233.77KB 50.02KB
plugin-detail (index.js) 259.78KB 68.65KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 179.38KB 46.59KB
plugin-gantt (index.js) 178.36KB 45.08KB
plugin-grid (index.js) 259.06KB 72.25KB
plugin-kanban (index.js) 53.23KB 16.71KB
plugin-list (index.js) 123.35KB 31.60KB
plugin-map (index.js) 27.14KB 8.99KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 46.66KB 13.13KB
plugin-timeline (index.js) 39.21KB 11.85KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 91.99KB 23.23KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 12.07KB 3.68KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.07KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.81KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 1.13KB 0.65KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 5.78KB 2.70KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (node-slots.js) 7.18KB 2.34KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-approval-decision.js) 0.20KB 0.20KB
types (record-components.js) 0.20KB 0.19KB
types (record-panels.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.26KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.48KB 3.50KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 83d91d37f63c42d5138206b014f935be213cf4e6
Local-runs: none

Inputs read: card #12363 body, the first grade (6165654268), the unlock scan (6168991320), the claim (6174876942, Clause-②: yes), the os-dev-report (6175444442); PR #12382 body (line 2 Clause-②: yes), its 10-file list and net diff against the merge-base with main (d78e70b87, which is one commit past the fc211e817 the dev measured on; the only file main moved in packages/types, packages/cli, packages/plugin-detail or .changeset/ since that base is .changeset/12359-create-owner-pick.md, not one of this PR's); the two advisory bot comments on the PR (6175431643 changeset-claim-re-read, 6175522008 bundle report); the head's 33 check-runs; the spec rows in objectstack packages/spec/src/ui/component.zod.ts and page.zod.ts at 18.0.0-next.0 (the version packages/types/package.json and the lockfile resolve); the #12342 arm (record-approval-decision.zod.ts), its changeset 12342-register-approval-decision.md and its contract review (6167866021) as the precedent; record:history's host-slot refusals in public-blocks.zod.ts; buildDefaultPageSchema.ts lines 273-282 and 926-933 (the approvals.node spread); the 11573-any-component-emit-headroom.md changeset the re-read bot named.

① Derived judgments

  • Checks. All 33 check-runs on 83d91d3 are completed; every one is success except the three skipped (dependabot, Test (coverage), the coverage shard matrix). That includes Test and all 12 shards, Type Check, Lint, Repo Gates, Spec Main Shape Gate, Build & E2E, Test (dist pins), the four changeset gates, Governed Surface Queue Guard and Line Citation Gate. No gate is red on this head; the dev's local check:spec-floors exit 1 is the known main failure (objectui#12334) and Repo Gates is green regardless.
  • Arms: RecordApprovalsBlockSchema and RecordAttachmentsBlockSchema = BaseSchema + type literal + optional properties: z.strictObject({}) + body / children tombstoned; two new AnyComponentSchema members. Right, and the derivation from the spec rows is sound. On the installed spec, ComponentPropsMap['record:attachments'] is emptyProps('record:attachments') built inline and exported under no name; ComponentPropsMap['record:approvals'] is the module-private const RecordApprovalsProps, a strictObject with no member (its guidance map carries per-key prescriptions for approvals and currentUserId, which are refusal messages, not declared keys). Neither can cross the objectui#8317 boundary by reference, so a restated empty strict bag is the only faithful form, the same construction app-shell: spec 18's record:approval_decision page block renders nothing — ApprovalDecisionRenderer is written but never registered (the objectui half of spec 18's three record blocks, after #12030) #12342 landed and the card asked for. The accept set of each bag is {} alone, identical to its row. record-panels-arms-12363.test.ts reads the INSTALLED rows on every run and pins each arm's key set and accept set to them (five probe bags including both host keys, plus a control that the row itself refuses a key), so a prop the spec adds later turns the file red. Derivation with a gate: right.
  • Not in public-blocks.zod.ts. Right: that union is pinned to the ADR-0080 curated list, neither block is curated, and app-shell: spec 18's record:approval_decision page block renders nothing — ApprovalDecisionRenderer is written but never registered (the objectui half of spec 18's three record blocks, after #12030) #12342 set the precedent of a module of its own.
  • Refused by name: body / children via retirementTombstone. Right. The spec reads neither panel as a container, and the arm's text matches the app-shell: spec 18's record:approval_decision page block renders nothing — ApprovalDecisionRenderer is written but never registered (the objectui half of spec 18's three record blocks, after #12030) #12342 arm's shape line for line, with the renderer facts changed (RecordApprovalsRenderer also reads the host payload off the node; RecordAttachmentsRenderer reads className and designer attributes only).
  • Published surface, measured on the faces. @object-ui/types . entry gains two type exports (RecordApprovalsBlockSchema, RecordAttachmentsBlockSchema, from the new record-panels.ts) and AnySchema gains both members; ./zod gains two value exports of the same names (from the new record-panels.zod.ts), and AnyComponentSchema / StrictAnyComponentSchema / safeValidateSchema accept two more type literals. Nothing is removed or narrowed. No package.json exports field moves (the bundle report shows types (record-panels.js) as a new per-module chunk, which is the existing build glob). @object-ui/cli and @object-ui/plugin-detail publish nothing new: each changes only under __tests__/. The dev's Clause-② table is right on every face.
  • Ratchet: NAMESPACED_REFUSED_AT_TYPE 308 to 306. Right. main at the merge-base reads 308; the pin falls by exactly the two keys armed; the new row pins refusedAtType(key) === false for both, BARE_KEYS without the bare twins (both registered skipFallback: true), and a lit control that an unclaimed record: literal is still refused. The app-shell: spec 18's record:approval_decision page block renders nothing — ApprovalDecisionRenderer is written but never registered (the objectui half of spec 18's three record blocks, after #12030) #12342 row's stale comment about unarmed siblings is corrected.
  • Mirror parity and emit headroom. EXPECTED_MIRROR_PAIRS 196 to 198 with both pairs declared in MIRRORS and Declared; two inline arm rows in the emit-headroom ledger. Consistent with the diff; the 11573 changeset's claim that the test "lists every member as named or inline" stays true with the two rows added.
  • Default record page validates whole. Right, and non-vacuous: buildDefaultPageSchema.validatesWhole-12363.test.ts first asserts both panel types are present in the page, then passes it on both faces, and passes the Attachments-only page. The pin lives in packages/plugin-detail/src/__tests__/ because @object-ui/types and @object-ui/cli do not depend on @object-ui/plugin-detail and the reverse import is what check:phantom-deps refuses; no plugin-detail source changes.
  • The A2 boundary: a page with the host's approvals.node payload is refused at the record:approvals bag. Right to pin it as a refusal and not widen the arm. buildDefaultPageSchema spreads approvals.node verbatim into the node (line 931), and RecordDetailView threads approvals + currentUserId through it; the spec row names both the HOST's runtime channel ("not authorable surface") and refuses them. No runtime path hands that synthesized page to safeValidateSchema / StrictAnyComponentSchema (none in RecordDetailView.tsx or plugin-detail/src outside tests), and before this PR the same page was refused at type anyway, so nothing regresses. The pin asserts exactly one issue, unrecognized_keys at children.0.properties, keys [approvals, currentUserId], and the record:history entries / loading class is the stated precedent.

② Semver level

  • .changeset/12363-approvals-attachments-arms.md: @object-ui/types minor, nothing else.
  • types minor: right. Two new exports on each of two published faces and a wider accept set on AnyComponentSchema / StrictAnyComponentSchema / safeValidateSchema; nothing removed. Under objectui AGENTS.md §9 nothing here could be major, and Changeset Bump Policy, Changeset Declaration, Changeset Fixed Group Check and Changeset Overwrite Report are green on the head.
  • cli and plugin-detail not declared: right. Unlike app-shell: spec 18's record:approval_decision page block renders nothing — ApprovalDecisionRenderer is written but never registered (the objectui half of spec 18's three record blocks, after #12030) #12342, which changed KNOWN_SCHEMA_TYPES and so gave the cli a bump, this PR touches only test files in both packages, and objectui validate's behaviour moves through the @object-ui/types dependency, which the fixed group publishes at the same minor anyway.
  • The body is accurate to the diff: it names the two arms, the two twins, the {}-only accept set, the refused host keys and the refused content channels. One typo that does not change meaning: "the default record page buildDefaultPageSchema builds emits both" (a stray "builds"). Prose only; the seat may fix it before the queue, no frontmatter change.
  • Clause-②: yes. The claim says yes, the PR body line 2 says yes, and the measured surface above confirms it: two new published arms widen objectui validate's accept set.

③ Boundary flags

  • Deviation (file surface): one new test file in packages/plugin-detail/src/__tests__/, outside the claim's named surface. Answered: the card's third pin ("the default record page buildDefaultPageSchema emits validates whole") cannot live in types or cli without a phantom dependency; plugin-detail already depends on @object-ui/types; no plugin-detail source changed. No breach.
  • Deviation (process): one verify-lock attempt ended exit 99 and was retried. Answered: procedural, no bearing on the diff; the head's check-runs are the gate verdicts.
  • A2 boundary (host-threaded approvals / currentUserId refused). Answered in ①: right by the spec row, pinned as a refusal, and no validator path reaches that page.
  • Serial note: deps(v18): move objectui's @objectstack/* from 18.0.0-next.0 to 18.0.0-next.1 and follow its contract — the bump the held follow-ups wait on #12370 (18.0.0-next.1) had not landed; rows measured on next.0. Answered: still true at the merge-base d78e70b87; packages/types/package.json and the lockfile resolve 18.0.0-next.0, and the pin re-reads the installed rows on every run, so a bump that adds a prop would turn record-panels-arms-12363.test.ts red rather than silently widen or narrow anything.
  • Out-of-scope finding (not filed): the restated record:approvals bag drops the spec row's per-key guidance text; the arm reports a generic unrecognized_keys. Answered: the accept set is identical, which is what this review judges; the message loss is the same consequence app-shell: spec 18's record:approval_decision page block renders nothing — ApprovalDecisionRenderer is written but never registered (the objectui half of spec 18's three record blocks, after #12030) #12342's restatement carries and the card asked for that construction. A follow-up could restate the two keys as named tombstones inside the bag (the record:history hostFeedSlotGuidance pattern, which lives in objectui and needs no spec export) without changing the accept set; that is optional and not this PR's. Not escalated.
  • open_questions: none.
  • check:spec-floors exit 1 locally. Answered: the known main failure (objectui#12334); no dependency field moves; Repo Gates is green on the head.
  • changeset-claim-re-read (advisory, exit 0) named 11573-any-component-emit-headroom.md. Answered on reading: the sentence it quotes stays true with two more rows in the list.

Implemented-by: claude/issue-12363-approvals-attachments-arms
Reviewed-by: session_01XVp19E1MYUTFM3dnkwz8gB

VERDICT: PASS


Generated by Claude Code

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

Projects

None yet

2 participants