Skip to content

fix(lint)!: a field group's visibleWhen is judged like a field's own, at build and at the object save door - #26928

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-6zlhbv
Oct 27, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-6zlhbv

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Yibo Wang · project thread

Fixes #26646

Clause-②: no (narrowing)
Metadata-protocol: narrowed
Skills-docs: none — skills/** never teaches fieldGroups[].visibleWhen (the two skills that mention fieldGroups teach key, label and membership only); the docs page content/docs/ui/field-grouping-and-order.mdx gains the slot and its check here

维护者速读

  • 改了什么:对象的字段分组 fieldGroups[].visibleWhen 现在和字段自己的 visibleWhen 用同一套表达式检查,os validate / os build 和对象保存口都会拦,错误规则 expression-invalid,定位到分组的 key。
  • 为什么改:分组条件写错一个字段名(如 record.no_such_field == 'x')以前照样 200 保存,渲染器按失败即隐藏处理,整个分组(连标题)在所有表单和详情页消失,没有任何提示;同样的写法放在字段或动作上早就被 422 拒绝。
  • 风险与回滚:收窄(BREAKING minor):以前能存的错误分组条件现在被拒。仓库内 8 个字段分组(示例、模板、脚手架)都没有写 visibleWhen,零命中。回滚即撤销本 PR。
  • 先看:packages/lint/src/validate-expressions.ts(对象遍历末尾新增的分组循环,以及 fieldRuleRootIssue 里为分组改写的措辞);packages/lint/src/runtime-gate.object-field-group-visibility-writes.test.ts;packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts 最后一个 describe 块。
  • 两分钟验证:pnpm --filter @objectstack/lint exec vitest run src/runtime-gate.object-field-group-visibility-writes.test.ts,预期 14 个用例全绿,其中卡片原样探针被拒、定位 object 'qa_fg_probe' · field group 'g1' visibleWhen。
  • 需要你做的:无,席位落地。

What changes

  • @objectstack/lint — the field-group pass. runStackExpressionPasses gains one loop at the end of the object walk, after the field walk: every fieldGroups[].visibleWhen is judged with the field visibleWhen slot's rules — the record-scoped check (it parses, reads record.FIELD and never a bare field, names a field the object declares, calls only registered functions) and the root verdict over record / previous / parent. Same rule id (expression-invalid), located at object 'NAME' · field group 'KEY' visibleWhen. Not fenced on an object write, so the object save door (PUT /api/v1/meta/object/:name, draft promotion, package draft publish) gives the build's finding, and os validate / os build give it too.
  • The root verdict is worded for the group. fieldRuleRootIssue takes a field group visibleWhen slot: the message opens with "visibleWhen reads current_user, but a field group's section predicate binds only record…", and its consequence cell names the group's fail-closed gate (the renderer's sectionGateVisible resolves a fault to hidden, where a field's own visibleWhen falls back to visible). The field slots' wording is unchanged.
  • Registry note in authoring-rules.ts beside the finding(lint): the object save door gives no build verdict on validation conditions, field-rule slots (requiredWhen etc.), option visibleWhen or action predicates; os build refuses them, a metadata save stores them (#22019's sibling) #22032 pass history, with the corpus measurement.
  • Docs: one paragraph in content/docs/ui/field-grouping-and-order.mdx on the group's visibleWhen, its fail-closed gate and the check.
  • Changeset: .changeset/26646-field-group-visible-when-judged.md, @objectstack/lint minor, both declaration lines, ADR-0087 not-required (no-migration-prescription).

Why

The form renderer gates the whole group on this predicate and fails it closed, so a misspelt field hid the section on every form and record page with nothing said at authoring time, while the same text on fields.*.visibleWhen or actions[].visible was refused 422 at the same door. No pass of the expression rule read the slot (validate-expressions.ts had no fieldGroups site).

Tests

  • New packages/lint/src/runtime-gate.object-field-group-visibility-writes.test.ts (14 cases): the card's probe body and four more refused bodies (bare reference, unregistered function, syntax error, current_user root) each refused once at the group's key; three valid predicates and an absent one publish clean at the door and the build; door findings equal build findings; the card body earns the same message on the group as on the field's own visibleWhen; a stored sibling's broken group does not block this write.
  • New block in packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts (4 cases) through the real saveMetaItem / publishMetaItem: the card's probe is refused 422 INVALID_METADATA on an active save and on a draft's promotion with the finding at the group's key and nothing landing; record.stage == 'won' saves; door issues equal build findings.
  • validate-expressions.test.ts meta-test: obj reads fieldGroups, and the new fieldGroup receiver (key, visibleWhen) is checked against ObjectFieldGroupSchema.
  • runtime-gate.object-formula-writes.test.ts: the "every object-borne site" pin carries a faulting group visibleWhen, at the build's position (after the option site, before the actions).

Acceptance notes

  • Re-measure on main 031d97a0 (fresh showcase boot, seed admin): the card's step 1 answered 200 state=active and read the predicate back verbatim; step 2 (actions[].visible) and step 3 (fields.stage.visibleWhen) answered 422 expression-invalid. Premise holds.
  • Corpus: the repository ships 8 field groups on 4 objects (app-showcase contact 4 and semantic_zoo 2, the create-objectstack blank template 1, the os init scaffold 1; platform objects declare none) and none carries a visibleWhen. Nothing to fix.
  • current_user on a group is refused, as on a field's own visibleWhen. In a form whose host binds the user it used to hide the group by role in the renderer only; elsewhere it faulted and hid the group for everyone. The prescription points at field-level security for role-based hiding.

Verification

All at head 75bff196c (main merged) unless noted.

Live replay on a showcase boot (head 6df7deb, lint dist rebuilt)
  • Card body: 422 INVALID_METADATA, one issue, rule expression-invalid, path object 'qa_fg_probe' · field group 'g1' visibleWhen, message "unknown field no_such_field on qa_fg_probe". GET /meta/object/qa_fg_probe afterwards: 404.
  • Control record.stage == 'won' on the same group: 200 state=active.
  • Steps 2 and 3 unchanged: 422 at action 'qa_go' visible and at field 'stage' visibleWhen.
  • os validate (app-showcase) with visibleWhen: "record.no_such_field == 'x'" planted on the work group: exit 1, object 'showcase_contact' · field group 'work' visibleWhen: unknown field; restored, exit 0 with 0 field-group findings. app-crm and app-todo validate: exit 0.
Suites
  • pnpm --filter @objectstack/lint test: 170 files, 7485 tests passed. typecheck: exit 0.
  • @objectstack/metadata-protocol protocol.runtime-authoring-gate.test.ts: 218/218. typecheck: exit 0.
  • Full dogfood (pnpm --filter @objectstack/dogfood test, closure built): 334 files passed, 3 skipped; 2716 tests passed, 20 skipped; lock VERDICT command-exit 0.
Ablation (protocol block reads lint through dist)

Mutated the loop to read obj.ABLATED_fieldGroups_26646 with scripts/ablation-replace.mjs --hold, rebuilt lint, ablation-dist-preflight found the marker in 4 built files: the #26646 protocol block went 3 failed / 1 passed (the two refusals and the parity case red, the valid control green). Restored (blob == HEAD, git diff HEAD empty), rebuilt, preflight --absent clean: 4/4 green.

Derived gates

node scripts/pm/dispatch-gates.mjs --commands derived 88 families; all 88 ran with exit 0. --ran reconciliation: 88 derived, 88 run, 0 NOT-MEASURED, 0 UNRUN. CI runs the rest of the farm.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (No spec change.)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes. (CI owns the repo-wide lint run.)
  • 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: n/a.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WGufzj32KwHUJobhFR5iHF

…ject door

A field group's section predicate now takes the field visibleWhen slot's
verdict (expression-invalid), located at the group's key, in os validate /
build and at the object save door.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzj32KwHUJobhFR5iHF
…ugh the real save door

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzj32KwHUJobhFR5iHF
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Oct 27, 2026
@github-actions

github-actions Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/lint, touching 5 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/validating-metadata.mdx (via AUTHORING_RULES (symbol, a top-level const object))
What this run could not see
  • the SDK route bridge reached 55 of 207 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 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; 97 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 4049efa34584a18485ccd13efcf4351f509205eb → packageMentionDocs.

Which tree this was computed on

This run read content/docs from a8e485cacd722ffc9b31133cca6bd08d5b1bd4b5 — the merge of head 365a6e4830fd27520c07daee7f1de542a2e77121 into base 4049efa34584a18485ccd13efcf4351f509205eb, 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 a8e485cacd722ffc9b31133cca6bd08d5b1bd4b5 && git checkout a8e485cacd722ffc9b31133cca6bd08d5b1bd4b5
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 4049efa34584a18485ccd13efcf4351f509205eb 365a6e4830fd27520c07daee7f1de542a2e77121 && git checkout -B drift-repro 4049efa34584a18485ccd13efcf4351f509205eb && git merge --no-ff 365a6e4830fd27520c07daee7f1de542a2e77121

node scripts/docs-audit/affected-docs.mjs --json 4049efa34584a18485ccd13efcf4351f509205eb

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 4049efa34584a18485ccd13efcf4351f509205eb → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 75bff196c79ecae58c0cf8ebc5eb1f53ea97ac61
Local-runs: none
Scope: packages/lint/src/validate-expressions.ts, packages/lint/src/authoring-rules.ts, packages/lint/src/validate-expressions.test.ts, packages/lint/src/runtime-gate.object-field-group-visibility-writes.test.ts, packages/lint/src/runtime-gate.object-formula-writes.test.ts, packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts + packages/spec/src/data/object.zod.ts (ObjectFieldGroupSchema.visibleWhen), packages/spec/src/data/field-group-layout.ts, ui/packages/plugin-form/src/fieldGroups.ts (visibleWhenFault: 'hidden'), ui/packages/components/src/renderers/form/form.tsx (isFailClosedSectionGate / sectionGateVisible), ui/packages/plugin-detail/src/index.tsx (the group section reference), packages/lint/src/object-graph.ts (recordsOf), packages/qa/dogfood/test/expression-conformance.ledger.ts (cel-field-group-section), docs/adr/0085-object-semantic-roles-over-surface-hint-blocks.md (addendum of 2026-08-28), scripts/check-adr-0087-registration.mjs (no-migration-prescription). The two prose faces (content/docs/ui/field-grouping-and-order.mdx, .changeset/26646-field-group-visible-when-judged.md) are read for ② and the docs claim, not reviewed.

Card #26646 (body, triage 6441442924, retriage 6445801471 / 6446746341, claim 6453045956, dev report 6454514811), PR #26928 (body, 8 files, net diff vs main at merge-base 1708115e) and the head's check-runs were the inputs. The PR body's account was not taken on trust; every claim below is read off the tree.

① Derived judgments

  • Accept-set change (the narrowing), right. runStackExpressionPasses gains one loop after the field walk of the object walk: for every fieldGroups[] entry it computes fieldRuleRootVerdict('field group visibleWhen', raw) and then check(where, raw, objectName, 'record', verdict !== null), pushing the root verdict as expression-invalid at error. That is byte-for-byte the call pair the field triad loop makes for visibleWhen (fieldRuleRootVerdict(key, raw) + check(where, raw, objectName, 'record', verdict !== null)), so the group predicate is judged by exactly the field visibleWhen rules: it parses, reads record.FIELD and never a bare field, names a field the object declares (the object's columns alone, no attachedOnRead block, since the group is not a SERVED_ROW_SITES entry), calls only registered functions, and reads no root outside FIELD_RULE_BOUND_ROOTS (record, previous, parent). None of the write-path gates (the parent gate, the requiredWhen null guard, the traversal refusal) is applied, which matches the field visibleWhen slot (they are applied to readonlyWhen / requiredWhen only). Not fenced on runtimeWriteType === 'object', so the object save door, draft promotion and the build give one finding. Right.
  • Scope of fields a group predicate may reference: right. The predicate is checked against the whole object's field index, not the group's members. That is what the renderer evaluates: sectionGateVisible calls evalFieldPredicate(f.visibleWhen, ruleRecord, false, previousRecord, predicateScope) with the same ruleRecord (every declared field seeded) and the same host scope the field's own visibleWhen gets, so a group may read any declared field, and previous. Same binding, same rules.
  • Location object 'NAME' · field group 'KEY' visibleWhen: right. It follows the KIND 'NAME' SLOT shape of its siblings in the same walk (field 'X' visibleWhen, field 'X' option 'v' visibleWhen, action 'X' visible, validation 'X'), keyed by the group's key, which is the name a field's group and a layout section's group point at. The order pin in runtime-gate.object-formula-writes.test.ts fixes its position after the option site and before the actions, at build and door alike.
  • Public surface. No export is added or changed; FIELD_GROUP_VISIBLE_WHEN_SLOT is module-private, fieldRuleRootIssue's signature is unchanged and its message for the four existing slots is byte-identical (the new branch is keyed on the slot name). FIELD_RULE_SLOT_CONSEQUENCE gains one cell. Right.
  • Corpus: measured as the dev says. fieldGroups is declared on exactly four objects outside tests and the spec/lint/ui packages: examples/app-showcase/.../contact.object.ts (4 groups), semantic-zoo.object.ts (2, one carrying icon/description/collapse), packages/create-objectstack/.../note.object.ts (1), packages/cli/src/commands/init.ts scaffold (1). None carries a visibleWhen; packages/platform-objects/src declares no fieldGroups at all (its hits are generated translation bundles). 8 groups on 4 objects, 0 findings. Right.
  • validate-expressions / validate-security-posture 也有同形的 spec 不声明键的 ?? 别名读法(#5009 建议 3 的核对结果) #5017 receiver scan kept whole. obj.fieldGroups, fieldGroup.key, fieldGroup.visibleWhen are literal member reads; validate-expressions.test.ts adds fieldGroups to the obj receiver's expected set and a fieldGroup receiver pinned to ObjectFieldGroupSchema through shapeKeysOf (the schema is a lazySchema). Right.
  • Dogfood conformance ledger. cel-field-group-section classifies the slot by its evaluator and fail policy (fail-closed, renderer), as cel-field-rule does for the field slots without naming lint; nothing there becomes stale.

② Semver level

  • Changeset .changeset/26646-field-group-visible-when-judged.md is in the root .changeset/, '@objectstack/lint': minor. The diff moves one published package (@objectstack/lint; metadata-protocol gains a test only), and the kind is Metadata-protocol: narrowed (a value an author could write is now refused), which takes at least minor under the Post-Task rule. Right.
  • Clause-②: no (narrowing) on the PR body and in the changeset, Metadata-protocol: narrowed on both; the (narrowing) arm is BREAKING, and the body carries the **BREAKING** banner with Before / Now / Unchanged. The triage's yes (narrowing) spelling is the malformed arm; the claim corrected it to no (narrowing). Right.
  • ADR-0087 marker: exactly one, not-required (no-migration-prescription). The gate refuses that category only when the body carries a FROM → TO prescription; the body carries none (no FROM token, no arrow, a **The remedy.** label, which findMigrationPrescription's label branch does not match) and no key is added, removed or renamed, so the category is honest. Check Changeset, the job that runs check-changeset-no-major, check-empty-changeset and check-adr-0087-registration --base MERGE_BASE, concluded success on this head. Right.
  • Title fix(lint)!: carries the bang. Right.

③ Boundary flags

  • Dev flags: none raised; open_questions: []; out_of_scope_findings says only that no other group slot carries an expression (ObjectFieldGroupSchema confirms: key, label, icon, description, visibleWhen, collapse and three deprecated booleans). Answered.
  • Premise: re-measured by the dev on 031d97a0 (200 / 422 / 422), consistent with the mechanism I read on main (no fieldGroups read anywhere in validate-expressions.ts before this diff). Answered.
  • Skills-docs: skills/** never teaches fieldGroups[].visibleWhen; the docs page named is the one that gains the paragraph. Answered.
  • Serial constraint spec(lint): the view publish door accepts a chart list view whose dataset does not exist or whose values names no measure of its dataset #26847 (validate-chart-bindings.ts): disjoint from this diff's hunks; the head has main merged. Answered.
  • Lane: packages/lint is domain:spec by the retriage answer; the PR touches packages/lint and a metadata-protocol test file only. Answered.
  • current_user on a group is refused as on a field's visibleWhen, not as on an option's. That is the card's "same rules as field visibleWhen", and it is right for the slot: the renderer binds the group gate with the same predicateScope as the field's own rule (nothing server-side ever evaluates a group predicate), so the acceptance that per-option visibleWhen earns from the server's option check does not transfer. No escalation.

④ Design judgment

Read against the published spec on main: ObjectFieldGroupSchema.visibleWhen is declared as a CEL predicate "evaluated by the form renderer's section-gating contract against the record in scope: FALSE (or a faulting predicate, fail-closed) hides the WHOLE group, header included" (ADR-0085 addendum of 2026-08-28; deriveFieldGroupLayout passes it through verbatim; plugin-form stamps visibleWhenFault: 'hidden'; form.tsx takes a stamped divider off the fail-open triad and resolves it through sectionGateVisible with fallback false; the detail renderer's group section reference evaluates the same predicate and renders nothing when hidden).

  • Does the narrowing take a declared capability away? No. The spec declares a CEL predicate over the record in scope; a predicate that cannot evaluate was never a capability, it was the spec's own refusal rendered as an invisible section. Every body the pass now refuses (unknown field, bare reference, unregistered function, syntax error, a root the renderer never binds) faulted at the renderer and hid the group for everyone on every form and record page. The one arguable case, current_user under a host that does bind it, is exactly the case the field visibleWhen slot already refuses for the same reason (display-only, no server evaluator, faults everywhere else), and the card asked for parity with that slot. The narrowing makes the spec's refusal real at authoring time, in the build and at the door, which is the ADR-0049 posture the schema's own history cites.
  • Fail-closed semantics in the message. The group's consequence cell and the root-verdict opening are worded for the group ("binds only record …", "fails CLOSED: the whole group, header included, is hidden for everyone") and the field slots' wording is untouched; the pin asserts both. Right, and the direction matches sectionGateVisible's fallback of false.
  • Try to break it. (i) A group written with the name / id alias of key would locate at field group '?'; the strict schema refuses that body by name at the same door, so the location can only degrade on a body that is refused anyway. (ii) recordsOf reads the array shape the schema declares and the record shape its sibling rules tolerate, so a raw body at the door and the parsed body at the build walk the same entries. (iii) A stored sibling's broken group is not charged to this write (the door's differential, pinned). (iv) Draft save still lands and promotion is refused with the finding at the group's key (pinned through the real saveMetaItem / publishMetaItem); that is the existing draft-door contract for every other pass. (v) Order: build and door emit the group site after the option site and before the actions, pinned in the every-site body. Nothing broke.
  • Docs claim vs code. The paragraph says the gate is fail-closed, whole section, header included; that os validate, os build and the save door judge it with the field visibleWhen rules; and that an unknown field is refused expression-invalid at the group's key. All three match the tree. One imprecision, should-fix 3 below.

Findings, none must-fix:

  • should-fix 1 — the user-root prescription is the field's, not the group's. For current_user / user / ctx / os on a group, fieldRuleRootIssue appends the shared prescription, which opens with "To gate the CHOICES of a select by user, move the predicate to the option's own visibleWhen …" and continues "To hide the FIELD by role, declare field-level security …". A field group has no options and is not a field; the fitting prescription is the FLS clause (per field of the group) and the rewrite against record, which is what the changeset's **The remedy.** already says. Branch the prescription on FIELD_GROUP_VISIBLE_WHEN_SLOT as the opening sentence already is.
  • should-fix 2 — the receiver table in validate-expressions.ts's header docblock is one row short. It lists every read with the schema that declares it ("all verified against the live .shape in validate-expressions.test.ts"); the test now pins objects[].fieldGroups (ObjectSchema) and fieldGroups[].key / .visibleWhen (ObjectFieldGroupSchema), and the table does not.
  • should-fix 3 — docs: "a CEL predicate over record". The check (and the renderer) also bind previous (and parent on a master-detail line item); the changeset's own Unchanged bullet accepts previous.stage != record.stage. Say so, or the page under-states what the gate accepts.

Check-runs on 75bff196c as read at review time: 25 completed success (among them Check Changeset, Governed Surface Queue Guard, PR-body guards, Lint · PM dispatch-gates, Lint · ESLint, Lint · docs & code guards, Lint · repo & tooling gates, Lint · CI, release & engine gates, Lint · scoped · slot lookup & corpus, Lint · scoped · query-options erasure, Build Docs, Type Check · source gates, Type Check · debt ledger, Spec property liveness, Dogfood Regression Gate, Test UI, Check Documentation Links, Check PR Size), 7 skipped (opt-in and matrix placeholders), 13 still in_progress: Build Core, Build & E2E UI, Test Core 1/8 – 8/8, Type Check UI, Type Check · consumer gates, Type Check · workspace. Their conclusions are the gate verdicts for those families and are not pre-empted here; the dev's local run reports @objectstack/lint 170 files / 7485 tests and protocol.runtime-authoring-gate.test.ts 218/218 at this head.

Implemented-by: claude/project-thread-6zlhbv
Reviewed-by: session_01WGufzfhobWEgUUrjz4rrbh

VERDICT: PASS

…e previous/parent in the docs

Address the contract review's three should-fixes: the group slot's
current_user/user/ctx/os prescription offers field-level security or a
record rewrite instead of the select-option move, the header receiver
table lists fieldGroups reads, and the docs paragraph names previous and
parent beside record.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzj32KwHUJobhFR5iHF
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · domain:spec seat 1 (#6017) · session session_01WGufzfhobWEgUUrjz4rrbh · 2026-10-27T22:31Z

Head 365a6e4830fd27520c07daee7f1de542a2e77121. Card #26646 (triage 6441442924, retriage answer 6446746341: lane domain:spec), claim 6453045956, dev report 6454514811.

  • Contract review: owed at CONTRACT_REVIEW_TIER on the narrowing face. The review (6454674770) at 75bff196 was a PASS with no must-fix. It found:
    • fieldGroups[].visibleWhen is judged with the same call pair as a field's visibleWhen (fieldRuleRootVerdict + the record-scoped check), not fenced on an object write. That is right for a group: the renderer's sectionGateVisible evaluates it in the same scope and fails closed, and nothing server-side evaluates it.
    • The location object 'NAME' · field group 'KEY' visibleWhen matches its siblings, and its order is pinned.
    • The corpus is 8 groups on 4 objects, none with visibleWhen, and platform-objects declares no groups, so nothing is refused.
    • The narrowing takes no declared capability away: every refused body faulted at the renderer and hid the group everywhere. It makes the spec's own fail-closed refusal real at authoring time.
  • Since then: 365a6e48 closes all three should-fixes.
    • The group's user-root prescription now names FLS on a permission set or a rewrite against record, instead of the field's select-option wording, and the door test pins it.
    • The receiver table in the docblock of validate-expressions.ts gains the fieldGroups rows.
    • The docs name previous, and parent on a master-detail line item, beside record.
  • Shape: Fixes #26646, 8 files, +397/−12. One root changeset, @objectstack/lint minor, with a BREAKING banner and ADR-0087 not-required (no-migration-prescription) under a **The remedy.** label. Clause-②: no (narrowing) and Metadata-protocol: narrowed. check-governed-merges --pr 26928: NOT governed, 409 changed lines.
  • Gates: check-runs on this head are 40 success, 7 skipped and 0 red. mergeable_state is clean.
  • Landing: Tier S. pr_ready + automerge_enable in this act.

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/m tests tooling

Projects

None yet

2 participants