Skip to content

feat(types,plugin-detail): declare the field-security triple on record:details / record:highlights / record:related_list and read it un-cast (objectui#8649) - #11184

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-8649-record-security-triple
Sep 30, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-8649-record-security-triple

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #8649
Clause-②: yes — declaring enforceFieldSecurity, redactFields and requiredPermissions on the three record blocks' published props types and registry inputs widens the accepted authoring surface to exactly what @objectstack/spec 17.5.0 accepts on those blocks, and no further. This PR waits as a draft for the director seat's contract review.

What this does

The card's last open item: the nine reads of the field-security triple in @object-ui/plugin-detail, on record:details, record:highlights and record:related_list. PR objectui#9469 (Part of) took the other three reads and routed these nine to the platform as objectstack#18159. That card closed completed with PR objectstack-ai/objectstack#19913 (ruling A), and main now installs @objectstack/spec 17.5.0, so the routing has expired into "align the mirror":

  • @object-ui/types: RecordDetailsComponentProps, RecordHighlightsComponentProps and RecordRelatedListComponentProps each gain enforceFieldSecurity?: boolean, redactFields?: string[] and requiredPermissions?: string[].
  • Renderers: the nine reads in record-details.tsx, record-highlights.tsx and record-related-list.tsx lose their (schema as any) cast. For example, (schema as any).enforceFieldSecurity === true becomes schema.enforceFieldSecurity === true. The record-related-list.tsx props docblock that said "Do not reopen it to admit a key the renderer reads through a cast" is rewritten, because the contract now declares the three keys on that block.
  • Registry inputs (packages/plugin-detail/src/index.tsx): each of the three blocks publishes the three keys. The types are the contract's (boolean, or array of string), and each description is that block's own .describe() text from the installed spec, verbatim.
  • The console guard (registry-inputs-spec-parity.test.ts): this card's nine OWED TO objectui#8649 entries are struck. OBJECTUI_11111_LEDGER_CAPS.unpublishedKeys goes from 57 to 48, the owner-count pin 'objectui#8649' goes from 9 to 0, and the six new array inputs get member pins. No other owner's entry was touched.
  • Changeset: minor on @object-ui/types and @object-ui/plugin-detail, stating the widening.

⛔ No runtime permission, gating or masking behaviour changes (triage floor 5619608221). The proof is under "Honest types, same behaviour".

Premises re-measured on main before any edit (Partition 2)

1. What 17.5.0 declares. The installed node_modules/@objectstack/spec/package.json reads 17.5.0. Two instruments were run over the contract's own block-tag map ComponentPropsMap, and each was self-tested on known schemas first:

  • A is a parse probe that reads unrecognized_keys.
  • B enumerates .shape.
enforceFieldSecurity   A = [record:details, record:related_list, record:highlights]   B = same   agree
redactFields           A = [record:details, record:related_list, record:highlights]   B = same   agree
requiredPermissions    A = [..the three.., record:quick_actions]                     B = same   agree
aria / fields          many blocks                                                   CONTROL (lit)
zzqx_no_such_key       none                                                          CONTROL (lit)

⚠️ On the first pass the two instruments disagreed on one block, user:profile. Its props schema is z.never(), so it refuses every key without unrecognized_keys, and instrument A read that as "not refused by name" for every key, including the nonsense control. That block is now excluded from A's population and named in the output, and the two instruments agree on every key.

Declared shapes, identical on the three blocks: enforceFieldSecurity is z.boolean(), redactFields is z.array(z.string()) and requiredPermissions is z.array(z.string()). All three are optional with no default. The enforceFieldSecurity and redactFields describes are block-specific. The requiredPermissions describe is the one ruling 5798783314 shares word for word with record:quick_actions.

⇒ Premise holds: all nine keys are declared, on exactly those three blocks.

2. The read sites. The checker (membership, via getPropertyOfType on each binding) and an expression probe (getTypeAtLocation on each read) were run over the three renderers. The probe refuses to report while any TS2307 is present: its first run in this fresh worktree had 48 of them and was discarded, the dependency closure was built, and the probe was re-run.

  • Before: all 15 textual reads (nine sites, where requiredPermissions and redactFields are each read twice in their ternary) are cast, typed any, and are not members of the binding.
  • After: none is cast. Each read carries its declared type: boolean | undefined, string[] | undefined, or string[] inside the Array.isArray narrowing.
  • Expectation vs spec: === true against z.boolean(), and Array.isArray(...) ? ... : [] into a string[] against z.array(z.string()). Both match, so there is no behaviour question.

3. Honest types, same behaviour. Before and after, each renderer was transpiled with removeComments:

record-details.tsx       js IDENTICAL (sha256/16 336f03c50dbc3cc5 both sides, 9083 bytes)
record-highlights.tsx    js IDENTICAL (1936043763ee768a both sides, 2734 bytes)
record-related-list.tsx  js IDENTICAL (c42d56544b3a6130 both sides, 6397 bytes)
CONTROL  `=== true` -> `== true` on the enforceFieldSecurity read   js DIFFERS  (instrument fires)
CONTROL  a comment inserted at the same site                         js IDENTICAL (comments stripped)

The @object-ui/types change is interface-only.

4. Registry inputs. The three blocks' inputs live in packages/plugin-detail/src/index.tsx. recordDetailsInputs.spec-parity.test.ts and its two siblings asserted only the forward half: no input that the spec does not accept. The reverse half lives in the console guard.

5. The ledger. See "What this does".

6. objectui#10200's premise does not hold on 17.5.0. That card was written against 17.4.0 and says record:details "honours three security keys the pinned spec REFUSES". On the installed 17.5.0:

  • RecordDetailsProps accepts all three keys, by both instruments above, with value-level parses in the new pins.
  • Its ruling item 1 (stop reading requiredPermissions) was withdrawn by ruling A on objectui#10281.
  • Its item 2 (the pin bump) landed as objectui#11086.
  • ⇒ Neither of its two premises stands. This PR does not work or edit that card; this is a reading for its seat.

Tests

New and updated pins:

  • detailRendererUndeclaredKeys-8649.test.ts is this card's own pin. It now carries:
    • Equal type pins for each block: the mirror members equal the contract's ComponentPropsInput members, spelled out, and each renderer binding carries them.
    • Source-text legs showing that each of the nine reads is still read un-cast, each with controls.
    • Value-level parses of the triple on each block.
    • A note on why the binding pins are split per block: in one nested tuple the three compared unequal under the identity trick, although each compares equal on its own.
  • record-details.hideFieldsUncast-9965.test.ts and record-related-list.relationshipValueFieldUncast-9475.test.tsx now have empty cast ledgers, as both files said this card's landing would do.
    • The 9965 program leg lost its only cast and any reads, which it used as calibration. It now measures a virtual control file in the same program, which pins that the checker reports a cast read as any, an un-cast read as string[] | undefined, and an undeclared read as any.
    • A new leg pins that the triple is still read, and read with the mirror's type.
  • record-related-list.propsRefusal-9963.test.tsx: the three refusal rows become acceptance rows, with wrong-type and misspelling refusals kept as @ts-expect-error controls.
  • The three record*Inputs.spec-parity.test.ts files check each key. The key is declared on the block. The published type is proved on values: an accepted value parses, and a rejected value is refused at that key (redactFields: [1] is refused at redactFields.0, the member). The description equals the installed describe.
  • RecordRelatedListRenderer.columnMembers.test.tsx: its CONTROL leg asserted the probe keys were unpublished. It now asserts that an input exists if and only if RecordRelatedListProps declares the key. Per triage carry 5627847529, that file is still not cited as a declaration.
  • record-highlights.fieldSecurity-8649.test.tsx is new. No test drove redactFields or enforceFieldSecurity on record:highlights. It pins which chips paint their value, with a control for each row.

Behaviour unchanged, cited:

  • The gating is pinned for all three blocks by record-blocks.requiredPermissions-gate.test.tsx, under a real MePermissionsProvider.
  • The field folds are pinned by:
    • record-details.unresolvedIdentityFailClosed-9054.test.tsx (details);
    • RecordRelatedListRenderer.redactedDerivation-9053.test.tsx and RecordRelatedListRenderer.unresolvedIdentityFailClosed-8793.test.tsx (related list);
    • the new highlights file.
  • All of them pass unchanged at this head. The only edits to the 9054 and 8793 files are docblocks whose "renderer-only key" sentence the declaration made false.

Runs at head bf0385366 (exit codes captured before any pipe):

  • vitest run packages/types/: exit 0, Test Files 283 passed (283), Tests 6496 passed (6496).
  • vitest run packages/plugin-detail/: exit 0, Test Files 225 passed | 1 skipped (226), Tests 2235 passed | 8 skipped (2243). The skipped file is summaryChip.dateOnlyZone-10183.test.tsx, which carries its own skip condition and is not touched here.
  • A union of the console and cross-package readers of these surfaces: exit 0, Test Files 11 passed (11), Tests 452 passed (452). The files are the eight apps/console tests that name the three blocks, RecordDetailView.pageHeaderTitleFls-10499.test.tsx, public-tier.test.ts and check-handler-key-read-sites.test.ts.
  • Type check, run after rebuilding @object-ui/types: pnpm --filter @object-ui/types run type-check (including tsconfig.test.json) exits 0, and pnpm --filter @object-ui/plugin-detail run type-check exits 0.
  • Lint: @object-ui/types exits 0 with 0 errors, and @object-ui/plugin-detail exits 0 with 0 errors. eslint --format json on the console guard file reports 1 file, 0 errors and 0 warnings.

Ablations: direction predicted first, each mutation shown on disk, restored by blob-hash equality with git diff HEAD empty

All three legs went through ablation-replace.mjs (a literal anchor that must hit, a trap-armed restore on an absolute path) while holding the verify lock.

  1. Re-cast one read. schema.enforceFieldSecurity === true became (schema as any).enforceFieldSecurity === true in record-highlights.tsx.
    • Prediction: exactly the highlights enforceFieldSecurity un-cast leg goes red, and the emitted JS does not move.
    • Observed: anchor 1 to 0, blob moved. Tests 1 failed | 26 passed (27), and the failing case was that leg. record-highlights.tsx js IDENTICAL under the mutation, so the source-text pin is the only instrument that can see a re-cast.
    • ⚠️ The first attempt never ran: the lock timed out (exit 99) and the mutation was restored untouched. It was re-run with the mutation inside the held lock.
    • 1b, the negative leg on its own. Only the second of the two requiredPermissions reads in record-related-list.tsx was re-cast (? schema.requiredPermissions became ? (schema as any).requiredPermissions), so the liveness half still holds.
      • Prediction: the negative leg of that case fires on its own, and the objectui#9475 guard, now without carve-outs, names the key.
      • Observed: Tests 2 failed | 35 passed (37). The first failure was "expected ... not to match" the cast matcher; the second was expected [ 'requiredPermissions' ] to deeply equal [].
  2. Undeclare one key. enforceFieldSecurity was removed from RecordDetailsComponentProps, then @object-ui/types was rebuilt, because plugin-detail's tsc resolves it through dist/.
    • Prediction: the type pins go red in this card's test file only, record-details.tsx still compiles through its index signature, and the 9965 program guard goes red on the membership leg and the triple-type leg.
    • Observed:
      • The dist proof: declarations in dist/record-components.d.ts went from 3 to 2.
      • tsc -p tsconfig.test.json exited 2 with six errors, all in detailRendererUndeclaredKeys-8649.test.ts: three TS2344 (the details mirror pin, the every-block shape pin and the details binding pin), the TS2339 pair that accompanies the first two, and TS2353 on the details fixture. There were zero errors in the renderer.
      • The 9965 guard showed Tests 2 failed | 8 passed (10) on exactly the two predicted legs.
      • The direction held. The count was higher than predicted (6 errors, not 4) because of the TS2339 companions.
      • Restore: blob equals HEAD, then a rebuild brought the dist count back to 3.
    • ⚠️ The first attempt was refused by the tool before running: its replacement was a substring of the anchor, so its count could not rise. It was redone with a marker replacement.
  3. Unpublish one input. The record:highlights redactFields input was replaced by a comment.
    • Prediction: two highlights parity legs go red, and the console guard reds on the reverse direction and on the member-pin population.
    • Observed: Tests 4 failed | 239 passed (243). The four were those two parity legs, record:highlights publishes every top-level key its spec props schema declares, and every member pin names a key that is still array/object-armed on a covered block.

Gates run locally (verdict lines read from each gate)

Exit 0:

  • Changeset gates: check-changeset-presence ("1 changeset(s) added"), check-changeset-no-major, check-changeset-fixed, check-changeset-overwrite ("0 modified"), check:changeset-claims (report-only, see the notes below), check:pending-changeset-literals.
  • Text gates: check:control-bytes, check:new-line-citations ("0 new citation(s)"), check:spec-symbols, check:handler-key-reads, check:installed-pin-claims.
  • Governance: check-governed-queue-guard --test over the diff reads "NOT GOVERNED", and --self-test passes.
  • Coverage: check-type-check-coverage, check-lint-coverage.
  • Test hygiene: check:test-path-roots, check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape.
  • Package gates: check:element-data-source-declaration, check:unreferenced-sources, check:phantom-deps, check:self-import.

NOT MEASURED:

  • check:sdui-registration-pins: its prerequisite was not met (exit 2, "No console build to weigh"). This diff moves no registration array or sideEffects entry.
  • check:spec-floors -- --cross-check and check:published-dist: both need a full-repo build, so they are left to CI.
  • The repo-wide pnpm lint and the full pnpm test farm are CI's runs.

Acceptance notes

  • objectui#10224 covers this card's ground for two of its asks: requiredPermissions inputs on the three record blocks, and the record-related-list.tsx docblock in carrier note 5826463187. Both are done here. Its record:quick_actions description ask is not in this PR's scope, so objectui#10224 remains open for that ask. The seat can narrow it.
  • A pending note goes false: the last paragraph of .changeset/8649-detail-renderer-undeclared-keys.md, from this card's first half, says the three keys are deliberately NOT declared. That was true against 17.4.0. The claim's file surface names one changeset, so that body is not edited here; this PR's changeset states the supersession instead. A prose-only correction of that body is a one-paragraph change once the surface allows it. It is raised as an open question in the report.
  • objectui#11168 edits the same guard file in parallel. The shared lines are the unpublishedKeys cap and the owner-count pin, and whichever PR lands second merges main and re-derives them. The 57 in the bookings comment is now worded as "57 at the bump" and points at the cap constant, so it no longer needs re-deriving.
  • Not touched: objectui#9475, record:quick_actions, record-reference-rail.tsx.
  • The three renderer docblocks above the requiredPermissions gate say an unheld capability "hides the whole block". The renderer draws an insufficient-permissions notice, which is what the spec describe says. This is an observation only, not changed here. carrier: whoever takes objectui#10224's record:quick_actions description ask, which has the same wording drift one block over.
  • README: the package README and the docs guide are not updated. They are outside the claim's file surface. The published input descriptions carry the contract text.

Round 2: text only, head 2b17f990b

This round follows the seat's ACCEPT 5907807607. It adds two commits on top of bf0385366, with no rebase, no force-push and no rewrite of a pushed commit.

  • 731b1de1d, docs(plugin-detail). The docblocks above the requiredPermissions capability gate in record-details.tsx, record-highlights.tsx and record-related-list.tsx said an unheld capability "hides the whole block / strip / section". Each now says the content is withheld and an insufficient-permissions notice (role="status") renders in its place. That is what the renderers do, and it is what the contract's requiredPermissions describe on these blocks says: "this block does not render its content; wherever it would otherwise render, an insufficient-permissions notice takes its place". The related-list docblock also says that the automatic child-object read gate above it is a different gate, and that one does hide the section (it returns null). Comment text only.
  • 2b17f990b, chore(changeset). One dated correction note is appended to the end of each of two pending changesets, under the standing rule 「Allow the appended note (Recommended)」:
    • .changeset/8649-detail-renderer-undeclared-keys.md: the paragraph that begins "Three keys are deliberately NOT declared", and the census under it, are superseded, because this PR declares the triple on the three blocks.
    • .changeset/10155-record-blocks-capability-gate.md: "hides the block" is corrected to the notice behaviour. The capability set and the fail-closed verdict stand. The note names the related list's child-object read gate as the one that does hide.

Proof that the round is text only:

  • Append-only. git diff --numstat origin/main -- FILE reads 11 0 for each changeset. The first 4927 bytes (8649) and the first 2269 bytes (10155) of the new files are byte-identical to the blobs on main (cmp exit 0), so no existing line and no frontmatter moved. check-changeset-overwrite lists both as modified, with the same declarations at base and now.
  • Emitted JS, measured with round 1's instrument (transpile with removeComments, bf0385366 against 2b17f990b):
record-details.tsx       source DIFFERS   js IDENTICAL (336f03c50dbc3cc5 both sides, 9083 bytes)
record-highlights.tsx    source DIFFERS   js IDENTICAL (1936043763ee768a both sides, 2734 bytes)
record-related-list.tsx  source DIFFERS   js IDENTICAL (c42d56544b3a6130 both sides, 6397 bytes)
CONTROL  `=== true` -> `== true` on the enforceFieldSecurity read   js DIFFERS on all three (instrument fires)
CONTROL  a comment inserted at the same site                         js IDENTICAL on all three (comments stripped)

The three JS hashes are the same ones round 1 measured after its change.

Runs at head 2b17f990b (exit codes captured before any pipe):

  • vitest run packages/plugin-detail/: exit 0, Test Files 225 passed | 1 skipped (226), Tests 2235 passed | 8 skipped (2243).
  • pnpm --filter @object-ui/plugin-detail run type-check, after building the package's dependency closure: exit 0.
  • pnpm --filter @object-ui/plugin-detail run lint: exit 0 with 0 errors. The warning count on each of the three renderers equals round 1's.
  • Named gates, each exit 0:
    • check-changeset-presence ("1 changeset(s) added"), check-changeset-no-major, check-changeset-fixed.
    • check-changeset-overwrite (report-only): "1 changeset(s) added, 2 modified, 0 deleted".
    • check:changeset-claims (report-only): 6 pending changesets name a file this PR touches, against 7 in round 1. The one that left the list is the 8649 changeset. It left because this PR now modifies it, and the gate's went-false reading skips the changesets a change adds or modifies. That is not a verdict on its prose.
    • check:pending-changeset-literals, check:control-bytes, and check:new-line-citations ("0 new citation(s)").
  • Also run, because the diff touches markdown: check-shell-escape-residue, check-doc-links, and check-governed-queue-guard --test over the five paths ("NOT GOVERNED"). Each exits 0.

Round 2 acceptance notes:

  • In the three docblocks, a pre-existing line join remains: the paragraph that ends "skipped its declared gate entirely." (details, highlights) or "the wrong question either way." (related list) runs into the next line's *. It is left as is, because it is comment formatting outside this round's wording ask.
  • Test names in record-blocks.requiredPermissions-gate.test.tsx still say the gate "hides the WHOLE block" or "hides the block". Their assertions pin the notice: the refusal text is found and the block body is absent. They are left as is, because this round covers renderer comments and changesets only.

Generated by Claude Code

…hree record blocks and read it un-cast (objectui#8649)

`@objectstack/spec` 17.5.0 declares `enforceFieldSecurity`, `redactFields`
and `requiredPermissions` on `record:details`, `record:highlights` and
`record:related_list`. The mirror now declares them on the three props
interfaces, the renderers read them without the `(schema as any)` cast, and the
three blocks' registry inputs publish them with the contract's types and
describe text. The renderers' emitted JavaScript is byte-identical.

Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm
Co-authored-by: Claude <noreply@anthropic.com>
…d strike its objectui#11111 bookings (objectui#8649)

The three cast ledgers booked to this card (the objectui#9965 and
objectui#9475 source guards and this card's own routed-key ledger) are emptied,
the objectui#9963 refusal rows flip to acceptance with value-level controls,
and each block's parity pin proves the published type on contract values and
the description against the installed describe. A new pin drives
`record:highlights`' two fold keys, which no test covered. The console guard
strikes the nine entries, lowers the cap and the owner count, and registers
member pins for the six new array inputs.

Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm
Co-authored-by: Claude <noreply@anthropic.com>
… and plugin-detail (objectui#8649)

Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm
Co-authored-by: Claude <noreply@anthropic.com>
…jectui#8649)

The nested three-block form compared unequal under the identity-trick Equal
while each block compared equal alone; split per block, with the reason kept
beside the pins.

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

github-actions Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 6 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/7997-detail-view-related-retired.md

  • names renderers/record-details.tsx → packages/plugin-detail/src/renderers/record-details.tsx — edited by this change

    Why it retired. @objectstack/spec declares no DetailView schema at all — every DetailView occurrence in packages/spec/src is prose about this repo's own RecordDetailView.tsx — so this array mirrored no protocol schema and drifted freely: it declared columns as TableColumn[] while the renderer it fed also accepted bare field names, { field, label } and legacy { name, label } spellings. The axis that carried the ruling was measured zero pull: no application code authored the member, both internal producers of a detail-view node (RecordDetailDrawer, renderers/record-details.tsx) synthesize it without related, and the only in-tree authorings carrying real columns were two documents — both rewritten here.

.changeset/8067-component-input-member-kind.md

  • names apps/console/src/__tests__/registry-inputs-spec-parity.test.ts → apps/console/src/__tests__/registry-inputs-spec-parity.test.ts — edited by this change

    A registration's type: 'array' said a value was a list and stopped there, so a member that drifted from @objectstack/spec was invisible to every layer that reads a declaration. page:header.actions is the measured cost: the contract declares z.array(z.string()) ("Action IDs"), the renderer read the members as ActionDef objects, and the repo-wide parity gate in apps/console/src/__tests__/registry-inputs-spec-parity.test.ts stayed green for the whole life of the drift because both sides carried the key and neither could say what was inside it. What settled it was a maintainer ruling, not a test — and even after the fix, "these are ids" survived only as English in the registration's description.

.changeset/8400-kanban-name-field-skip-set.md

  • names record-details.tsx → packages/plugin-detail/src/renderers/record-details.tsx — edited by this change

    Deliberately one rung, unlike the same dedupe in record-details.tsx, which also carries deriveTitleField: that ladder filters a synthesized field list, whereas this one filters an author-declared cardFields, where dropping a field the author asked for is a worse failure than a repeated title. A regression test pins both directions, including an object whose declared and derived pointers disagree.

.changeset/9280-record-highlights-entry-icon-retired.md

  • names packages/types/src/record-components.ts → packages/types/src/record-components.ts — edited by this change

    • packages/types/src/record-components.ts — RecordHighlightsComponentProps.fields[]'s object arm: { name; label?; icon?; type?; readonly? } → { name; label?; type?; readonly? }. The key is removed, not tombstoned: the contract's arm is $strict, so the refusal an author needs already exists upstream and arrives named (invalid_union at the entry). A ?: never tombstone buys nothing here — it is the remedy for a non-strict mirror that would otherwise strip in silence, which is not this arm. - packages/plugin-detail/src/renderers/record-highlights.tsx — the entry normalizer stops copying icon: f?.icon into the normalized entry. That read was unreachable, not merely unused: no author could feed it past the $strict arm, and HeaderHighlight renders no .icon on the far side either, so the copy had no consumer in either direction. - packages/plugin-detail/src/index.tsx — the registry manifest's fields input description sketched the entry as {name,label?,icon?,type?,readonly?} → {name,label?,type?,readonly?}. The inputs ARE the published contract (gen-manifest.ts serializes them into sdui.manifest.json and sdui-intrinsics.d.ts), so leaving the sketch standing would have gone on teaching AI and human authors a key that gets the whole document refused at publish.
  • names packages/plugin-detail/src/renderers/record-highlights.tsx → packages/plugin-detail/src/renderers/record-highlights.tsx — edited by this change

    • packages/types/src/record-components.ts — RecordHighlightsComponentProps.fields[]'s object arm: { name; label?; icon?; type?; readonly? } → { name; label?; type?; readonly? }. The key is removed, not tombstoned: the contract's arm is $strict, so the refusal an author needs already exists upstream and arrives named (invalid_union at the entry). A ?: never tombstone buys nothing here — it is the remedy for a non-strict mirror that would otherwise strip in silence, which is not this arm. - packages/plugin-detail/src/renderers/record-highlights.tsx — the entry normalizer stops copying icon: f?.icon into the normalized entry. That read was unreachable, not merely unused: no author could feed it past the $strict arm, and HeaderHighlight renders no .icon on the far side either, so the copy had no consumer in either direction. - packages/plugin-detail/src/index.tsx — the registry manifest's fields input description sketched the entry as {name,label?,icon?,type?,readonly?} → {name,label?,type?,readonly?}. The inputs ARE the published contract (gen-manifest.ts serializes them into sdui.manifest.json and sdui-intrinsics.d.ts), so leaving the sketch standing would have gone on teaching AI and human authors a key that gets the whole document refused at publish.
  • names packages/plugin-detail/src/index.tsx → packages/plugin-detail/src/index.tsx — edited by this change

    • packages/types/src/record-components.ts — RecordHighlightsComponentProps.fields[]'s object arm: { name; label?; icon?; type?; readonly? } → { name; label?; type?; readonly? }. The key is removed, not tombstoned: the contract's arm is $strict, so the refusal an author needs already exists upstream and arrives named (invalid_union at the entry). A ?: never tombstone buys nothing here — it is the remedy for a non-strict mirror that would otherwise strip in silence, which is not this arm. - packages/plugin-detail/src/renderers/record-highlights.tsx — the entry normalizer stops copying icon: f?.icon into the normalized entry. That read was unreachable, not merely unused: no author could feed it past the $strict arm, and HeaderHighlight renders no .icon on the far side either, so the copy had no consumer in either direction. - packages/plugin-detail/src/index.tsx — the registry manifest's fields input description sketched the entry as {name,label?,icon?,type?,readonly?} → {name,label?,type?,readonly?}. The inputs ARE the published contract (gen-manifest.ts serializes them into sdui.manifest.json and sdui-intrinsics.d.ts), so leaving the sketch standing would have gone on teaching AI and human authors a key that gets the whole document refused at publish.
  • names packages/plugin-detail/src/__tests__/recordHighlightsInputs.spec-parity.test.ts → packages/plugin-detail/src/__tests__/recordHighlightsInputs.spec-parity.test.ts — edited by this change

    Pinned in packages/types/src/__tests__/record-highlights-fields-icon-9280.test.ts across three instruments that do not see the same thing — a tsc @ts-expect-error leg with a {name,label} control that stays green, safeParse legs against the installed spec artifact, and a source-text read whose lit control is that very sections[].icon member, so an empty result on the highlights arm is a reading rather than a matcher that cannot match. packages/plugin-detail/src/__tests__/recordHighlightsInputs.spec-parity.test.ts gains the REVERSE direction it was missing: it already failed when a spec entry key went undocumented, and now also fails when the description advertises an entry key the spec refuses.

.changeset/9964-related-list-picker-filter-mirror.md

  • names record-related-list.tsx → packages/plugin-detail/src/renderers/record-related-list.tsx — edited by this change

    So one key carried two declarations, with the looser one on the face an author — or an AI writing metadata — reads: unknown offers no shape guidance for a key whose consumer demands a specific shape. Nothing could report the divergence either, because record-related-list.tsx reached the block through four (schema as any).add reads; a cast unwraps the declaration at its own read site, so no compiler ever compared the two.

.changeset/record-alert-cta-label-i18n-4998.md

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 37d166280 (merge-base with origin/main): 17 file(s) changed outside .changeset/, read against 1793 pending declaration(s) that publish a body (2400 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 330 chunks) 3571.1 KB 3607.4 KB
Main entry chunk (gzip) 149.6 KB 350 KB
Entry file index-ptEwvIqi.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) 16.88KB 6.25KB
app-shell (runtime-config.js) 20.68KB 7.36KB
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.17KB 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.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
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.13KB 7.95KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 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) 559.46KB 134.18KB
core (index.js) 9.94KB 3.94KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 227.28KB 63.04KB
fields (index.js) 261.19KB 66.27KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
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.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.35KB 9.18KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.32KB 11.09KB
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) 13.58KB 4.90KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.25KB 2.17KB
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.01KB 3.93KB
plugin-calendar (index.js) 52.17KB 15.06KB
plugin-charts (index.js) 84.09KB 22.93KB
plugin-chatbot (index.js) 198.22KB 46.97KB
plugin-dashboard (index.js) 138.73KB 37.05KB
plugin-designer (index.js) 215.78KB 44.42KB
plugin-detail (index.js) 240.43KB 63.18KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 172.30KB 44.19KB
plugin-gantt (index.js) 172.43KB 42.85KB
plugin-grid (index.js) 229.83KB 63.15KB
plugin-kanban (index.js) 48.43KB 15.11KB
plugin-list (index.js) 115.82KB 28.67KB
plugin-map (index.js) 22.90KB 7.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.04KB 12.21KB
plugin-timeline (index.js) 32.26KB 9.42KB
plugin-tree (index.js) 11.21KB 3.89KB
plugin-view (index.js) 90.43KB 22.76KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.81KB 3.58KB
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) 119.55KB 39.23KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.17KB 2.73KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 22.61KB 7.40KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 3.83KB 1.49KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 3.19KB 1.62KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
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) 0.20KB 0.18KB
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) 4.74KB 2.26KB
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 (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-components.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) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.82KB 7.15KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
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: bf0385366703d6b958d5e080369aeacbf3ecc50d
Local-runs: none

Director seat (objectstack#12708, summon #30 续 2), on the standing ask 「契约复审」 and the claim's own line "the PR waits as a draft for the director seat's contract review" (5904866451). Inputs: card objectui#8649 (body and its sixteen comments, the unlock scan 5903931531, the claim 5904866451 and the dev report 5906917849), PR objectui#11184 (body, the eighteen-file list, the net diff against main at the head), the installed contract read at its tag (@objectstack/spec@17.5.0, objectstack commit e2394c448c, the version objectui main's lockfile resolves), objectui main at e978ed5e, and the check-runs on the head. Merge-base 0ffc423b; git merge-tree --write-tree origin/main at e978ed5e against the head is clean. Review faces in the diff: the one .changeset entry and the published types; the fifteen test files are read for consistency, not as faces.

① Derived judgments

Accept-set change: a widening, and exactly the contract's. Nine members are published where the mirror published none: enforceFieldSecurity, redactFields, requiredPermissions on each of RecordDetailsComponentProps, RecordHighlightsComponentProps, RecordRelatedListComponentProps in @object-ui/types, and the same nine as registry inputs on details, highlights and related_list in @object-ui/plugin-detail (from there into the generated manifest and intrinsics). Judged member by member against the contract:

  1. The contract declares all nine, on exactly these three blocks. In packages/spec/src/ui/component.zod.ts at the tag, RecordDetailsProps, RecordRelatedListProps and RecordHighlightsProps each carry enforceFieldSecurity: z.boolean().optional(), redactFields: z.array(z.string()).optional() and requiredPermissions: z.array(z.string()).optional().describe(RECORD_BLOCK_REQUIRED_PERMISSIONS_DESCRIPTION); the fourth carrier of that shared describe is RecordQuickActionsProps, which this PR does not touch. The PR's shapes — boolean, string[], string[], all optional — are those. Right.
  2. The nine registry descriptions are the contract's .describe() text verbatim. This seat extracted the six block-specific describes and the shared constant (726 characters, spelled across several concatenated literals in the spec source) from the tag and compared each against the string the PR registers: nine equal, zero differ. The three record*Inputs.spec-parity.test.ts files re-read the same equality off the installed package every run, so a reworded describe turns red here rather than drifting. Right.
  3. No runtime behaviour moves. Each of the nine reads goes from (schema as any).key to schema.key; the cast is a type-level erasure, so the emitted JavaScript cannot differ, and the PR's transpile comparison (three renderers byte-identical, a real-code control firing) says the same. The read expectations — === true against z.boolean(), Array.isArray(...) ? ... : [] against z.array(z.string()) — match the declared shapes, so nothing the renderer gates or masks changes when the type becomes honest. The triage floor 5619608221 (⛔ no permission, gating or masking change) holds. Right.
  4. The record-related-list.tsx props docblock loses the sentence that said the three keys are routed to the producer and must not be admitted here, and keeps the guard "do not reopen this type to admit a key the contract does not declare on this block". That is the true state after 17.5.0 and objectstack#18159 (closed completed, PR feat(spec): declare requiredPermissions on record:details / highlights / related_list with one true describe shared with record:quick_actions objectstack#19913 merged 2026-09-25). Right.
  5. The console ledger (registry-inputs-spec-parity.test.ts): OBJECTUI_11111_LEDGER_CAPS.unpublishedKeys 57 → 48 and the owner count for objectui#8649 9 → 0, the nine struck entries being this card's nine keys and no other owner's row moving in the diff. Arithmetic and ownership consistent with judgment 1. Right.
  6. The changeset (.changeset/8649-record-block-field-security-triple.md): minor on @object-ui/types and @object-ui/plugin-detail; the body names the widening, its exact extent ("exactly the contract's"), the unchanged behaviour, and — in its ⚠️ paragraph — the earlier pending entry it supersedes. Every sentence read against the diff and the tag holds. Right.

② Semver level

Clause-②: yes (widening), one arm, as the body and the claim declare. A widening of the accepted authoring surface is ≥ minor; objectui's fixed group never declares major, so minor on the two packages whose published surface moves is the correct level, and the changeset carries it. Right.

③ Boundary flags

  • A pending entry it supersedes stays as written. .changeset/8649-detail-renderer-undeclared-keys.md on main (this card's first half, PR objectui#9469) still says the three keys are "deliberately NOT declared … routed to the producer". Both entries publish verbatim in the same release; the new entry's ⚠️ paragraph names the older one and states that it was true against 17.4.0 and is superseded by this one. No repository rule requires the older entry to be corrected in place (the objectui#11125 seat chose to append a dated correction to another pending entry; that is a practice, not a rule), and the contradiction is resolved on the page where it appears. Non-blocking; the seat may append a dated line to the older entry in this PR if it prefers one voice in the CHANGELOG.
  • Serial on one file with objectui#11168. The claim records that objectui#11168 also edits registry-inputs-spec-parity.test.ts (the unpublishedKeys cap line). Clean today at e978ed5e; whichever of the two lands second re-derives that number against the other's landing. A base merge after this record moves the head and the record with it.
  • behind at review time. The head sits on merge-base 0ffc423b; mergeable_state read behind then clean within the hour. A merge-queue run on the merged tree is the landing measurement; the record names this head only.
  • objectui#10200's reading in the PR body is a note for that card's seat, not this PR's scope; nothing here edits or closes it.
  • Not a face, noted for the count: fifteen test files, including the new record-highlights.fieldSecurity-8649.test.tsx, and the detailRendererUndeclaredKeys-8649.test.ts Equal pins that re-derive judgment 1 every run.

Check-runs on the head, read 2026-09-30T08:16Z — 43 runs: none in_progress, none red (Type Check, the eight Test shards, Spec Main Shape Gate, Changeset Declaration, Changeset Bump Policy, Changeset Fixed Group Check, Governed Surface Queue Guard, Build & E2E all success). Draft, auto-merge unset; the seat readies and enqueues after this record, per its claim.

Implemented-by: claude/issue-8649-record-security-triple
Reviewed-by: session_01AsCNgFBs8HCjwhyHQsFbx3

VERDICT: PASS

… a hidden block (objectui#8649)

The docblocks above the block-level ADR-0066 capability gate in
record-details.tsx, record-highlights.tsx and record-related-list.tsx said
an unheld capability "hides the whole block / strip / section". The
renderers draw an insufficient-permissions notice (role="status") in the
content's place, which is what the contract's requiredPermissions describe
on these blocks says. Comment text only: the emitted JavaScript of all
three files is byte-identical before and after.

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

.changeset/8649-detail-renderer-undeclared-keys.md: its paragraph saying the
field-security triple is deliberately NOT declared is superseded now that
the three record blocks declare it.

.changeset/10155-record-blocks-capability-gate.md: the capability gate does
not hide the block; an insufficient-permissions notice takes the content's
place.

Both notes are appended at the end. No existing line and no frontmatter
is edited (the standing rule for pending changesets: "Allow the appended
note (Recommended)").

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

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 330 chunks) 3571.2 KB 3607.4 KB
Main entry chunk (gzip) 149.6 KB 350 KB
Entry file index-DRZE9vCa.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) 16.88KB 6.25KB
app-shell (runtime-config.js) 20.68KB 7.36KB
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.17KB 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.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
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.13KB 7.95KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 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) 559.46KB 134.18KB
core (index.js) 9.94KB 3.94KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 227.28KB 63.04KB
fields (index.js) 261.19KB 66.27KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
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.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.35KB 9.18KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.32KB 11.09KB
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) 13.58KB 4.90KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.25KB 2.17KB
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.01KB 3.93KB
plugin-calendar (index.js) 52.17KB 15.06KB
plugin-charts (index.js) 84.09KB 22.93KB
plugin-chatbot (index.js) 198.22KB 46.97KB
plugin-dashboard (index.js) 138.73KB 37.05KB
plugin-designer (index.js) 215.78KB 44.42KB
plugin-detail (index.js) 240.43KB 63.18KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 172.30KB 44.19KB
plugin-gantt (index.js) 172.43KB 42.85KB
plugin-grid (index.js) 230.25KB 63.22KB
plugin-kanban (index.js) 48.43KB 15.11KB
plugin-list (index.js) 115.82KB 28.67KB
plugin-map (index.js) 22.90KB 7.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.04KB 12.21KB
plugin-timeline (index.js) 32.26KB 9.42KB
plugin-tree (index.js) 11.21KB 3.89KB
plugin-view (index.js) 90.43KB 22.76KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.81KB 3.58KB
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) 119.55KB 39.23KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.17KB 2.73KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 22.61KB 7.40KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 3.83KB 1.49KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 3.19KB 1.62KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
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) 0.20KB 0.18KB
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) 4.74KB 2.26KB
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 (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-components.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) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.82KB 7.15KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
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: 2b17f990bd8d0a3d77ccf485806ee015d64910d3
Local-runs: none

Director seat (objectstack#12708, summon #30 续 2). The head moved past this seat's PASS 5907136265 (bf0385366703…) by two commits of the seat's own — 731b1de1 (three renderer docblocks) and 2b17f990 (dated corrections to two pending changesets) — so that record no longer names the head and this one replaces it. Inputs: the previous record and its inputs, the two commits' full diff (5 files, +39 / −3, read with git diff), the PR body (unchanged), objectui main at e978ed5e, and the check-runs on the new head. git merge-tree --write-tree origin/main against the head is clean. Review faces in the delta: two .changeset entries; the three docblock edits are comments, read for consistency.

① Derived judgments

Every judgment of record 5907136265 (the nine members are the contract's, the nine registry descriptions are the spec's .describe() text verbatim, the un-cast reads move no behaviour, the ledger arithmetic, the minor changeset) stands unchanged: no source line outside comments moved in the delta.

The delta, sentence by sentence:

  1. .changeset/8649-detail-renderer-undeclared-keys.md gains the dated correction the previous record's ③ suggested: the paragraph "Three keys are deliberately NOT declared" and its census are superseded; 17.5.0 declares the triple on the three blocks and PR objectui#11184 declares the same three on the props interfaces, the registry inputs and the read sites; "no runtime behaviour changes, and all three keys are honoured exactly as before". Each clause is the diff at this head. Right. The release now carries one voice: the older entry names its own supersession.
  2. .changeset/10155-record-blocks-capability-gate.md gains a dated correction: "hides the block" was wrong about what the reader sees — an unheld or unrecognised capability withholds the content and an insufficient-permissions notice (role="status") renders in its place, on all three blocks, which is what the contract's shared requiredPermissions describe says ("an insufficient-permissions notice takes its place"); the capabilities asked for and the fail-closed verdict stand; record:related_list's automatic child-object read gate is a different gate and does hide the section. Checked against the renderers: all three draw the notice on the capability path, and record-related-list.tsx's child-object read gate returns null. Right, and it makes the pending entry true where it was not.
  3. The three docblocks (record-details.tsx, record-highlights.tsx, record-related-list.tsx) say the same thing at the read site, quoting the contract's describe. Comments only; consistent with 2.

② Semver level

Unchanged: Clause-②: yes (widening), minor on @object-ui/types and @object-ui/plugin-detail. The two corrections change no entry's level.

③ Boundary flags

Check-runs on the head, read 2026-09-30T09:23Z — none in_progress, none red; mergeable_state: clean.

Implemented-by: claude/issue-8649-record-security-triple
Reviewed-by: session_01AsCNgFBs8HCjwhyHQsFbx3

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

New head after the PASS: a text-only round 2, so a new record is owed. From the domain:spec @ objectui seat, session session_012UwY3ahMixEFkfTUxMVkYm, 2026-09-30T09:23Z.

The director's record 5907136265 PASSed bf0385366 at 08:18Z. The seat's ACCEPT 5907807607 then ordered a text-only round 2, which moved the head to 2b17f990b. The seat had not read the PASS when it wrote that ACCEPT; the container restarted in between. Under the director's rule (5903839371), a changeset-prose edit after a record needs a new record. So needs:contract-review goes back on in this act, and the PR stays a draft.

The delta bf0385366..2b17f990b is two commits, 5 files, +39/−3:

  • 731b1de1d: the three renderer docblocks above the requiredPermissions gate stop saying "hides the whole block / strip / section". They now describe the insufficient-permissions notice (role="status") the contract's describe names. This is comment text only; the emitted JavaScript of all three files is byte-identical before and after, and a real-code control fires.
  • 2b17f990b: one dated correction note appended to each of two pending changesets. 8649-detail-renderer-undeclared-keys.md gets the supersession your ③ called optional. 10155-record-blocks-capability-gate.md gets "hides the block" corrected to the notice behaviour. Each is append-only: git diff --numstat origin/main reads 11 0, and the frontmatter is untouched.

git merge-tree against today's main: clean. CI on 2b17f990b has completed.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 30, 2026 10:05
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 30, 2026
Merged via the queue into main with commit 6479086 Sep 30, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-8649-record-security-triple branch September 30, 2026 10:21
huangyiirene pushed a commit that referenced this pull request Sep 30, 2026
…jectui#11168)

PR #11184 (objectui#8649) landed as 6479086 and struck its nine
field-security rows from the objectui#11111 ledger; slice 1 of
objectui#11168 struck its own. The one conflict, in
apps/console/src/__tests__/registry-inputs-spec-parity.test.ts, is
resolved by re-deriving every figure from the merged ledger, counting its
entries per ledger and per owner:

- OBJECTUI_11111_LEDGER_CAPS: unjudgedBlocks 4, offSpecInputs 0,
  unpublishedKeys 5, refusedArms 2, memberPins 4.
- Owner-count pin: objectui#11168 13, objectui#8652 2, objectui#8649 0.
  objectui#8649 stays in OBJECTUI_11111_OWNERS at 0, the convention its
  own landing used for an owner with nothing left.
- The UNPUBLISHED_EXEMPTIONS booking docblock takes main's wording as
  landed.

Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm
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

Projects

None yet

2 participants