Repository navigation
plugin-approvals: sys_approval_request's 8 action visible predicates read record.viewer, a block the service attaches on read, and the shared expression validator refuses all 8 as an undeclared field #22211
Description
Activity
objectstack-fleet commented
on Oct 8, 2026 ContributorAuthorMore actionsTriage: first grade,
bug·priority:p3·domain:spec·area:workflow·pm:queue. Direction: an open declaration question, for the spec seat, inside triage's order on #22032Triage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-08T07:10Z. ⛔ Not a claim, ⛔ not a dispatch.Triage: lands in how a service-attached, per-caller block is declared so the shared validator can judge it (spec /
packages/lint), then inpackages/plugins/plugin-approvals/src/sys-approval-request.object.ts⇒domain:spec; rationale: the open question is a declaration shape, and every card that touches spec goes to the spec seat (lanes/services.md:11).- Why p3: nothing refuses these 8 predicates at a save today. Once 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's pass 4 lands, a publish save of this one packaged object under the
OS_METADATA_WRITABLE=objecthatch is refused. Pass 4's changeset already names that drift. - Inside the order on 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 (
6024268378): no new rule and no new refusal code, and widening the validator for one object is the default no.- So the fix declares what the service attaches, in a shape the validator already reads. It does not teach the validator an exception for
viewer. - If no existing shape can declare it, that is a spec addition. It is Clause-② yes, and the seat says so on this card before building.
- So the fix declares what the service attaches, in a shape the validator already reads. It does not teach the validator an exception for
- This is not a product fork for the maintainer: the per-caller semantics ([P3] approvals: server-computed viewer capability on getRequest (precise approver gating for declared actions) #3310) are settled; only their declaration is open.
- Why p3: nothing refuses these 8 predicates at a save today. Once 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's pass 4 lands, a publish save of this one packaged object under the
- addedarea:workflowApprovals and automation — the work that runs without a person driving itApprovals and automation — the work that runs without a person driving itbugSomething isn't workingSomething isn't workingand removed
on Oct 8, 2026 objectstack-fleet commented
on Oct 8, 2026 ContributorAuthorMore actionsClaim: PM loop round 2 · 2026-10-08T14:27Z
Session:session_01RPo7FUd6bSnAfkWMAKi848
Account:os-justin(the seat's linked user asGET /useranswers it; the card's assignee from this act)
Branch:claude/issue-22211-approval-viewer-declaration
Worktree:objectstack-issue-22211
Domain:domain:spec
Seat:domain:spec#3(seat post #18883)
File surface (atorigin/main799eb000c; stop on breach and explain in the report):- Step 1, measurement:
- the declaration shapes the shared validator (
validateStackExpressions) already reads for a field the object does not store: virtual, computed or read-only field kinds, and whateverpackages/lintresolvesrecord.<x>against; - how
ApprovalServiceattachesviewer(approval-service.ts).
- the declaration shapes the shared validator (
- Step 2, only if an existing shape declares a service-attached, per-caller block:
packages/plugins/plugin-approvals/src/sys-approval-request.object.ts, which declaresviewerin that shape. Then its tests, and.changeset/22211-*.md. - ⛔ Not
packages/lint/src/validate-expressions.ts(open PR fix(lint)!: os build and the object save door refuse a select option's visibleWhen that reads parent (#22157) #22268, finding(lint): a select option'svisibleWhenreadingparentpassesos buildand the object save door, and the server's option gate then faults open ("allowed through; the option's gate was NOT enforced") #22157, edits it), and no validator exception forviewer.
Container & model:S/M,mode:subagent,model: opus(--tier: no path-derived mandate for this surface; a measurement-first card)
Clause-②: no
Responsibility:this repo's plugin-approvals ships object metadata the shared validator refuses at error | none: the service attaches the block, but no declaration tells the validator | a publish save of this packaged object under OS_METADATA_WRITABLE=object, once #22032 pass 4 (landed, 73a0a6bf1) judges action predicates at the door
Thread-read: 6054592515
Serial constraints cleared: - Open PR fix(lint)!: os build and the object save door refuse a select option's visibleWhen that reads parent (#22157) #22268 (finding(lint): a select option's
visibleWhenreadingparentpassesos buildand the object save door, and the server's option gate then faults open ("allowed through; the option's gate was NOT enforced") #22157) editspackages/lint/src/validate-expressions.ts, which this claim excludes. - No open PR touches
plugin-approvals/src/sys-approval-request.object.ts(all open PRs' file lists read at this stamp).
Triage's order
6054592515, binding:- "the fix declares what the service attaches, in a shape the validator already reads. It does not teach the validator an exception for
viewer." - "If no existing shape can declare it, that is a spec addition. It is Clause-② yes, and the seat says so on this card before building."
So this claim is measurement-bounded:
- if an existing shape fits, the dev lands the declaration with
Clause-②: no; - if none fits, the dev reports
needs_decisionwith the measurement and builds nothing. This seat then posts theClause-② yesstatement here before any build.
- Step 1, measurement:
objectstack-fleet commented
on Oct 8, 2026 ContributorAuthorMore actionsos-dev-report
{ "issue": 22211, "status": "needs_decision", "branch": "claude/issue-22211-approval-viewer-declaration", "pr": null, "session": "session_01RPo7FUd6bSnAfkWMAKi848 (mode:subagent; this is the PM's id, and my identity is the branch). The newest Claim on #22211 (6062089126) names this branch and worktree objectstack-issue-22211. I checked it before any write.", "premise_still_valid": true, "summary": "Stopped at the fork, and built nothing. No existing declaration shape fits. The validator resolves `record.X` only by checking X against every key of the object's `fields` (any type) plus the closed set of platform-injected system columns. Every field type except `formula` creates a JSON or scalar column. `formula` is the only virtual type, and it is a row-derived expression the engine evaluates on every generic read. It returns only number, text, boolean or date, so it cannot carry the per-caller block. Any field declaration also enters the generated translation bundles, forms, list-view pickers and exports. And no shape gives the misspelling pin the dispatch asks for: the validator never resolves a segment after the first, so `record.viewer.can_actt` passes under every declaration. A spec addition is needed, Clause-② yes (widening). The proposed shape is in open_questions.", "measurement": { "where_the_validator_resolves_record_paths": [ "The field index is every key of `obj.fields`, in either the list or the map shape, with NO filter on field kind, unioned with `injectedColumnsFor(obj)`: packages/lint/src/validate-expressions.ts:152-177 (`buildFieldIndex`). Field kind is read only into the advisory type-soundness map (:259-281, `buildFieldTypeIndex`), where `json`, `composite` and every unmapped type become `dyn` (packages/formula/src/validate.ts:243-249).", "Action predicates are judged with that index, record scope: `checkAction`, validate-expressions.ts:2157-2169, over each object's `actions[]` at :2209-2213, through `check` at :1370-1375.", "The refusal: `checkFieldExistence`, packages/formula/src/validate.ts:625-648, emits `unknown-field` as an ERROR for each first segment not in the index. Its regex `RECORD_REF_RE` (:312) captures exactly ONE segment after `record.` or `previous.`.", "Nested paths: no pass resolves a segment after the first. `record.viewer.can_act` and `record.viewer.can_actt` are the same verdict under any declaration of `viewer`. packages/lint/src/validate-predicate-path-refs.ts:55-63 states that the `record.*` layer is not a closed key set, and leaves it to an open question on #7010.", "Injected names: `resolveInjectedSystemColumns` (packages/spec/src/data/injected-system-columns.ts:139) is a platform-owned closed plan, governed only by `systemFields` (packages/spec/src/data/object.zod.ts:2008-2049, closed to `tenant` and `audit`) and `ownership`. No authorable entry can add a name to it." ], "how_the_service_attaches_viewer": [ "`attachViewers`, packages/plugins/plugin-approvals/src/approval-service.ts:7018-7032 (doc :7001-7017). Shape: `{ can_act: boolean, is_submitter: boolean, can_override: boolean }`. It is computed per caller from `heldSlot`, the owner check and `isOverrideActor`. The status is ANDed into `can_act` and `can_override`.", "Read paths: `listRequests` (:6817) and `getRequest` (:6934) only, after `rowFromRequest` (:1032), which maps explicit keys. No other non-test source assigns `.viewer`.", "The generic data door serves this object (`apiMethods: ['get', 'list']`, sys-approval-request.object.ts:635) WITHOUT `viewer`, and there the 8 predicates fail closed by design (the comment at :399-401)." ], "candidate_shapes": [ "Shape `json` field (with `readonly`, `hidden` or `internal`) · the validator reads it: yes (any key) · fits: NO · side effects: a JSON column `viewer` on sys_approval_request, which is a schema migration on every deployment (packages/drivers/driver-sql/src/sql-driver.ts:19847, the JSON_COLUMN_TYPES branch of `createColumn`); a permanently stored null served on the generic door; `readonly` strips only non-system writes, so isSystem writes, seed replay and migration still write it (packages/spec/src/data/field.zod.ts:1894); `internal` omits it from generic reads but 'storage, filtering and indexing are untouched' (:1892); `hidden` is UI-only (:1859). This is the stored column for a per-caller block that the dispatch rules out.", "Shape `composite` field with declared sub-fields · the validator reads it: yes, first segment only; the sub-fields are never consulted, so it is no better than `json` for the misspelling pin · fits: NO · side effects: the same JSON column (FieldType comment 'stored as JSON on the parent row', field.zod.ts:87-88; sql-driver.ts:19847).", "Shape `formula` field · the validator reads it: yes, and it also judges its expression · fits: NO · it is the only virtual type: no column (sql-driver.ts:19806-19807 and :19101-19103; driver-turso remote-transport.ts:2044; plugin-audit audit-writers.ts:372 `VIRTUAL_FIELD_TYPES = {'formula'}`). But: (1) the engine evaluates it on every generic read with no projection (packages/objectql/src/engine.ts:1553), so the generic door would start serving a computed `viewer` key; (2) `returnType` is closed to number, text, boolean and date (field.zod.ts:1696), so no object block; (3) a formula is row-derived, and `viewer` is per caller (heldSlot over positions and teams, the admin override), which the card states cannot be a formula; any expression written there, `null` included, declares something false; (4) it enters the generated field labels (plugin-approvals/src/translations/en.objects.generated.ts, one entry per field, times 4 locales), forms, list-view column pickers and exports.", "Shape `summary` or `autonumber` · fits: NO · both are stored or rolled up from children (`RUNTIME_OWNED_FIELD_TYPES`, field.zod.ts:243-271), and neither describes a service-attached block.", "Shape injected system column (`systemFields`, `ownership`) · the validator reads it: yes (`injectedColumnsFor`) · fits: NO · it is a closed platform plan (injected-system-columns.ts:139; object.zod.ts:2008-2049); `viewer` cannot be added without a spec change.", "Shape surface roots (`ExprSchemaHint.roots`, packages/formula/src/validate.ts:94-160) · fits: NO · the validator sets roots per authoring surface, and object metadata never sets them; the predicates read `record.viewer`, not a root; using roots means editing validate-expressions.ts, which this claim excludes, and is the validator exception the order forbids." ], "the_eight_predicates_at_799eb000c": "sys-approval-request.object.ts :451-452 approve, :481-482 reject, :504-505 reassign, :525 send_back, :540 request_info, :567 remind, :608-609 recall, :624 resubmit. The object declares no `viewer` key in `fields` (grep: `viewer` occurs only in the comments at :389-418 and in these predicates)." }, "tests": "Read-only measurement: nothing installed, built, run or committed. Every claim above is a source read at origin/main 799eb000c, which equals the claim's stamp, with file:line. NOT MEASURED: an executed rerun of the 8 refusals at 799eb000c. Reason: the container has no node_modules and no built dist in any checkout, and this card is measurement-bounded. The verdict is the one #22032's pass-4 dev measured at 5d1d1aca6d (report 6053764412). The code path is re-read here at 799eb000c and unchanged in mechanism: formula validate.ts:312 and :625-648, lint validate-expressions.ts:152-177 and :2157-2213.", "mcp_calls": "0", "api_writes": "1 REST write: this os-dev-report comment, through scripts/pm/post-stamped.mjs (POST /repos/objectstack-ai/objectstack/issues/22211/comments, by whichever transport the tool's auto route picked). Not REST: one `git push -u` of the empty branch claude/issue-22211-approval-viewer-declaration (the write probe; it equals origin/main 799eb000c). No pr_create and no label-write, because nothing was built. Reads: single-card REST reads of #22211, its comments, and comment 6053764412.", "open_questions": [ { "question": "Clause-② yes (widening): which spec addition declares a block that a service attaches to the rows it serves, per caller and never stored, so that the build validator can judge `record.viewer.*`? No existing shape can declare it without a column, a generic-door change or a false formula (measurement above). The seat posts the Clause-② statement on this card before any build. NOTE: the dispatch did not carry the four-axis escalation framework, so the options below are compared on measured blast radius and cost only. They are NOT axis-graded; the seat grades them from its own copy.", "options": [ "A: an object-level declaration outside `fields`. A new optional `ObjectSchema` key, for example `attachedOnRead: { viewer: { description, attachedBy: 'getRequest/listRequests', keys: { can_act: 'boolean', can_override: 'boolean', is_submitter: 'boolean' } } }`, as a strict record of closed leaf maps. It is not a field, so no driver, column, form, list view, export, write path or translation bundle ever reads it: the drivers iterate `fields` only. Consumers from landing day (ADR-0049): (1) the validator adds the declared top-level names to the field-existence set, and judges the SECOND segment against `keys`, so `record.viewer.can_actt` is refused. That is the misspelling pin the dispatch wants, and no existing shape can give it. (2) A plugin-approvals conformance pin: the keys `attachViewers` emits equal the declared `keys`. Cost: spec `minor` (ObjectSchemaBase is a strictObject, object.zod.ts:1706, so this widens the accept set); `patch` for lint and formula (buildFieldIndex in validate-expressions.ts, serial behind open PR #22268; checkFieldExistence in formula validate.ts:625 for the nested segment) and for plugin-approvals; plus the generated spec artifacts (schema, authorable-surface baseline, docs) and a liveness-ledger row. It also gives the service's other undeclared read attachments a home: `decision_progress`, `pending_approver_groups` and the flow steps (approval-service.ts :6994, :6997, :7065). None of them is read by a predicate today.", "B: a field-level 'not stored' marker, either a new FieldType member or a new `storage` key such as `persisted: false` (`storage` is the ADR-0113 strict block, field.zod.ts:1160). Every column-creating seam must honor it from landing day: driver-sql `createColumn` and schema-drift (its default branch would otherwise create a column, sql-driver.ts:19808-19847), turso, mongodb, memory and sqlite-wasm, the objectql write path (refuse writes) and read projection. The field still enters forms, list-view pickers, exports and the translation bundles unless each of those learns the marker too. And sub-keys are still unjudged, unless `composite`-style sub-fields are reused AND the validator learns nested segments. Cost: spec `minor`, plus `patch` across 5 drivers, objectql and lint. This is the largest blast radius of the three.", "C: move the gate off `record`. The action surface mounts a `viewer` root, and the predicates become `viewer.can_act`. That is a renderer binding in objectui, an authoring-surface root in the validator (validate-expressions.ts, excluded by this claim), and a change to where the service attaches its result. It is cross-repo, it changes the authored predicates rather than declaring them, and it arguably reopens how the #3310 surface is shaped, which the card says is settled." ], "recommendation": "A. It is the only shape with no storage, no write-path, no generic-door and no UI or translation side effect, because nothing but the validator and one conformance pin reads it. It is also the only one that can make `record.viewer.can_actt` a refusal, so the declaration is not a wildcard. Its build is serial behind #22268 (validate-expressions.ts). Declared arm: `Clause-②: yes (widening)`, with a `minor` changeset on @objectstack/spec." } ], "out_of_scope_findings": [ "carrier: the build that follows whichever option the seat picks · noted, not filed. The validator never resolves a `record.X.Y` segment after the first (formula validate.ts:312; lint validate-predicate-path-refs.ts:55-63, which leaves it open on #7010), so a misspelled nested key on ANY declared json or composite field passes the build. This is read-only inference, not a reproduced defect, so it is not filed. Option A closes it for declared attachments only." ], "files_changed": [], "gates": "None run: nothing was built or committed, so `dispatch-gates.mjs --commands` has no change set to derive from. NOT MEASURED: every gate family, reason: no diff.", "deviations": [ "Stopped at the first fork, as the measurement-bounded claim requires: no existing shape fits without side effects, so nothing was built, no PR was opened and no label was written.", "os-dev expects the dispatch to carry the four-axis escalation framework for a needs_decision report. This dispatch did not carry it, and per os-dev I did not invent one. The options are compared on measured blast radius only, and the seat grades them.", "The dispatch's Step-2 pin ('a misspelled `record.viewer.can_actt` is still refused') cannot be met by ANY existing shape, because the validator checks only the first segment. That alone would have forced this fork, even if a side-effect-free shape had existed.", "The empty branch claude/issue-22211-approval-viewer-declaration is pushed (equal to origin/main 799eb000c) and left for the seat to reuse or delete. The worktree had no install and no edits; it is removed after this report is posted, without --force." ] }
Generated by Claude Code
objectstack-fleet commented
on Oct 8, 2026 ContributorAuthorMore actionsSeat verdict: ACCEPT the measurement,
needs-user-decision: how to declare a block a service attaches per callerdomain:specseat 3 (#18883) ·os-justin· sessionsession_01RPo7FUd6bSnAfkWMAKi848· 2026-10-08T14:38Z · holder of claim6062089126, which this act releases.Checked on
origin/main799eb000c, not from the report (6062234309):buildFieldIndex(packages/lint/src/validate-expressions.ts:152–:177) takes every key offieldsplusinjectedColumnsFor(obj), the closed set of platform-injected columns.checkFieldExistence(packages/formula/src/validate.ts:625–:648) refuses the first segment that is not in that index, andRECORD_REF_RE(:312) captures only one segment.attachViewers(packages/plugins/plugin-approvals/src/approval-service.ts:7018–:7032) attaches{ can_act, is_submitter, can_override }to each row onlistRequests/getRequestonly.- Every field type except
formulacreates a column, andformulais row-derived, returning only number / text / boolean / date.
Triage
6054592515set two conditions: "in a shape the validator already reads", and "if no existing shape can declare it, that is a spec addition. It is Clause-② yes, and the seat says so on this card before building". The measurement found no existing shape that works without a side effect. This is a spec addition,Clause-②: yes (widening). The options differ in the shape of the public contract, so the choice goes to the maintainer.Release:
session_01RPo7FUd6bSnAfkWMAKi848· why: the measurement round is delivered, and choosing the contract shape is the maintainer's · to:needs-user-decision, unassigned. Whoever implements the ruled shape claims afresh. Branchclaude/issue-22211-approval-viewer-declarationis empty, equal to799eb000c, and can be deleted.
待裁(
needs-user-decision):审批服务在读取时按调用者附加的viewer块,该怎么声明,才能让构建校验器判得了它?维护者速读
- 改了什么: 什么都没改。本轮只做了测量,没有 PR。
- 测出的关键事实:
- 审批单
sys_approval_request自带 8 个操作按钮(同意、拒绝、转办等)。它们的显示条件读record.viewer.can_act这类值。 viewer不是存储的字段。它是审批服务读取时按「谁在看」现算、再附上去的。- 构建校验器只认对象里声明了的字段,所以平台自带的这 8 个条件在
os build里全部报错。以OS_METADATA_WRITABLE=object发布保存这个对象,也会被拒。 - 现有的字段类型里,没有一种能既声明它、又不建数据库列、不进表单和翻译。
- 校验器只检查第一段:
record.viewer.can_actt这样的拼错,现在无论怎么声明都拦不住。
- 审批单
- 席位意见: 推荐 A:在对象上新增一个与
fields并列的声明,专门登记「服务读取时附加的块」。 - 你要做的: 回一个字母,A / B / C。
一句话问题
平台里有「读取时按调用者计算、从不存储」的数据,这类数据在元数据里用什么形状声明?
Governing text
- triage
6054592515:「the fix declares what the service attaches, in a shape the validator already reads. It does not teach the validator an exception forviewer」。没有现成形状时,按 spec 新增处理,Clause-②: yes。 - ADR-0049(enforce-or-remove):声明必须有读者兑现。
- [P3] approvals: server-computed viewer capability on getRequest (precise approver gating for declared actions) #3310:按调用者计算的语义已经裁定;本卡只问声明形状。
前提(各带复核命令)
- 校验器只认
fields加注入列。git show origin/main:packages/lint/src/validate-expressions.ts | sed -n 152,177p - 只判第一段。
git show origin/main:packages/formula/src/validate.ts | sed -n 312p viewer的形状与附加点。git show origin/main:packages/plugins/plugin-approvals/src/approval-service.ts | sed -n 7018,7032p
选项 × 真实代价
选项 做什么 代价 A 在 ObjectSchema上新增可选键(暂名attachedOnRead),登记块名和它的叶子键与类型。它不是字段,驱动、表单、导出、翻译都不读它。校验器把块名加入可识别集合,并且判第二段,所以can_actt会被拒。plugin-approvals 加一个一致性测试:attachViewers实际附加的键要等于声明的键。spec minor,lint / formula / plugin-approvals 各一个patch。lint 那一侧要排在 #22268 之后串行。审批服务另外几个读时附加的块(decision_progress等)以后也有地方登记。B 给字段加一个「不存储」标记(新类型,或 storage.persisted: false)。5 个驱动、objectql 的写入和读取路径都要认这个标记,表单、导出、翻译也要各自认。第二段仍然判不了。影响面最大。 C 不再从 record读,改在操作按钮这一层挂一个viewer根变量。要动 objectui 的渲染绑定和校验器的作者面(正是 triage 禁止的例外),等于重开已经裁定的 #3310 形态。 业务含义直译
- A: 给「服务现算、不落库」的数据单独开一本登记簿,校验器照着登记簿查,连拼错都能查出来。
- B: 硬把它塞进字段登记簿,再在每个读字段的地方加一句「这个别存」。
- C: 换一扇门,不从
record进。
os-decision-facets
- ① 项目长远合理性:
- A 把「读时附加的块」做成一等声明:一处登记、一个读者,以后再有这类块(审批进度、待审组)也有地方放。
- B 让每个处理字段的地方都多一个分支,长期维护成本最高。
- C 改的是已经裁定的作者面。
- ② 实际业务拉动:
- 实测拉动:平台自带的审批单今天就有 8 个按钮条件被构建报错,发布保存会被拒。
- A 只给这一个真实生产者开口子,此外还有 3 个已知的读时附加块。
- B 的影响面远大于需求。
- ③ 防 AI 犯错:
- A 是三者里唯一能拒收
record.viewer.can_actt这种拼错的。 - 声明加一致性测试,保证声明的键等于服务实际附加的键,不会声明了却不兑现。
- B、C 都只能放行第一段。
- A 是三者里唯一能拒收
- ④ 创业阶段不扩散:
- A 新增一个可选键,读者只有校验器和一条一致性测试,面最小。
- B 牵动 5 个驱动。
- C 跨仓。
- 新增门禁:无(沿用现有的
unknown-field规则)。
Prior rulings read: triage
6054592515(this card); #3310 (the per-callerviewersemantics, settled); #7010 (record.*nested keys as an open question,validate-predicate-path-refs.ts:55–:63); #22032's pass-4 report6053764412(the measured refusal). None rules the declaration shape.推荐
A。 读者只有校验器和一条一致性测试,不建列、不碰写入路径和界面。它也是唯一能把嵌套键的拼错变成拒收的形状。
- 自检: 只看 ① 选 A;②③④ 都不翻转。
- 回退: 如果暂不想加 spec 键,就维持现状:8 个条件在构建里报错,平台运行时按设计在通用数据门「失败即关闭」。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 第 4 部分的 changeset 已经记下这个偏差。
- 置信缺口: 这 8 处拒收,本轮没有在
799eb000c上重新执行。用的是 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 第 4 部分 dev 在5d1d1aca6d上的实测,代码路径已逐行复读,机制没有变化。
裁后执行
- 选 A: spec 卡(新键 + 第二段判定 + liveness 台账行)和 plugin-approvals 卡(声明 + 一致性测试),lint 侧排在 fix(lint)!: os build and the object save door refuse a select option's visibleWhen that reads parent (#22157) #22268 之后;附契约复核。
- 选 B / C: 按选项重新立卡,并先把影响面清单化。
objectstack-fleet commented
on Oct 8, 2026 ContributorAuthorMore actionsRuling: batch #295 item 1 · letter A · maintainer 「同意」 2026-10-08T23:18Z
Director seat, summon #35,
session_01VYToj6PQehTEKNrjGM9akg(GitHubos-zhuang; written asobjectstack-fleet[bot]via the relay). Presented in batch #295 from thedomain:specseat 3's decision request (6062299739) on the measurement report 6062234309: A a new optionalObjectSchemakey, working nameattachedOnRead, registering the blocks a service attaches per caller on read with their leaf keys and types; B a field-level "not stored" marker; C aviewerroot variable on the action surface. The seat recommended A; the maintainer answered 「同意」. Thread-read: 6062299739. Freshness: no comment since the presentation; body unchanged; labelsbug,needs-user-decision,domain:spec,priority:p3,area:workflow. Premises re-read onorigin/mainb460153912:buildFieldIndex(packages/lint/src/validate-expressions.ts:152,:174) indexes everyfieldskey plusinjectedColumnsFor(obj);RECORD_REF_RE(packages/formula/src/validate.ts:312) captures one segment afterrecord.;attachViewersis called fromlistRequestsandgetRequestonly (packages/plugins/plugin-approvals/src/approval-service.ts:6817,:6934); the stringattachedOnReadoccurs nowhere in the repository.The ruling
A — an object-level declaration beside
fields.ObjectSchemagains one optional key, working nameattachedOnRead: a strict record of the blocks a service attaches to the rows it serves, computed per caller and never stored, each block naming its leaf keys and their types. It is not a field. Drivers, forms, list views, exports, write paths and translation bundles never read it. Its readers from landing day (ADR-0049, enforce-or-remove): (1) the shared validator adds each declared block name to the field-existence set and judges the second segment against the block's declared keys, under the existingunknown-fieldrule, sorecord.viewer.can_acttis refused; (2) a plugin-approvals conformance test pins that the keysattachViewersemits equal the keyssys_approval_requestdeclares.sys_approval_requestdeclaresviewerwithcan_act,can_overrideandis_submitter, allboolean. ⛔ Not taken: B (a stored-column marker that every driver, the write path, forms, exports and translations must each learn, and the nested segment still unjudged), C (moves the gate offrecord, is cross-repo, and reopens the #3310 surface the card calls settled). ⛔ Not ruled: a validator exception forviewer; triage's order stands (no new rule, no new refusal code; the declaration is what the existing rule reads). The service's other read attachments (decision_progress,pending_approver_groups, the flow steps) may be declared under the same key when a predicate first reads them; this ruling requires none of them.Prior rulings read: triage 6054592515 (this card: declare in a shape the validator reads, else a spec addition with Clause-② yes); #3310 (the per-caller
viewersemantics, settled); #7010 (record.*nested keys, open atvalidate-predicate-path-refs.ts:55–:63); #22032 pass-4 report 6053764412 (the measured refusal) and its order 6024268378; ADR-0049.check-prior-rulingsover 7 terms → 3 ADR hits (ADR-0005 §5, ADR-0105 D5, ADR-0119 D3), all on the termenforce-or-removealone, none on the declaration shape; thread: 0 rulings of 4 comments. 自检: 只看①选 A;②③④ 是否翻转:否(③ 反而加强:三者中唯一能拒收第二段拼错的形状)。置信缺口:8 处拒收未在b460153912上重新执行,沿用 #22032 第 4 部分在5d1d1aca6d的实测,代码路径逐行复读未变;attachedOnRead是暂名,终名由 spec 卡定。State
needs-user-decision→pm:queue(domain:spec,priority:p3) in this act; theRuled:line added to the body. The spec seat files the two build cards the verdict names: spec (the new key, the second-segment judgement, the liveness-ledger row;Clause-②: yes (widening), aminorchangeset on@objectstack/spec) and plugin-approvals (the declaration and the conformance test); the lint side is serial behind fix(lint)!: os build and the object save door refuse a select option's visibleWhen that reads parent (#22157) #22268; contract review attached. Branchclaude/issue-22211-approval-viewer-declarationis empty and may be deleted by whoever claims.
Generated by Claude Code
objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionspm:queue→pm:blocked: the ruling's two build cards are filed, #22386 (spec) and #22387 (plugin-approvals) ·Blocked-by: #22387domain:specseat 2 (#18549) ·os-sales· sessionsession_01DhTqaEHqPVSVnAkjG3jywn· 2026-10-09T01:03Z · acting on the ruling of record6070963704. Thread-read: 6070963704.The ruling's state section reads: "The spec seat files the two build cards the verdict names: spec (the new key, the second-segment judgement, the liveness-ledger row;
Clause-②: yes (widening), aminorchangeset on@objectstack/spec) and plugin-approvals (the declaration and the conformance test); the lint side is serial behind #22268; contract review attached."Filed (
finding, for triage's first grade and route):- spec(data):
ObjectSchema.attachedOnRead— an object declares the blocks a service attaches per caller on read, and the validator judgesrecord.<block>.<leaf>against it (#22211 ruling A, spec half) #22386, the spec half: theObjectSchemakey (working nameattachedOnRead), the validator's second-segment judgement under the existingunknown-fieldrule, the liveness-ledger row, and the contract review. - plugin-approvals:
sys_approval_requestdeclares its per-callerviewerblock underattachedOnRead, with a conformance test againstattachViewers(#22211 ruling A, plugin-approvals half) #22387, the plugin-approvals half:sys_approval_requestdeclaresviewer, and a conformance test checks it againstattachViewers. It isBlocked-by: #22386, and its PR carriesFixes #22211.
The ruling's premises, re-read on
origin/mainat this stamp:buildFieldIndexis atpackages/lint/src/validate-expressions.ts:152, andinjectedColumnsForat:174.RECORD_REF_REis atpackages/formula/src/validate.ts:312, one segment.attachViewersis called from:6817and:6934.attachedOnReadhas 0 hits.- The serial predecessor, PR fix(lint)!: os build and the object save door refuse a select option's visibleWhen that reads parent (#22157) #22268, merged at 2026-10-09T00:43Z.
This card:
pm:queue→pm:blockedin this act, and the body gainsBlocked-by: #22387. It stays unassigned and holds the defect's record. It closes when #22387's PR lands. No seat dispatches it on its own.
Generated by Claude Code
- spec(data):
- added a commit that references this issue
on Oct 9, 2026 objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsClosing as
completed: its plan said it closes when #22387's PR lands, and PR #22479 landedTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-09T14:06Z. Blocked audit. ⛔ Not a claim, ⛔ not a dispatch.Thread-read: 6072125030
- My
6072125030said this card "stays unassigned and holds the defect's record. It closes when plugin-approvals:sys_approval_requestdeclares its per-callerviewerblock underattachedOnRead, with a conformance test againstattachViewers(#22211 ruling A, plugin-approvals half) #22387's PR lands". - PR fix(plugin-approvals): sys_approval_request declares its per-caller viewer block under attachedOnRead (#22387) #22479 (
Fixes #22387) merged as3403be84cbat 2026-10-09T13:37Z. Its body did not carryFixes #22211, so this card stayed open. - On
main:sys_approval_requestdeclaresattachedOnRead.viewer(sys-approval-request.object.ts:383). The shared validator reads the declared block (spec(data):ObjectSchema.attachedOnRead— an object declares the blocks a service attaches per caller on read, and the validator judgesrecord.<block>.<leaf>against it (#22211 ruling A, spec half) #22386, PR feat(spec): ObjectSchema.attachedOnRead declares per-caller read attachments, and the shared validator judges record.BLOCK.LEAF against them #22425). The 8 actionvisiblepredicates that readrecord.viewerare therefore judged against declared leaves instead of being refused as an undeclared field. - The follow-up (the block must count only at served-row sites, not flow conditions) is lint: a declared
attachedOnReadblock is accepted in a flow condition on its object, where the bound row is the stored row and never carries it (os validatepasses, the flow fails at run time) #22481, already graded.
- My
Blocked-by: #22387
Ruled: 6070963704 · letter A · 2026-10-08T23:19Z
Filing gate: ① a product defect with a named producer.
reach:named real producer,packages/plugins/plugin-approvals/src/sys-approval-request.object.ts. The platform ships object metadata that the validatorformulas.mdxsays "backsos buildand metadata registration" refuses at error. Reported by #22032's pass-4 dev (report6053764412,out_of_scope_findings[0], measured). Read in source and filed by thedomain:specseat 1 (seat post #6017,session_01LAi5BVvQNiYzepSAcsoFLK). ⛔ Not graded or routed here; ⛔ not a claim.What was measured (by #22032's dev, at
origin/main5d1d1aca6d; none of the files read moved up to7d7943dd0d)sys_approval_requestdeclares 8 actions whosevisiblereadsrecord.viewer.can_act,record.viewer.can_overrideorrecord.viewer.is_submitter:approval_approve,approval_reject,approval_reassign,approval_send_back,approval_request_info,approval_remind,approval_recall,approval_resubmit(:426–:617). The comment block at:388–:418gives the design ([P3] approvals: server-computed viewer capability on getRequest (precise approver gating for declared actions) #3310) and the leaf guards.viewercomes from.ApprovalServiceattaches a per-viewer capability block to each row it reads (approval-service.ts:7002–:7026, [P3] approvals: server-computed viewer capability on getRequest (precise approver gating for declared actions) #3310). The object does not declarevieweras a field.validateStackExpressionstogether withrunAuthoringRules('build'), refuses all 8 at error:unknown field `viewer` on `sys_approval_request`. These are the only refusals among the 117 action predicates the repository ships: 58 on raw object files and 59 on thedefineStackoutput of the example apps.PUT /api/v1/meta/object/sys_approval_requestwith the served body, underOS_METADATA_WRITABLE=object, answers422 INVALID_METADATAwith 8expression-invalidissues.?mode=draftanswers 200.403 NOT_OVERRIDABLEbefore the gate, with or without the lift.The question this card carries
vieweris computed per caller: the pending approver, the submitter, the override actor. So it cannot be a formula, andcurrent_useralone does not express position or team approvers. How a per-viewer gate should be declared so that the build validator can judge it is a design question. It is open, and this card does not answer it.The validator's verdict comes from triage's order on #22032 (
6024268378: "No new rule, and no new refusal code"). That order makes widening the validator for one object the default no.Not this card
#22032 pass 4 lifts the object door's action fence, so the door gives the verdict
os buildalready gives. Thedomain:specseat ruled that pass 4 is not blocked by this card; the ruling is on #22032. Until this card lands, pass 4's changeset names the drift: a publish save of this one packaged object under theOS_METADATA_WRITABLE=objecthatch.Dedupe
MCP
search_issues, repo-scoped, closed included:approval_recallaction hides the #3424 admin-override recall — the service admits it, the visible predicate never shows it #12716 (closed;approval_recall's predicate hid the Approval routed to an empty position permanently locks the record (no admin override, no recovery) #3424 override). It is not this card.viewerblock, and finding(plugin-approvals): Setup → Approvals → Requests opens sys_approval_request's caller-scoped first view my_pending, so an administrator sees only requests pending on themselves (#21972's eighth family member) #21984 (closed) is the first view of the same object. Neither is this card.Dedupe words:
approval request action visible record.viewer unknown field·sys_approval_request viewer block undeclared field validator refuses·plugin-approvals action predicate viewer can_act build error