Repository navigation
feat(spec): an app page binds its row as record, so visibleWhen can follow a bound count or a plan field - #24275
Conversation
…ollow a bound count or a plan field Claude-Session: https://claude.ai/code/session_012SGQNHx8abCKZKoyj2VQym Co-authored-by: Claude <noreply@anthropic.com>
…fy it planned in the liveness ledger Claude-Session: https://claude.ai/code/session_012SGQNHx8abCKZKoyj2VQym Co-authored-by: Claude <noreply@anthropic.com>
… dropped-refinement totals Claude-Session: https://claude.ai/code/session_012SGQNHx8abCKZKoyj2VQym Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012SGQNHx8abCKZKoyj2VQym Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012SGQNHx8abCKZKoyj2VQym Co-authored-by: Claude <noreply@anthropic.com>
… a plan is read Claude-Session: https://claude.ai/code/session_012SGQNHx8abCKZKoyj2VQym Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 143 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 9f7e67e43cba1caac54eaa82dbe02286f790647b && git checkout 9f7e67e43cba1caac54eaa82dbe02286f790647b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 7a8733f1c77681137d25abb1c02b5c889b71c01e 88dbe4191ecbaddd12b68193a6e286e2cb4b5a32 && git checkout -B drift-repro 7a8733f1c77681137d25abb1c02b5c889b71c01e && git merge --no-ff 88dbe4191ecbaddd12b68193a6e286e2cb4b5a32
node scripts/docs-audit/affected-docs.mjs --json 7a8733f1c77681137d25abb1c02b5c889b71c01e
|
Contract reviewServed-tier: Inputs: card #24265 (body, claim 6246461293, dev report 6247235785), PR #24275 (body, its 16 files, the net diff against main 93b4acb), and the head's 39 check-runs, read at 2026-10-18T07:54Z. The head had not moved past a150ec5 at that read. The rest of the diff (the generated artifacts, content/docs/references/**, the strictness-ledger counts) is the lane seat's ACCEPT at TIER_DEFAULT. ① Derived judgments
② Semver level
③ Boundary flagsDev deviations, each answered:
Flags of my own:
Implemented-by: VERDICT: PASS Generated by Claude Code |
…inding; name the renderer card on its planned row The served page schema gained one declared key, so the derivation control moves 25 -> 26 the way it moved for print. The pages guide's visibleWhen row no longer calls the retired visibility spelling accepted. Claude-Session: https://claude.ai/code/session_012SGQNHx8abCKZKoyj2VQym Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012SGQNHx8abCKZKoyj2VQym Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012SGQNHx8abCKZKoyj2VQym Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Inputs, read at 2026-10-18T09:12Z: card #24265 (body; comments 6246461293 claim, 6247235785 dev report, 6247697989 patch round 1 report; no round-2 report was on the card at this read), PR #24275 (body, its 17 files, the net diff against main at merge-base 9822b05), and the head's 40 check-runs. The head had not moved past 88dbe41 at this read. This record replaces comment 6247358877 (PASS on a150ec5) and is complete on its own; where the move left a judgment of that record untouched it is carried here by name. The remainder of the diff (content/docs/references/index.mdx, content/docs/references/ui/page.mdx, docs/audits/2026-07-unknown-key-strictness-ledger.counts/ui.md: generated, and read here as consistent with their sources) is the lane seat's ACCEPT at TIER_DEFAULT. What moved since a150ec5, measured: the branch's own commits are 03b3e5a (three files: the metadata-protocol census, content/docs/ui/pages.mdx, liveness/page.json), the merge commit 6480ad0 (origin/main 9822b05 into the branch) and 88dbe41 (one file: authorable-surface/ui.json, three lines). packages/spec/src/ui/page.zod.ts is byte-identical between a150ec5 and this head. ① Derived judgments
② Semver level
③ Boundary flagsDev deviations and
Flags of my own, for the seat, none a FAIL reason:
Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对 · ACCEPT · PR #24275 @
|
Fixes #24265
Clause-②: yes (widening)
Summary
An
apppage can now declare the row it is about:PageSchema.record, a one-row query{ object, view?, filter?, sort? }. Every componentvisibleWhenon the page reads that row asrecord, which is already a contract-bound root on this surface. The count and the tier are both fields on that row: asummaryroll-up and the bound object's own plan field. No new expression root is added, the lint's declared roots do not move, and nothing only the control plane could fill is named.The renderer half is objectstack-ai/objectui#12931. Until the console mounts the binding,
recordstays unbound on anapppage, exactly as before. The liveness ledger grades the keyplanned, citing the parse-time refusal as its code pointer.The card's three questions, measured
All objectstack readings are on
origin/maina422f04fe. All objectui readings are on objectuimain79cb8c03eb.Q1: the root for an
apppage's bound row → the existingrecord, given a page-level bindingMeasured:
SchemaRenderer'sconfigEvaluationScope(packages/react/src/SchemaRenderer.tsx) bindsrecordfromRecordContext(boundRecord = recordContext?.data) and bindspagefrom page variables. It binds nothing else.apppage, the only provider of that context is a record block's own one-row binding (plugin-detail/src/renderers/recordBinding.tsx,BoundRecordScope). That provider wraps the block's body, so neither the block's ownvisibleWhennor a sibling's ever sees the row. H2 holds: "the row its page binds" was not a page-level concept at all. What actually existed was "a row one block binds, private to that block".PageVariableSchema.sourcenames an interactive writer, such aselement:record_pickerorelement:text_input, which write a selection. No block publishes data into a variable (H3, measured).PageSchema.record→ existingrecordroot (chosen)sort, read as the caller, re-read on write), so the renderer gets no second pathlimit,idandrecordIdare refused with prescriptions. The key is refused on every non-apppage. Still aplannedwindow until objectui#12931 lands, the same posture asElementApiDataSourceSchemaand the formerprintrowpage.NAMEsourcemeans (selection writer → data publisher). A variable is empty until the block loads, which gives a second timing modelbound.*or similar)recordanddata, against ADR-0089 D1's per-layer rootQ2: tier without a cloud-only root → a field on the bound row, not
featuresMeasured (H4):
features.Xalready passes the lint, becausefeaturesis in@objectstack/formula'sSCOPE_ROOTS.featuresfrom/api/v1/auth/config.features, inAppContent.tsxandRecordFormPage.tsx.PUBLIC_AUTH_FEATURES(packages/spec/src/kernel/public-auth-features.ts). It is a closed, drift-guarded registry of deployment-level auth-capability flags, served to anonymous clients by plugin-auth'sgetPublicConfig().@objectstack/formula'sbuildScopedoes not fill it at all.So an organisation's plan in
featureswould need a per-organisation flag that open plugin-auth cannot know. Only the control plane could fill it, which is exactly the card's ⛔. Promotingfeaturesto contract-bound would be spec text only, but it would still not carry a tier. The answer isrecord.plan != 'free'(or whatever field the bound object carries). ThevisibleWhendocblock, its describe and the pages guide now say outright that a plan is notfeatures.*.featuresstays ambient and unpromoted.record). Docs refuse thefeaturesreading by namefeatures, map the plan to flagsgetPublicConfig()featuresas deployment config and the plan as org data, two meanings under one rootQ3: the count → a
summaryroll-up on the bound row, through Q1's rootsummaryis a spec field type (data/field.zod.ts). "A second environment exists" isrecord.environment_count > 1on the organisation row. This needs no root and no spec change beyond Q1. The docblock names the pattern.ADR check (H5)
record/org/envnamespaces out. Nothing here touchescurrent_user.record. That is the root this uses, unchanged.Neither ADR is contradicted.
What changed
packages/spec/src/ui/page.zod.ts:PageRecordBindingSchema(strict; aliases pluslimit/id/recordIdguidance).PageSchema.recordwith aliases (boundRecord,recordBinding,recordSource,dataSource).checkPageRecordBindingPlacement: refusesrecordunlesstype: 'app', with a separate sentence for a record page. It is exported and attached by identifier like the six sibling checks.visibleWhendocblock and describe: the row's meaning, the sibling limit, and count/plan as fields, notfeatures.PageRecordBinding/PageRecordBindingParsedtype aliases.PageComponentSchema. That ordering keeps the generatedskills/objectstack-ui/references/_index.md(a governed path) byte-identical.object-refinement-check-exports.test.tsadds the new export: six fixtures (app accept, no binding, home, record, untyped default, list), attachment counts and barrel identity.metadata-form-zod-reconciliation.test.tsadds a root ledger entry forrecord(planned, on theprintprecedent).liveness/page.jsonadds arecordrow,planned, drilled toobject/view/filter.{field,operator,value}/sort.{field,order}.dropped-refinements.baseline.jsongets the newui/PageRecordBindingsite,ui/Page's firstViewFilterRulesite moved frominterfaceConfig.filterBytorecord.filter, and totals 237 / 768.authorable-surface,api-surface,export-origins,declaration-map,json-schema.manifest,liveness/state-counts, the strictness-ledger counts, andcontent/docs/references/**.content/docs/ui/pages.mdx: therecordproperty row and thevisibleWhenrow..changeset/24265-app-page-record-binding.md:@objectstack/specminor, with theClause-②line.Not touched:
packages/lint(the root set did not move:recordandpagewere already declared),packages/formula, objectui,.objectui-sha,rule-explanations.ts,component.zod.ts,content/docs/releases/.Verification
These runs were on head
a150ec532f. CI then went red there in@objectstack/metadata-protocol: the census inprotocol.meta-types-degenerate-derivation.test.tspinned 25 top-levelpageproperties. Patch round 1 (03b3e5ad4b, report6247697989) moved that census to 26 forrecord. The merge ofmainafter PR #24270 (6480ad0b6, with its regeneration commit88dbe419) followed. The head's check-runs are its verification. Contract review PASS6248097604read that head.pnpm --filter @objectstack/spec buildexited 0.check:generatedreported "All 14 generated artifacts are up to date".pnpm --filter @objectstack/spec test(local project): 693 files, 21015 passed, 1 todo.pnpm --filter @objectstack/spec test:repo: 68 files, 1125 passed.pnpm --filter @objectstack/spec typecheckexited 0.node scripts/pm/dispatch-gates.mjs --commands: 111 derived commands, all run, all exit 0. Seven first exited 3 on prerequisites (an unbuilt closure, a shallow clone). They exited 0 afterpnpm --filter '@objectstack/lint...' --filter '@objectstack/client-react...' --filter '@objectstack/objectql...' build, a fullturbo run buildandgit fetch --unshallow.--ranreconciliation: 111 derived, 111 run, 0 NOT-MEASURED, 0 UNRUN.check-changeset-no-major --eventwith this body: no major bump, and the level axis acceptsClause-②: yes (widening)with@objectstack/specatminor.scripts/pm/clause2-line.mjsreads the same line from this body and from the changeset.check:livenessis green, with therecordrowplannedand drilled.scripts/ablation-replace.mjswith a trap restore. Thetype === 'app'accept arm ofcheckPageRecordBindingPlacementwas changed so it can never match.object-refinement-check-exports.test.tsthen failed exactly the accepting fixture ("arecordbinding on anapppage": expected[ 'record' ]to deeply equal[], 1 failed / 251 passed). The file was restored with blob == HEAD and an emptygit diff HEAD.Acceptance notes
content/docs/ui/pages.mdx'svisibleWhenrow used to say "Legacy aliasvisibilityis accepted but deprecated per ADR-0089". At 18 the key is aretiredKey()tombstone. Patch round 1 corrected the row: the retired spelling is refused at parse (ADR-0089 D2). This was review flag F1.record.*read on anapppage that declares no binding. Adding a gate defaults to no, and the predicate faults open there exactly as before.Generated by Claude Code
🤖 Generated with Claude Code
https://claude.ai/code/session_012SGQNHx8abCKZKoyj2VQym