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
Conversation
…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>
|
changeset-claim-re-read
|
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: 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 ① Derived judgmentsAccept-set change: a widening, and exactly the contract's. Nine members are published where the mirror published none:
② Semver level
③ Boundary flags
Check-runs on the head, read 2026-09-30T08:16Z — 43 runs: none Implemented-by: 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>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: Director seat (objectstack#12708, summon #30 续 2). The head moved past this seat's PASS 5907136265 ( ① Derived judgmentsEvery judgment of record 5907136265 (the nine members are the contract's, the nine registry descriptions are the spec's The delta, sentence by sentence:
② Semver levelUnchanged: ③ Boundary flags
Check-runs on the head, read 2026-09-30T09:23Z — none Implemented-by: VERDICT: PASS |
|
New head after the PASS: a text-only round 2, so a new record is owed. From the The director's record The delta
|
…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>
Fixes #8649
Clause-②: yes — declaring
enforceFieldSecurity,redactFieldsandrequiredPermissionson the three record blocks' published props types and registry inputs widens the accepted authoring surface to exactly what@objectstack/spec17.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, onrecord:details,record:highlightsandrecord:related_list. PR objectui#9469 (Part of) took the other three reads and routed these nine to the platform as objectstack#18159. That card closedcompletedwith PR objectstack-ai/objectstack#19913 (ruling A), andmainnow installs@objectstack/spec17.5.0, so the routing has expired into "align the mirror":@object-ui/types:RecordDetailsComponentProps,RecordHighlightsComponentPropsandRecordRelatedListComponentPropseach gainenforceFieldSecurity?: boolean,redactFields?: string[]andrequiredPermissions?: string[].record-details.tsx,record-highlights.tsxandrecord-related-list.tsxlose their(schema as any)cast. For example,(schema as any).enforceFieldSecurity === truebecomesschema.enforceFieldSecurity === true. Therecord-related-list.tsxprops 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.packages/plugin-detail/src/index.tsx): each of the three blocks publishes the three keys. The types are the contract's (boolean, orarrayofstring), and each description is that block's own.describe()text from the installed spec, verbatim.registry-inputs-spec-parity.test.ts): this card's nineOWED TO objectui#8649entries are struck.OBJECTUI_11111_LEDGER_CAPS.unpublishedKeysgoes 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.minoron@object-ui/typesand@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
mainbefore any edit (Partition 2)1. What 17.5.0 declares. The installed
node_modules/@objectstack/spec/package.jsonreads17.5.0. Two instruments were run over the contract's own block-tag mapComponentPropsMap, and each was self-tested on known schemas first:unrecognized_keys..shape.user:profile. Its props schema isz.never(), so it refuses every key withoutunrecognized_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:
enforceFieldSecurityisz.boolean(),redactFieldsisz.array(z.string())andrequiredPermissionsisz.array(z.string()). All three are optional with no default. TheenforceFieldSecurityandredactFieldsdescribes are block-specific. TherequiredPermissionsdescribe is the one ruling5798783314shares word for word withrecord:quick_actions.⇒ Premise holds: all nine keys are declared, on exactly those three blocks.
2. The read sites. The checker (membership, via
getPropertyOfTypeon each binding) and an expression probe (getTypeAtLocationon each read) were run over the three renderers. The probe refuses to report while anyTS2307is 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.requiredPermissionsandredactFieldsare each read twice in their ternary) are cast, typedany, and are not members of the binding.boolean | undefined,string[] | undefined, orstring[]inside theArray.isArraynarrowing.=== trueagainstz.boolean(), andArray.isArray(...) ? ... : []into astring[]againstz.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:The
@object-ui/typeschange is interface-only.4. Registry inputs. The three blocks'
inputslive inpackages/plugin-detail/src/index.tsx.recordDetailsInputs.spec-parity.test.tsand 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:RecordDetailsPropsaccepts all three keys, by both instruments above, with value-level parses in the new pins.requiredPermissions) was withdrawn by ruling A on objectui#10281.Tests
New and updated pins:
detailRendererUndeclaredKeys-8649.test.tsis this card's own pin. It now carries:Equaltype pins for each block: the mirror members equal the contract'sComponentPropsInputmembers, spelled out, and each renderer binding carries them.record-details.hideFieldsUncast-9965.test.tsandrecord-related-list.relationshipValueFieldUncast-9475.test.tsxnow have empty cast ledgers, as both files said this card's landing would do.anyreads, 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 asany, an un-cast read asstring[] | undefined, and an undeclared read asany.record-related-list.propsRefusal-9963.test.tsx: the three refusal rows become acceptance rows, with wrong-type and misspelling refusals kept as@ts-expect-errorcontrols.record*Inputs.spec-parity.test.tsfiles 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 atredactFields.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 ifRecordRelatedListPropsdeclares the key. Per triage carry5627847529, that file is still not cited as a declaration.record-highlights.fieldSecurity-8649.test.tsxis new. No test droveredactFieldsorenforceFieldSecurityonrecord:highlights. It pins which chips paint their value, with a control for each row.Behaviour unchanged, cited:
record-blocks.requiredPermissions-gate.test.tsx, under a realMePermissionsProvider.record-details.unresolvedIdentityFailClosed-9054.test.tsx(details);RecordRelatedListRenderer.redactedDerivation-9053.test.tsxandRecordRelatedListRenderer.unresolvedIdentityFailClosed-8793.test.tsx(related list);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 issummaryChip.dateOnlyZone-10183.test.tsx, which carries its own skip condition and is not touched here.Test Files 11 passed (11),Tests 452 passed (452). The files are the eightapps/consoletests that name the three blocks,RecordDetailView.pageHeaderTitleFls-10499.test.tsx,public-tier.test.tsandcheck-handler-key-read-sites.test.ts.@object-ui/types:pnpm --filter @object-ui/types run type-check(includingtsconfig.test.json) exits 0, andpnpm --filter @object-ui/plugin-detail run type-checkexits 0.@object-ui/typesexits 0 with 0 errors, and@object-ui/plugin-detailexits 0 with 0 errors.eslint --format jsonon 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 HEADemptyAll 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.schema.enforceFieldSecurity === truebecame(schema as any).enforceFieldSecurity === trueinrecord-highlights.tsx.enforceFieldSecurityun-cast leg goes red, and the emitted JS does not move.Tests 1 failed | 26 passed (27), and the failing case was that leg.record-highlights.tsx js IDENTICALunder the mutation, so the source-text pin is the only instrument that can see a re-cast.requiredPermissionsreads inrecord-related-list.tsxwas re-cast (? schema.requiredPermissionsbecame? (schema as any).requiredPermissions), so the liveness half still holds.Tests 2 failed | 35 passed (37). The first failure was "expected ... not to match" the cast matcher; the second wasexpected [ 'requiredPermissions' ] to deeply equal [].enforceFieldSecuritywas removed fromRecordDetailsComponentProps, then@object-ui/typeswas rebuilt, becauseplugin-detail'stscresolves it throughdist/.record-details.tsxstill compiles through its index signature, and the 9965 program guard goes red on the membership leg and the triple-type leg.dist/record-components.d.tswent from 3 to 2.tsc -p tsconfig.test.jsonexited 2 with six errors, all indetailRendererUndeclaredKeys-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.Tests 2 failed | 8 passed (10)on exactly the two predicted legs.record:highlightsredactFieldsinput was replaced by a comment.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, andevery 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:
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.check:control-bytes,check:new-line-citations("0 new citation(s)"),check:spec-symbols,check:handler-key-reads,check:installed-pin-claims.check-governed-queue-guard --testover the diff reads "NOT GOVERNED", and--self-testpasses.check-type-check-coverage,check-lint-coverage.check:test-path-roots,check:vi-mock-specifiers,check:vi-mock-inherit,check:vi-mock-override-shape.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 orsideEffectsentry.check:spec-floors -- --cross-checkandcheck:published-dist: both need a full-repo build, so they are left to CI.pnpm lintand the fullpnpm testfarm are CI's runs.Acceptance notes
requiredPermissionsinputs on the three record blocks, and therecord-related-list.tsxdocblock in carrier note5826463187. Both are done here. Itsrecord:quick_actionsdescription ask is not in this PR's scope, so objectui#10224 remains open for that ask. The seat can narrow it..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.unpublishedKeyscap and the owner-count pin, and whichever PR lands second mergesmainand re-derives them. The57in the bookings comment is now worded as "57 at the bump" and points at the cap constant, so it no longer needs re-deriving.record:quick_actions,record-reference-rail.tsx.requiredPermissionsgate 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'srecord:quick_actionsdescription ask, which has the same wording drift one block over.Round 2: text only, head
2b17f990bThis round follows the seat's ACCEPT
5907807607. It adds two commits on top ofbf0385366, with no rebase, no force-push and no rewrite of a pushed commit.731b1de1d, docs(plugin-detail). The docblocks above therequiredPermissionscapability gate inrecord-details.tsx,record-highlights.tsxandrecord-related-list.tsxsaid 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'srequiredPermissionsdescribe 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 returnsnull). 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:
git diff --numstat origin/main -- FILEreads11 0for each changeset. The first 4927 bytes (8649) and the first 2269 bytes (10155) of the new files are byte-identical to the blobs onmain(cmpexit 0), so no existing line and no frontmatter moved.check-changeset-overwritelists both as modified, with the same declarations at base and now.removeComments,bf0385366against2b17f990b):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.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, andcheck:new-line-citations("0 new citation(s)").check-shell-escape-residue,check-doc-links, andcheck-governed-queue-guard --testover the five paths ("NOT GOVERNED"). Each exits 0.Round 2 acceptance notes:
*. It is left as is, because it is comment formatting outside this round's wording ask.record-blocks.requiredPermissions-gate.test.tsxstill 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