Skip to content

fix(lint)!: refuse a page variable source naming no component on the page, and a page.NAME read the page does not declare - #28769

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/project-thread-1t94a5
Nov 1, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/project-thread-1t94a5

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt

Requested by Leehom · project thread

Fixes #28725

Clause-②: no (narrowing)
Metadata-protocol: narrowed
Skills-docs: none — no file under skills/** teaches a page variable's source binding or page.NAME reads; both refusals carry their own fix and os explain entries.

维护者速读

  • 改了什么:页面变量的 source 写了页面上不存在的组件 id(如 no_such_component),以及组件 visibleWhen 读了页面没声明的变量(如 page.nosuch == 'x'),现在都被拒:保存、草稿诊断、发布、os validate 四处同一条结论,定位到 variables[i].source 或该组件。
  • 为什么改:以前这两种都能保存、草稿显示 valid、照常发布。控制台按组件 id 精确匹配写入方,source 写错就没有任何组件会写这个变量;page 里只有声明过的变量,读没声明的名字永远取不到值,组件就无条件显示。作者毫不知情。
  • 风险与回滚:低。只收紧写入口,已存页面照常加载和服务,只有下次保存才被判。仓内语料 3 个 source、5 个 page.X 读取(都在 showcase 两个页面),零命中;examples 三个 os validate 全过。回滚即撤回 @objectstack/lint 这次改动。
  • 先看:packages/lint/src/validate-visibility-predicates.ts(firstUndeclaredPageRead 与 checkElement 的 3c 段);packages/lint/src/validate-page-variables.ts(source 判定);packages/lint/src/runtime-gate.page-variable-references.test.ts。
  • 两分钟验证:pnpm --filter @objectstack/lint exec vitest run src/runtime-gate.page-variable-references.test.ts,预期 17/17 通过。
  • 需要你做的:无,席位落地。

What changes

  • packages/lint/src/validate-page-variables.ts: a fourth id beside authoring gate: an app navigation visible with a CEL syntax error passes save, draft diagnostics and publish (a page visibleWhen with the same fault is refused), and page variables publish with duplicate, empty or wrong-typed declarations #28663's three, page-variable-source-unknown. A variable whose source names a component id the page does not hold is refused, located at variables[i].source. The page's component ids come from walkPageComponents, the one traversal every page rule shares (regions, slots, and the sub-trees inside properties), read once per page and only when some entry has a source; no second walk. A source-authored page (kind: 'react' / 'html') is not judged on source: its components live in the code, which that walk deliberately skips.
  • packages/lint/src/validate-visibility-predicates.ts: a new gating id, visibility-page-variable-undeclared. On a page component, a visibleWhen reading page.NAME or page.?NAME where the page declares no variable NAME is refused, located at the component, naming the first such read. Judged from the AST the has() check already holds (formula's parseCelToAst, no second parser), with the page's declared names passed into checkElement from the pages loop only. The nav walker is untouched (authoring gate: an app navigation visible over the CEL size budget passes save and draft diagnostics, where a page visibleWhen of the same size is refused #28726 owns it). Stand-downs are missed catches only: an index read (page['x']), and any predicate whose comprehension binds page as its loop variable (COMPREHENSION_MACROS, reused from predicate-rhs-position.ts).
  • Why that file and not validate-predicate-path-refs.ts (measured, as the order asked): the path rule resolves data.* against a metadata type's schema only; nothing in it reads a page root. The visibility rule is where page is declared as a root (VIEW_PAGE_EXTRA_ROOTS) and where the page walk already hands each component's predicate to the parser.
  • packages/lint/src/index.ts: exports both id constants. packages/lint/src/rule-explanations.ts: two os explain entries.
  • No authoring-rules.ts change: both ids live inside rules already registered on page writes, so save, draft _diagnostics, publish and os validate carry them with no registry or metadata-protocol source change, and the CLI rule count does not move (no content/docs/** edit).
  • .changeset/28725-page-variable-source-and-read.md: @objectstack/lint minor, BREAKING banner, both declaration lines, ADR-0087 not-required (no-migration-prescription).

Why

#28663 judged the variables list itself; the contract review of PR #28723 (finding S3) left these two references admitted, and this card measured both on a stock showcase: draft _diagnostics valid: true, publish succeeds. What the console does with each, read at ui/ on this head:

  • source. usePageVariableBinding(componentId) (ui/packages/react/src/hooks/usePageVariables.tsx) returns definitions.find((d) => d.source === componentId), and the two writer elements (element:record_picker, element:text_input) call it with their own schema.id. A source no component carries matches nothing, so no element writes the variable and it keeps its default. The refusal matches a binding that can never resolve.
  • page.NAME. buildNodeEvaluationScope (ui/packages/react/src/SchemaRenderer.tsx) sets page: pageVariables, written after the ambient spread, so nothing else can supply page. The variables come from PageVariablesProvider, mounted by the page renderer only when the page declares variables and seeded from the declarations alone. Its only writers are a bound element (by a declaration's source) and the dashboard filter bar (its own declared filters), so an undeclared name is never present. A page with no variables gets the frozen empty fallback. No route params or other bindings reach page. The read faults and the element renders, which is the refusal's stated consequence.
  • Nesting: a provider merges an outer scope (objectui#2578), documented for a filtered dashboard embedded in a page. That is a dashboard reading the page's variables, not a page component reading another scope's, so it does not reach this rule.

Migration note. Shipped corpus (examples/**, packages/*/src, platform apps): 3 variable source bindings and 5 page.NAME predicate reads, all on two showcase pages (page-variables.page.ts, contact-form.page.ts). 0 refused. os validate at head on examples/app-showcase, app-crm, app-todo: "valid": true, 0 findings of the new ids. Positive controls on the same command: planting source: 'project_pickr' or visibleWhen: "page.selectedProjectID == ''" in the showcase page makes os validate exit 1 with the new id at the right path. Stored pages keep loading and serving; only the next save of one with a finding is refused.

Tests

  • packages/lint/src/runtime-gate.page-variable-references.test.ts (new): the card's source: "no_such_component", a case-mismatched id, the card's page.nosuch == 'x', a { dialect, source } envelope, page.?nosuch, and a page.X read on a page with no variables, each refused at the page door with its path, with door / os validate (runAuthoringRules('validate')) parity. Controls admitted at both: the showcase picker shape, a variable with no source, a has()-guarded declared read, a read through a declared object variable, a comprehension binding page, no page read. Also: a writer nested in a container's children resolves; a kind: 'react' page is not judged on source; two undeclared reads are one finding; both faults are two findings.
  • packages/metadata-protocol/src/protocol.draft-publish-verdict.test.ts: two new rows in the Studio/metadata: a draft the editors, the draft _diagnostics and the Problems tab all call valid is refused only at publish — and one such draft rolls back the whole package release #26707 table (the draft saves, reads valid: false with the publish's words, publish refuses 422 INVALID_METADATA); the clean controls read valid: true and publish; paths are item-relative (variables.0.source, regions.0.components.0).
  • packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts: the active saveMetaItem refuses each (door message = build message, nothing stored); the clean page lands.
  • Fixture triage (declaration added, not respelled): validate-visibility-predicates.test.ts and runtime-gate.page-visibility-predicate-writes.test.ts each had a clean control reading page.selectedId / page.selectedProjectId on a page that declared no variables. Each page now declares the variable; the second file's family id set gains the new id, so its controls also assert it stays silent.
Red on base 8bceb6a, green on head

Base = the four edited lint src files written from 8bceb6a, lint rebuilt (ablation-dist-preflight --absent exit 0), then restored (git diff HEAD empty) and rebuilt.

base  lint   runtime-gate.page-variable-references.test.ts   Tests 9 failed | 8 passed (17)   (the 8 = the controls)
base  protocol draft-publish-verdict + runtime-authoring-gate Tests 5 failed | 255 passed (260)
head  lint   Tests 17 passed (17) · protocol Tests 260 passed (260)

Record: pm-orders/server/evidence/28725/redbase2.log (script redbase.sh).

Ablations (scripts/ablation-replace.mjs, lint rebuilt, ablation-dist-preflight both legs, restore = blob equals HEAD)
SRC  (source check behind globalThis.ABL_SRC)   lint 4 failed | 283 passed (3 files) · protocol 3 failed | 257 passed
READ (read check behind globalThis.ABL_READ)    lint 7 failed | 280 passed (3 files) · protocol 3 failed | 257 passed
each leg: anchor x1 -> x0, marker in dist (preflight exit 0), restore blob == HEAD, git diff HEAD empty; rebuild, preflight --absent exit 0 for both markers

Record: pm-orders/server/evidence/28725/ablate.log (scripts ablate.sh, ablate-run.sh).

Live probe: both card cases on a stock showcase boot, base vs head

Commands: pnpm --filter @objectstack/example-showcase dev -- --fresh -p PORT; POST /api/v1/auth/sign-in/email as the seeded admin; POST /api/v1/packages a scratch writable package; PUT /api/v1/meta/page/qa_src_page?mode=draft&package=PKG (a picker project_picker, variables[0].source: "no_such_component"); PUT /api/v1/meta/page/qa_read_page?mode=draft&package=PKG (visibleWhen: "page.nosuch == 'x'", variable declared and sourced from the picker); GET …?state=draft&package=PKG for each; POST /api/v1/packages/PKG/publish-drafts; an active PUT of a clean control and of the read case. Base = 8bceb6a lint sources over the four edited files, lint rebuilt.

base  case 1 draft _diagnostics {"valid":true} · case 2 {"valid":true} · publish-drafts "outcome":"published" (2) · active PUT case 2 200
head  case 1 _diagnostics valid:false [variables.0.source page-variable-source-unknown]
head  case 2 _diagnostics valid:false [regions.0.components.1 visibility-page-variable-undeclared]
head  publish-drafts "outcome":"refused" INVALID_METADATA pages[0].variables[0].source · active PUT case 2 422 INVALID_METADATA
both  clean control (source = the picker, reads page.selectedProjectId) active PUT 200

Record: pm-orders/server/evidence/28725/live-probe.txt (probe.sh, probe-base.sh).

Acceptance notes

Verification

Derived gate loop (node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, 59 families) at d350dda3 (the second merge of origin/main): 59 exit 0. dispatch-gates --ran: 59 derived famil(ies) accounted for — 59 run, 0 NOT-MEASURED (a DERIVED zero — all 59 recorded an exit code and none of them is 3). Record: pm-orders/server/28725-gates-ran.txt.

  • Three families first read exit 3 (prerequisite, nothing measured) and were re-run to exit 0 on the same head: check-plugin-teardown-shape --self-test (shallow clone; git fetch --depth=1 origin 621a4876), check:dual-build-cjs-loads (seven packages outside the built closure had no dist/; built), check:type-check-debt (lock queue-timeout behind this run's own dogfood battery; re-run: --re-measure: OK — 0 raw tsc error(s)). The same 59 also all read exit 0 at the first merge cef4f60f.
  • Package suites at d350dda3: @objectstack/lint Test Files 188 passed, Tests 7780 passed; @objectstack/metadata-protocol Test Files 294 passed / 4 skipped, Tests 22029 passed / 26 skipped; both typecheck exit 0. eslint packages/lint packages/metadata-protocol --no-inline-config exit 0. check-platform-checklist --changed-from origin/main OK.
  • Dogfood battery (the (narrowing) arm) at d350dda3, closure built, pnpm --filter @objectstack/dogfood test under the lock: VERDICT command-exit 0 · Test Files 374 passed / 3 skipped (377) · Tests 3034 passed / 28 skipped (3062).
  • os validate at d350dda3 on examples/app-showcase, app-crm, app-todo, app-multi-package: "valid": true, 0 findings of the new ids (pm-orders/server/evidence/28725/os-validate.txt).
  • CI owns the repo-wide lint and every family outside the derived set.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (No packages/spec edit.)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes (scoped: eslint packages/lint packages/metadata-protocol --no-inline-config exit 0).
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: not touched.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English (the maintainer brief excepted, by the dispatch contract).

Patch round: over-budget builder fold (seat-ordered deviation)

Ordered by server seat 5 after #28756 (PR for #28726) landed: checkElement's bounds arm and #28756's navOverBudgetFinding built the visibility-predicate-over-budget sentence in two places. Head f0519ad0 (after merging origin/main d08b859a) folds them into one overBudgetFinding(refusal, where, path, surface, root), the way syntaxFinding already works: the SyntaxSurface gains overBudgetHint(root), so the element surface keeps its ROOT.f in [...] / formula-field remedy (root from the site's layer) and the nav surface keeps its current_user.positions.exists(...) remedy. navOverBudgetFinding is gone.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FEkQ5mBAqi5RnPHhcrHgTn


Generated by Claude Code

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 1, 2026
@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

18 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ⚠️ 1 changed file(s) yielded no anchor (packages/lint/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/lint/src/index.ts) — pages documenting those are invisible to this run
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 4 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json d08b859a7177cf16d6190163f2e42efa2d980d3a → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 96c93b7787545281da05a2579faaf2f6ec1c2d57 — the merge of head f0519ad0f54cbb47b050e43c26f1860b8d00d6b3 into base d08b859a7177cf16d6190163f2e42efa2d980d3a, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 96c93b7787545281da05a2579faaf2f6ec1c2d57 && git checkout 96c93b7787545281da05a2579faaf2f6ec1c2d57
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d08b859a7177cf16d6190163f2e42efa2d980d3a f0519ad0f54cbb47b050e43c26f1860b8d00d6b3 && git checkout -B drift-repro d08b859a7177cf16d6190163f2e42efa2d980d3a && git merge --no-ff f0519ad0f54cbb47b050e43c26f1860b8d00d6b3

node scripts/docs-audit/affected-docs.mjs --json d08b859a7177cf16d6190163f2e42efa2d980d3a

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

…-1t94a5

# Conflicts:
#	packages/metadata-protocol/src/protocol.draft-publish-verdict.test.ts
…er-surface remedy

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l and removed size/m labels Nov 1, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: f0519ad0f54cbb47b050e43c26f1860b8d00d6b3
Local-runs: none
Scope: packages/lint/src/validate-page-variables.ts, packages/lint/src/validate-visibility-predicates.ts, packages/lint/src/index.ts, packages/lint/src/rule-explanations.ts, packages/lint/src/runtime-gate.page-variable-references.test.ts, packages/lint/src/runtime-gate.page-visibility-predicate-writes.test.ts, packages/lint/src/validate-visibility-predicates.test.ts, packages/metadata-protocol/src/protocol.draft-publish-verdict.test.ts, packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts + packages/spec/src/ui/page.zod.ts, packages/spec/src/ui/component.zod.ts, packages/spec/src/ui/action.zod.ts, packages/lint/src/page-walk.ts, packages/lint/src/predicate-rhs-position.ts, packages/lint/src/rule-explanations.test.ts, packages/lint/src/runtime-gate.ts, packages/metadata-protocol/src/protocol-authoring-gate.ts, ui/packages/react/src/hooks/usePageVariables.tsx, ui/packages/react/src/hooks/usePageVariableActionBridge.tsx, ui/packages/react/src/SchemaRenderer.tsx, ui/packages/components/src/renderers/layout/page.tsx, ui/packages/components/src/renderers/basic/record-picker.tsx, ui/packages/components/src/renderers/basic/text-input.tsx, ui/packages/plugin-dashboard/src/DashboardRenderer.tsx, examples/app-showcase/src/ui/pages/page-variables.page.ts, examples/app-showcase/src/ui/pages/contact-form.page.ts. Not reviewed (prose face): .changeset/28725-page-variable-source-and-read.md, read for ② only.

Check-runs on the head: 67 runs, 56 success, 11 skipped, 0 failed, 0 cancelled, 0 pending (read with --paginate; a Check Changeset re-run that was in progress at the first read finished green). Green include Check Changeset, PR-body guards, Governed Surface Queue Guard, Dogfood Regression Gate, Lint · PM dispatch-gates, Lint · ESLint and the other Lint jobs, Type Check (workspace, source, consumer, debt ledger, UI), Test Core 1-8, Test UI 1-16, Build Core, Build and E2E UI, Live E2E UI.

① Derived judgments

  1. Page writes: a variables[i].source naming no component id the page holds is refused with page-variable-source-unknown at variables[i].source. RIGHT. The spec declares source as "Component id that writes this variable" (page.zod.ts line 913) and retired the reverse targetVariable keys on the two writers because the binding "is resolved from the page variable whose source names this component's id" (component.zod.ts lines 3271-3337); the console resolves it exactly so, definitions.find on d.source === componentId called with schema.id by record-picker.tsx and text-input.tsx, the only two callers. A source no component carries binds nothing, so the variable keeps its default for the page's life: a declared binding that never resolves, refused where it was written. Component ids come from walkPageComponents, the one shared walk whose slot positions derive from the spec's own list (regions, slots, every properties sub-tree including page:tabs / page:accordion panels), read once per page and only when some entry has a source; a kind: 'react' or 'html' page yields nothing from that walk and is not judged, which is the right stand-down (its components live in the source). Case is compared exactly, as the console compares it.
  2. Page writes: a page component visibleWhen reading page.NAME or page.?NAME for a NAME the page's variables does not declare is refused with visibility-page-variable-undeclared at the component, naming the first such read. RIGHT. The spec binds page.VAR to "page state" declared in variables (page.zod.ts lines 770, 884, 1964); the console's buildNodeEvaluationScope writes page: pageVariables after the ambient spread (SchemaRenderer.tsx lines 699-712), so nothing else can supply page; the store is seeded from the declarations alone, mounted only when the page declares variables (page.tsx line 973), and its writers are the two bound elements (declared names only) and a dashboard's filter bar, which mounts its own nested provider for its own declared filters (DashboardRenderer.tsx line 1583) and whose writes reach the page scope only for a name the page defines. No other setVariable caller exists in ui/. So an undeclared member of page is never present; the read faults and the element renders, the fail-open the family's other gating ids refuse. Judged from the AST the has() check already holds (no second parser), on page components only; a comprehension that binds page as its loop variable stands the predicate down, and an index read is not judged, both missed catches rather than false errors. A has(page.nosuch) or page.?nosuch.orValue(...) guard on an undeclared name is refused too, rightly: the name can never exist on that page, so the guard is dead code the author did not mean.
  3. Public surface of @objectstack/lint: PAGE_VARIABLE_SOURCE_UNKNOWN and VISIBILITY_PAGE_VARIABLE_UNDECLARED exported; two os explain entries (covers under 60 characters without a period, no issue number in a paragraph). No registry change: both ids live in rules already wired on page writes (validatePageVariables, validateVisibilityPredicates), so rulesRun, the CLI rule count and the content/docs transcripts do not move, and the nav walker is untouched by the two new verdicts. RIGHT.
  4. One implementation at every surface: save door, draft _diagnostics and publish through judgeRuntimeAuthoringRules over the same registry, the CLI through runAuthoringRules('validate'); the protocol tests pin the draft read (variables.0.source, regions.0.components.0), the 422 and the active save with the build's message; the lint tests pin door and os validate parity. RIGHT.
  5. The fold: checkElement's bounds arm and navOverBudgetFinding are replaced by one overBudgetFinding(refusal, where, path, surface, root) with SyntaxSurface.overBudgetHint(root); navOverBudgetFinding is removed. Wording byte-identical on the tree, judged by reading both sides against origin/main: the message is the same template (overruns ${boundName(...)}, the summary echoed only when the bound is unnamed, then the surface's consequence, which is NEVER_EVALUATES for ELEMENT_SURFACE and the nav clause for NAV_SURFACE); the element hint string with ${root}.f in ['a', 'b', …] and the formula/rollup clause is character-for-character the old arm's with the same CANONICAL_ROOT_BY_LAYER[layer] root; the nav hint is the 130-character fix(lint)!: refuse an app nav visible over the CEL parse budget #28756 line unchanged. The existing pins (the page/view over-budget cases in validate-visibility-predicates.test.ts, the authoring gate: an app navigation visible over the CEL size budget passes save and draft diagnostics, where a page visibleWhen of the same size is refused #28726 nav block with its 200-character line pin) are untouched and green. RIGHT; the nav call passes an unused 'record' root, commented as such.
  6. Fixture triage: two clean controls that read page.selectedId / page.selectedProjectId on pages declaring no variables now declare them, and the page door family set gains the new id so the controls also assert its silence. RIGHT: a declaration added, not a predicate respelled.
  7. Corpus: the shipped pages carry 3 source bindings and 5 page.NAME predicate reads, all on the two showcase pages and all resolving (read in page-variables.page.ts and contact-form.page.ts); the source: keys on the other showcase pages are page data bindings, not variables. 0 refused. RIGHT.

② Semver level

.changeset/28725-page-variable-source-and-read.md: @objectstack/lint minor, BREAKING banner, Clause-②: no (narrowing), Metadata-protocol: narrowed, Skills-docs: none with its reason (the skills name page.VAR in root tables and teach no source binding), one ADR-0087 marker not-required (no-migration-prescription) with the no-conversion argument and the corpus count. Matches what the diff publishes: two references an author could write are now refused at every door (narrowed), on the one package the diff moves; metadata-protocol changes are tests only. The (narrowing) arm is the BREAKING one by AGENTS.md and the kind takes at least minor; the fold publishes nothing new. The PR body carries the same lines; Check Changeset and the PR-body guards are green. Clause-②: no (narrowing) is right; the claim comment's bare no is the seat's own line (③ item 1).

③ Boundary flags

open_questions: none declared.

Deviations, each answered:

  1. Claim line Clause-②: no versus PR body and changeset Clause-②: no (narrowing). Answered: the PR body and changeset are the records the gates read and they are right; the seat amends its claim comment; no PR change.
  2. Two metadata-protocol test files added (test only) beyond the claim's packages/lint/src surface. Answered: fix(lint)!: refuse an app nav visible that does not parse, and page variables that are duplicated, unnamed or wrong-typed #28723's and fix(lint)!: refuse an app nav visible over the CEL parse budget #28756's precedent and the only proof of the three doors; no source edit there.
  3. The seat-ordered fold reported as not done in the os-dev-report, then done in the PR body's patch round. Answered on the tree: head f0519ad0 carries the fold (① item 5); the report predates the commit.
  4. check:type-check-debt first read a lock queue-timeout; re-run exit 0. Answered: Type Check · debt ledger is green on the head.
  5. Dogfood battery not re-measured at f0519ad0 (lint-only byte-identical refactor plus a merge). Answered: Dogfood Regression Gate is green on the head.

Out-of-scope observations the dev reported, each a missed catch and escalated for the seat's filing judgment, not this PR:
6. page.NAME reads outside a component visibleWhen are not resolved: props-bag CEL slots (page:tabs item predicates, which bind a different evaluator anyway) and {{page.NAME}} interpolation tokens in action bodies (bodyExtra, action.zod.ts line 1799). Agreed as outside the card's two observed shapes.
7. A source naming a component that exists but cannot write (only element:record_picker and element:text_input bind) still binds nothing. Agreed: the card and ruling ask for existence; a writer-capability check would be a new rule over the component vocabulary.

④ Design judgment

The narrowing, read against the published spec on main and the consumer: the spec says source names the writer component's id and page.VAR is the page's declared state; the console binds both exactly that way and nowhere else. A source with no matching id and a page.NAME with no declaration are references the spec declares as resolvable only against the page itself and that never resolve there, so refusing them at the author makes the spec's own contract real and takes no working capability away. Attempts to break it: a writer nested in a container, a tabs panel or a slot is found by the shared walk (pinned for properties.children); a source-authored page is not judged on source; the console mounts no provider for a page without variables and no outer provider exists in the shell, so a variable-less page reading page.X really has nothing to read (there is no component that embeds one page inside another's scope; the dashboard's nested scope is the dashboard's own and a page component outside it cannot read its filters without declaring the name, which is exactly what the rule asks); has()-guarded declared reads, deeper reads through a declared object variable, variables with no source, and comprehension-bound page are all admitted; two undeclared reads are one finding; the differential at the door stays clean because pages is not a context collection. Hint lines are 172 and 117 characters. The fold is byte-identical by construction, with one builder per id as syntaxFinding already does.

Findings:

  • Must-fix: none.
  • Should-fix: none. The two observations in ③ items 6 and 7 are the seat's to file or decline; neither is a defect of this change.

Implemented-by: claude/project-thread-1t94a5
Reviewed-by: session_01FEkQ5oc56xChD6Wrc4cGSt

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt

席位核对

ACCEPT · domain:server seat 5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt · 2026-11-01T19:28Z · head f0519ad0 · #28725

The maintainer's standing authorisation for landing reviewed PRs, verbatim: 「按照 pm-dispatch 规则复核过的都授权」.

席位意见:与 diff 相符。页面变量的 source 写了页面上不存在的组件,或组件的显示条件读了页面没声明的变量,以前能保存发布但绑定永远不生效;现在保存、草稿诊断、发布和 os validate 都会拒绝并指出位置。现有示例全部通过。

  1. Shape: draft against main, Fixes #28725, @objectstack/lint minor changeset with the BREAKING banner, Clause-②: no (narrowing), Metadata-protocol: narrowed, Skills-docs: none with its reason, ADR-0087 marker. check-governed-merges --pr 28769: NOT governed, +534 / -59. git merge-tree against main: clean. Landing tier S.
  2. Against the order: ruling 1 → page-variable-source-unknown in validate-page-variables.ts, ids from the shared walkPageComponents; ruling 2 → visibility-page-variable-undeclared judged from the AST in the visibility rule, no second parser; ruling 3 → migration count 0 (3 source bindings, 5 page.NAME reads, all resolving). One implementation at save, draft _diagnostics, publish and os validate.
  3. Accepted deviations: two metadata-protocol test files (tests only, as fix(lint)!: refuse an app nav visible that does not parse, and page variables that are duplicated, unnamed or wrong-typed #28723 and fix(lint)!: refuse an app nav visible over the CEL parse budget #28756 did). The seat-ordered fold of the two visibility-predicate-over-budget builders into one overBudgetFinding (fix(lint)!: refuse an app nav visible over the CEL parse budget #28756 review S2), wording byte-identical, judged on the tree by the review. The PR body's fold paragraph was added by the seat through the relay. The claim's Clause-②: no is the claim template's line; the PR body and changeset carry the (narrowing) arm the gates read.
  4. Gates: ## Contract review at CONTRACT_REVIEW_TIER on this head: PASS, comment 6562765111, no must-fix, no should-fix. Check-runs on this head: 56 success, 11 skipped, 0 failed. Dev: 59/59 derived families, --ran derived zero.
  5. Out of scope: the dev's two observations are missed catches outside the card, so neither is filed and no measured author was hurt ⇒ Acceptance notes: page.NAME inside props-bag slots and {{page.NAME}} action tokens are not judged; a source naming a component that exists but writes no variable is not judged.
  6. Landing: pr_ready + automerge_enable through the relay.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 1, 2026 19:29
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 1, 2026 19:29
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 1, 2026
Merged via the queue into main with commit d9c4cdc Nov 1, 2026
68 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-1t94a5 branch November 1, 2026 19:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

1 participant