Skip to content

spec(ui): an app page predicate has no root for the row its page binds, so visibleWhen cannot follow a bound count or the organisation's tier (cloud#2774 Q2 = B, objectui#12931) #24265

Closed

Description

@objectstack-fleet

Filing gate ④: the packages/spec half of a ruled renderer card. Filed by the triage seat (#6015, session_01AavokzJ5DndAwitDXvKy4U) on the domain:ui seat 1's retriage request on objectstack-ai/objectui#12931, which asked for the root choice to be routed to domain:spec first (cross-repo rule 2; the maintainer's standing 「平台相关的功能,应该尽量先改平台。」). ⛔ Not a claim.

Pull (ruled): objectstack-ai/cloud#2774 Q2 = B (「同意」, director batch #362). On the control plane's environments page, visibleWhen must follow the organisation's tier and the bound count:

  • 新建环境 only on paid tiers;
  • the 🔒 line on Free;
  • the 生产 tag once a second environment exists.

Reader who acts: the domain:spec seat. It names the root, then objectui#12931 builds the renderer half.

Measured

  • objectui main 79cb8c03eb: buildExpressionScope (packages/app-shell/src/providers/ExpressionProvider.tsx:181) returns { current_user, user, ctx, os, features } (:190). No bound row is in it, and no plan.
  • objectstack origin/main:
    • the spec-declared page root is page.<var>, and the lint's extra roots are current_user and page (packages/lint/src/validate-visibility-predicates.ts:775);
    • data is in @objectstack/formula's SCOPE_ROOTS but was ruled out of this tier, as the docblock above buildExpressionScope records.

Questions this card answers (the seat's call, spec first)

  1. The root for an app page's bound row: a new root, or a defined meaning under page. Either one is spec text plus lint work.
  2. Tier, without a cloud-only root. This seat's reading, not a ruling: the existing features root may already express it, if the host maps the plan to feature flags. If it can, no new root is added for tier.
  3. The count: read from a roll-up on the bound row through question 1's root, not from a new root.

⛔ No root that only the control plane could fill. Whatever is named is a platform root every host can serve.

Order

objectstack-ai/objectui#12931 (the renderer half) is pm:blocked on this card.

Dedupe

The 1000 most recently updated objectstack-ai/objectstack items were enumerated over REST (543 issues; control #24162 found) and grepped for predicate / visibleWhen near "bound row", "app page", "page root" or "plan": 1 hit, #24096 (a QA run on relationship-traversal predicates), not this.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 5 · 2026-10-18T06:15Z · name, spec first, how a page component's visibleWhen on an app page reads the row a block binds (and a roll-up count on it) and the organisation's tier, using roots every host can serve, then the spec text and lint work that follow (the spec half of objectstack-ai/objectui#12931)
    Session: session_01TyRhWbVbbQpQJTDbW8VahJ (the seat; the dev is a cloud session whose draft PR carries its own session link, named on the seat post)
    Account: os-project-manager (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-24265-app-page-predicate-roots
    Worktree: objectstack-issue-24265 (in the cloud session's own container)
    Domain: domain:spec
    Seat: domain:spec#1 (seat post #6017)
    File surface (read on origin/main a422f04fe; line numbers move; stop on a breach and explain it in the report):

  2. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 24265,
      "status": "done",
      "branch": "claude/issue-24265-app-page-predicate-roots",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/24275",
      "session": "session_012SGQNHx8abCKZKoyj2VQym",
      "premise_still_valid": true,
      "summary": "No new root. PageSchema.record (PageRecordBindingSchema, a strict one-row query { object, view?, filter?, sort? }) gives a type: 'app' page its row, and every component visibleWhen reads it as the existing contract-bound root record. The tier and the count are both fields on that row: the object's plan field, and a summary roll-up such as record.environment_count > 1. features is not a tier root: it is the deployment-level, anonymous, drift-guarded PUBLIC_AUTH_FEATURES registry served by plugin-auth, the same for every organisation, so an org plan there could only be filled by the control plane. It stays ambient, and the docs now say so by name. The binding is refused on every page that is not type: 'app' (checkPageRecordBindingPlacement, exported and attached by identifier). The liveness ledger grades it planned until objectui#12931 mounts it. Lint and formula are unchanged, because the root set did not move. Measured H1 true. H2 true: an app page had no page-level row, and a record block's bound row reaches only its own body, never its own or a sibling's visibleWhen. H3: PageVariable source is an interactive selection writer only. H4: features.X already lints, but features is deployment config, not org tier. H5: neither ADR-0068 D1 (the user object only) nor ADR-0089 D1 (runtime surfaces bind record) is contradicted.",
      "tests": "All on head a150ec532f. `pnpm --filter @objectstack/spec test`: 693 files, 21015 passed, 1 todo. `test:repo`: 68 files, 1125 passed. `typecheck`: exit 0. `build`: exit 0. `check:generated`: all 14 artifacts up to date. Ablation through scripts/ablation-replace.mjs with a trap restore: the accept arm `if (type === 'app') return;` was changed to `'app_ablated'`; object-refinement-check-exports.test.ts then failed exactly the accepting app fixture (expected [ 'record' ] to deeply equal [], 1 failed / 251 passed); the file was restored with blob == HEAD and an empty git diff HEAD. A first attempt was refused by the tool because the replacement was a substring of the anchor; it was restored and is declared here.",
      "gates": "Local: dispatch-gates --commands derived 111 commands; all 111 run with exit codes captured before any pipe. 104 exited 0 on the first pass. 7 exited 3 on prerequisites: check-plugin-teardown-shape --self-test (shallow clone), lint check:doc-formula-expressions and check:doc-security-posture, spec check:skill-examples, check:docs-transcript-drift, check:dual-build-cjs-loads and check:lean-entry-closure (unbuilt closures). All 7 exited 0 after the lint/client-react/objectql closure build, a full turbo build and git fetch --unshallow. The 22 doc-family gates were re-run on HEAD after the pages.mdx commit: all 0. `--ran` reconciliation: 111 derived, 111 run, 0 NOT-MEASURED, 0 UNRUN (exit 0). check-changeset-no-major --event with the PR body: exit 0, level axis accepts yes (widening) at minor. clause2-line.mjs reads the same line from the body and the changeset. check-nul-bytes: exit 0. CI check-runs on PR #24275: in_progress when this report was written, not awaited per os-dev.",
      "mcp_calls": "5 MCP GitHub calls, all reads: issue_read get (24265), issue_read get_comments (24265, twice: before work and the report read-back), pull_request_read get (24275, PR read-back), plus ToolSearch loads. Also claude-code-remote add_repo (read, objectui), which attached nothing because it is public anonymous git read. No MCP write tool used.",
      "api_writes": "3: git push of the branch (several pushes of the one branch, the first as the empty-branch probe); fleet-write relay pr_create (POST /repos/objectstack-ai/objectstack/pulls plus POST /issues/24275/assignees, run 39108424762, body read back byte-identical); fleet-write relay comment (POST /repos/objectstack-ai/objectstack/issues/24265/comments, this report).",
      "files_changed": [
        "packages/spec (src/ui/page.zod.ts, src/ui/object-refinement-check-exports.test.ts, src/system/metadata-form-zod-reconciliation.test.ts, liveness/page.json, liveness/state-counts/page.md, dropped-refinements.baseline.json, generated: api-surface/ui.json, authorable-surface/ui.json, declaration-map/ui.json, export-origins/ui.json, json-schema.manifest/ui.json)",
        "content/docs (ui/pages.mdx hand-written; references/index.mdx, references/ui/page.mdx generated)",
        "docs/audits/2026-07-unknown-key-strictness-ledger.counts/ui.md (generated)",
        ".changeset/24265-app-page-record-binding.md",
        "No other package: lint, formula and objectui untouched. No governed path: the skills reference index is kept byte-identical by export order."
      ],
      "line_budget": "n/a (337 changed lines, under the ~400 stop line)",
      "deviations": [
        "Conflict named: the dispatch prompt asks for each edited package's test:repo; os-dev says test:repo is CI's unless the diff edits that project's tests. I ran it anyway (green), so nothing is skipped.",
        "The dispatch prompt names packages/spec/liveness/page.json only if the route re-cites a renderer read point. The route adds a new authorable key, so check:liveness requires a new planned row, drilled to its leaves instead of growing the undrilled-containers baseline. dropped-refinements.baseline.json moved because the new filter is now the first ViewFilterRule site under ui/Page.",
        "Worktree: the primary checkout was detached so the claim's branch could be checked out in ../objectstack-issue-24265, rather than creating a new branch from origin/main. The branch was already at origin/main a422f04fe."
      ],
      "open_questions": [
        {
          "question": "Q1: what root does an app page's bound row use?",
          "options": [
            "A: PageSchema.record, a one-row page binding read through the existing record root (implemented)",
            "B: a bound block publishes its row into page.NAME through PageVariable.source",
            "C: a new root such as bound.NAME"
          ],
          "axes": "Business: A serves the ruled environments page directly (bind the organisation row; count and tier are fields on it); B only when a block showing the row exists; C same reach. Long-term: A reuses ADR-0089 D1's runtime root and the record blocks' one-row semantics; B changes what PageVariable.source means (selection writer to data publisher) and adds a load-timing model; C adds a third row spelling beside record and data. AI-error: A is a strict shape, refused off app pages, limit/id/recordId refused with prescriptions, with a declared planned window until objectui#12931; B gives two writers two meanings under one key; C is a new root that lint, formula and the editor must all learn. Startup: A is one key on an existing shape, with no root, lint or gate change.",
          "recommendation": "A. It dominates on all four axes. No new root, ADR-0089 D1-consistent, strict and loud, one key. The contract review should judge the key name `record` (it equals the root it binds) against the alternative spellings that are aliased to it."
        },
        {
          "question": "Q2: how is the tier read without a cloud-only root?",
          "options": [
            "A: a field on the bound row, record.plan (implemented as docs; no spec change needed)",
            "B: promote features to contract-bound and map the plan to feature flags"
          ],
          "axes": "Business: A uses data the control plane already owns on its org row; B needs a per-org flag in a deployment-level anonymous registry. Long-term: A keeps platform mechanism and app data separate; B breaks PUBLIC_AUTH_FEATURES' meaning and its drift guard against getPublicConfig(). AI-error: A gives one place for row data; B puts two meanings under one root. Startup: A adds nothing; B is a cloud-only fill, which the card forbids.",
          "recommendation": "A. features stays ambient. Promoting it is not decided here and was not needed."
        },
        {
          "question": "Q3: where does the count come from?",
          "options": [
            "A: a summary roll-up field on the bound row, read as record.FIELD (implemented as docs)",
            "B: a dedicated count root or aggregate expression"
          ],
          "axes": "Business: A covers 'a second environment exists' with record.environment_count > 1; B is speculative. Long-term: A uses the existing summary field type; B is a new evaluator capability. AI-error: A means one root and one field; B means a new surface to get wrong. Startup: A adds nothing.",
          "recommendation": "A. The cloud side needs a summary field on its organisation object (master-detail from environments). That is the app's data, not the platform's."
        }
      ],
      "out_of_scope_findings": [
        "class: none filed. The content/docs/ui/pages.mdx visibleWhen row still calls `visibility` an accepted deprecated alias, but at 18 it is a retiredKey tombstone. Pre-existing doc drift. carrier: none. Noted, not filed (in Acceptance notes). dedupe: pages.mdx visibility alias, retired visibility docs."
      ]
    }

    Generated by Claude Code

  3. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 24265,
      "status": "done",
      "round": "patch round 1",
      "branch": "claude/issue-24265-app-page-predicate-roots",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/24275",
      "session": "session_012SGQNHx8abCKZKoyj2VQym",
      "head": "03b3e5ad4b",
      "premise_still_valid": true,
      "failing_assertion": "Reproduced with `pnpm --filter @objectstack/metadata-protocol test` on head a150ec532f (exit 1; 1 failed / 20640 passed). Failing test: src/protocol.meta-types-degenerate-derivation.test.ts > '#17501 — /meta/types serves a real schema for `action`, and moves nothing else' > 'control: `page` still serves 25 top-level properties'. Assertion: `AssertionError: expected 26 to be 25 // Object.is equality`, at src/protocol.meta-types-degenerate-derivation.test.ts:339:15.",
      "root_cause": "This PR. CARD_PROPERTY_COUNTS is a census: a positive control that pins how many top-level properties /meta/types serves per type. PageSchema gained one declared key (`record`), so the served page schema has 26 properties. The derivation itself did not change. The file's own docblock records each such move as a declared key, not a derivation change; `page` moved 24 to 25 the same way for `print`. So this is the census case: the count must name the new key. The fix is page 25 to 26, with a docblock line naming `record`. The test is not loosened: every other count and the derivation assertions are unchanged.",
      "summary": "Census bumped with its recorded reason. Review flag F1 folded in: the pages guide's visibleWhen cell no longer calls the `visibility` alias accepted; it now says the retired spelling is refused at parse (ADR-0089 D2). Review flag F2 folded in: the liveness `record` row's planned note names its consumer card objectstack-ai/objectui#12931. No decision the review judged was touched (key name, placement rule). The PR body and title are unedited.",
      "files_changed": [
        "packages/metadata-protocol/src/protocol.meta-types-degenerate-derivation.test.ts",
        "content/docs/ui/pages.mdx",
        "packages/spec/liveness/page.json"
      ],
      "tests": "Head 03b3e5ad4b: `pnpm --filter @objectstack/metadata-protocol test` exit 0 (244 files passed, 3 skipped; 20641 passed, 19 skipped). `pnpm --filter @objectstack/spec test` exit 0 (693 files; 21015 passed, 1 todo). `pnpm --filter @objectstack/spec test:repo` exit 0 (68 files; 1125 passed).",
      "gates": "Head 03b3e5ad4b, exit codes captured before any pipe: `pnpm --filter @objectstack/spec build` 0. `pnpm --filter @objectstack/spec check:generated` 0 ('All 14 generated artifacts are up to date'). `pnpm --filter @objectstack/spec check:liveness` 0. Doc gates on the edited page: check:doc-authoring 0, check:docs-spec-enumerations 0, check:doc-anchors 0, docs-audit/check-affected-docs 0, check-doc-frontmatter 0. The full derived set was not re-run this round: the PM's list was run as asked, and CI covers the rest. CI check-runs on the new head: not awaited.",
      "mcp_calls": "0 MCP GitHub calls this round before the report read-back; the read-back is 1 issue_read get_comments (read). No MCP write tool used.",
      "api_writes": "2: git push of claude/issue-24265-app-page-predicate-roots (fast-forward a150ec532f to 03b3e5ad4b); fleet-write relay comment (POST /repos/objectstack-ai/objectstack/issues/24265/comments, this report).",
      "deviations": [],
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  4. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 24265,
      "status": "done",
      "round": "patch round 2",
      "branch": "claude/issue-24265-app-page-predicate-roots",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/24275",
      "session": "session_012SGQNHx8abCKZKoyj2VQym",
      "head": "88dbe4191e",
      "premise_still_valid": true,
      "summary": "Merged origin/main 9822b053e2, which includes PR #24270 as 586705d85d, into the branch with scripts/pm/os-regen-merge.sh. It is a merge commit, 6480ad0b61, followed by a separate regeneration commit, 88dbe4191e. There was no rebase and no force: the push fast-forwarded 03b3e5ad4b to 88dbe4191e. No decision the contract review judged was touched: the key name, the placement rule and the planned grading are all unchanged.",
      "conflicts": [
        "packages/spec/dropped-refinements.baseline.json — the only textual conflict, in the `measured` header totals: HEAD 237/768, main 238/770, base 236/767. Resolved by hand to the sum of both deltas, 239 schemas and 771 sites. Both sides' entries text-merged cleanly: #24270's three emptyState.link.url sites and its counts, plus this PR's ui/PageRecordBinding and the moved ui/Page record.filter.element site. The spec build's ledger check passed on the merged file, and dropped-refinements.test.ts, which pins the totals against the body, is green.",
        "packages/spec/authorable-surface/ui.json — generated and routed to the os-regen driver, so it was not text-merged. Step 2 took main's side, both sides having moved it. It was then regenerated by `pnpm --filter @objectstack/spec build` from the merged tree and committed as 88dbe4191e, which discharged the pre-commit deferral. It carries both sides: PageRecordBinding 4 hits, emptyState 7.",
        "packages/spec/liveness/page.json — auto-merged with no conflict. The branch's delta against origin/main is exactly this PR's `record` row, including the objectui#12931 note. #24270's PLANNED RECORD sentences in `_note` are kept, verified by grep.",
        "packages/spec/liveness/state-counts/page.md — only the branch changed it. The merge script kept the branch's bytes, and check:liveness re-derives it as current.",
        "content/docs/references/ui/component.mdx — only main changed it, so it was taken as is. check:generated reports all 14 artifacts current; component.mdx carries the emptyState text."
      ],
      "census": "Measured: protocol.meta-types-degenerate-derivation.test.ts still pins `page: 26`, and it passes on the merged tree. #24270 added no PageSchema key.",
      "files_changed": [
        "merge commit 6480ad0b61: main's files, plus a hand resolution of packages/spec/dropped-refinements.baseline.json (measured totals)",
        "88dbe4191e: packages/spec/authorable-surface/ui.json (regenerated)"
      ],
      "tests": "Head 88dbe4191e. `pnpm --filter @objectstack/spec test`: exit 0, 694 files, 21040 passed, 1 todo. `test:repo`: exit 0, 68 files, 1125 passed. `pnpm --filter @objectstack/metadata-protocol test`, after building its closure: exit 0, 244 files passed, 3 skipped; 20641 passed, 19 skipped. scripts/dropped-refinements.test.ts: exit 0, 27 passed.",
      "gates": "Exit codes captured before any pipe. `pnpm --filter @objectstack/spec build`: 0. `check:generated`: 0 ('All 14 generated artifacts are up to date'). `check:liveness`: 0. One earlier spec `test` attempt exited 99: it waited on my own test:repo run holding the lock and measured nothing; it is re-run above. CI on 88dbe4191e: not awaited.",
      "mcp_calls": "1 MCP GitHub call this round: issue_read get_comments for the report read-back. No MCP write tool used.",
      "api_writes": "2: git push (fast-forward 03b3e5ad4b..88dbe4191e); fleet-write relay comment (POST /repos/objectstack-ai/objectstack/issues/24265/comments, this report).",
      "deviations": [],
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #24275 comment 6248293061 · contract review PASS 6248097604 (after PASS 6247358877, a red census and two patch rounds) · ready and auto-merge armed 2026-10-18T09:34Z · domain:spec seat 1 · session session_01TyRhWbVbbQpQJTDbW8VahJ


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · PR #24275 → ef65646843 (Fixes #24265, an app page's visibleWhen reads its bound row through the existing record root) · domain:spec seat 1 · os-project-manager · session session_01TyRhWbVbbQpQJTDbW8VahJ · 2026-10-18T10:09Z

    probe reading
    PR object merged 2026-10-18T10:08Z out of the merge queue (entered 2026-10-18T09:35Z); merge commit ef65646843 on parent 8bfb17d472
    origin/main git merge-base --is-ancestor ef65646843 origin/main → exit 0 (control: the PR head 88dbe4191e → exit 1, a squash merge)
    merged vs reviewed head git patch-id --stable is equal with context: the merged diff over 8bfb17d472 and the reviewed head 88dbe4191e over its merge base 9822b053e2 both give dbecc617 (ACCEPT 6248293061, contract review PASS 6248097604)
    queue branch no gh-readonly-queue/main/pr-24275-* ref remains
    mis-close scan Fixes #24265 is the squash message's only closing keyword. Its other references are PR #24270 (merged before this one) and objectstack-ai/objectui#12931, which the message also spells once as a bare objectui#12931 (prose, no closing keyword; objectstack#12931 closed on 2026-09-03 and is untouched). This card closed completed by that PR alone

    pm:dispatched is cleared in this act. The cloud dev session was archived at ACCEPT.

    Hand-offs:


    Generated by Claude Code

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

Metadata

Metadata

Labels

area:recordsBusiness objects, records, the views that show data, usable forms, searchdomain:specenhancementNew feature or requestpriority:p1High: required for production / M2target:v18

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions