You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
formula: label(record.FIELD) on a field that declares no options is admitted at the active authoring door, then reads null on every row #28659
Filed by the PM seat for the W3-RT runner session_017xRBcRX4wwwVw5bEckffdp, whose own write was refused by its session permissions (RUNNER rule 2: staged text, posted verbatim through the same door).
QA-source: #28656 · api-backend.formula-stdlib-matrix · observed beside the clauses
What happens
The active object-authoring door refuses a formula that names an unknown field (label(record.nope) → 422 expression-invalid) or calls an unregistered function (sqrt(4) → 422). But label() pointed at a field that exists and declares no options (a text field) is admitted with 200. Every row then reads the formula as null, and a WARN line appears in the server log. A compound expression goes through the same way: label(record.kind) + label(record.first) → 200.
Expected: refused at author time with 422 expression-invalid naming the field. The door has the object schema in hand, and label() is only meaningful on a select or option-bearing field.
Measured at framework 6e11a929 (main, 2026-11-01), on a stock showcase boot with a runtime-installed scratch package.
Reproduction
As admin, install a writable scratch package: POST /api/v1/packages with manifest {"id":"com.objectstack.qa.formula","namespace":"qaformula",…} and enableOnInstall → 201.
PUT /api/v1/meta/object/qaformula_p4?package=com.objectstack.qa.formula with {"name":"qaformula_p4","sharingModel":"private","fields":{"first":{"type":"text"},"kind":{"type":"select","options":[{"value":"clean","label":"Clean"}]},"pf":{"type":"formula","expression":"label(record.first)"}}} → 200 (expected 422).
POST /api/v1/data/qaformula_p4{"first":"hello","kind":"clean"} → 201 with "pf": null. The boot log shows WARN "formula field 'pf' on 'qaformula_p4' could not be evaluated … first declares no options".
Mechanism
No lint or expression rule checks the target of label() (a grep of packages/lint/src finds no label-target or option check). The refusal exists only at evaluate time, in optionLabelOf in packages/formula/src/stdlib.ts (around L412–417). The authoring gate (expression-invalid, #22019) type-checks the call and does not look up whether its field argument carries options.
Not a reopen of #22019, which covered unregistered functions; this is a registered macro pointed at the wrong kind of field. Related: #26608 introduced label().
Read fresh. label() on a field that declares no options is admitted at the authoring door and reads null on every row. The door refuses unknown fields and functions, but not this. p3.
Claim: PM loop round 4 · card thread · 2026-11-01T13:31Z
Session: session_0186vfjHHMsRXC9EUT9hTFMo (the seat); the card thread is session_01FEkQ5pQB2Jwsce3RvocUcS
Account: os-project-manager (the seat's linked user; the card's assignee)
Branch: claude/project-thread-m6mfzr (the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)
Worktree: the card thread's own container checkout
Domain: domain:server
Seat: domain:server#2
Ruling-ref: the maintainer, 2026-11-01T03:46Z: 「server 车道现在就是你负责,任务有一大堆啊,都可以派,包括 p3」, with 2026-10-26T10:29Z 「p1 p2 处理完了,就可以处理 p3」. Triage p3 (6558290991) stands; offered to this seat by seat 1.
File surface: packages/lint/src/authoring-rules.ts (the expression-invalid gate), packages/formula/src/ read-only or minimal, tests; changesets
Container & model: M, mode:thread, model: opus (--tier names no path mandate: PM's call, default), effort: medium
Clause-②: no (narrowing) expected — a formula whose label() argument names an optionless field, admitted before, is refused
Metadata-protocol: none (expected; the dev measures and declares)
Responsibility: the dev extends the one existing authoring gate to refuse label() on an optionless field, with red-first, controls and ablation; the seat runs the contract review owed and lands
Thread-read: none
Premise: measured at 6e11a929 by the QA runner; the dev re-measures on origin/main first
Serial constraints cleared: seat 1's PR #28679 (#27904) touches packages/formula/src/; seat 2's #28660 touches stdlib.ts in another function; the dev merges origin/main if either lands first
Resume-from: none — fresh from origin/main
State: → pm:dispatched, assigned to os-project-manager, recorded by this claim.
domain:server#2 · os-project-manager · session_01FEkQ5pQB2Jwsce3RvocUcS
os-dev-report
{
"issue": 28659,
"status": "done",
"branch": "claude/project-thread-m6mfzr",
"pr": "#28714",
"session": "session_01FEkQ5pQB2Jwsce3RvocUcS",
"premise_still_valid": true,
"summary": "Premise re-measured on origin/main: validateExpression('value','label(record.first)', ...) ok:true, and the live showcase door saved it 200 and read pf null with one WARN. The expression-invalid gate now refuses label(record.FIELD) on a declared field that declares no options: formula's validateExpression takes an opt-in ExprSchemaHint.optionFields and refuses under a new closed-set code label-field-no-options (params {field}) in the evaluator's own fault sentence (optionlessLabelSentence, shared with optionLabelOf). 'Declares options' has one spelling, declaresOptions (formula, exported), now read by objectql's label map and by lint's formula pass, which passes optionFields. No new rule id. File surface beyond the claim: the gate body is lint/validate-expressions.ts (authoring-rules.ts unchanged), plus objectql/formula-option-labels.ts for the one-predicate reuse and one docs sentence.",
"tests": "Head 0cd2057. formula test exit 0 (62 files / 1590 tests), lint test exit 0 (185 / 7730), objectql test exit 0 (512 / 9898); typecheck exit 0 for all three. New: formula validate-option-label-target.test.ts (11), lint runtime-gate.object-formula-label-target.test.ts (12, os validate/build/lint + runtime gate); expression-refusal-codes pin + sweep extended; #5017 receiver meta-test excuses two plumbing locals. Red on base (six src files at base, formula rebuilt, preflight marker absent): formula 7 failed/30 passed, lint 8 failed/4 passed; restored, git diff HEAD empty. Ablation via ablation-replace.mjs (anchor 1->0, blob e7327b51ea43->991a89c6da33, marker hit dist/index.js; DTS step exit 1 from lost narrowing, JS emitted): formula 5 failed/6 passed, lint 8 failed/4 passed; restored blob == HEAD, rebuilt, marker absent. An earlier ablation leg broke the formula build and was discarded. Live REST (showcase, scratch package): base PUT label(record.first) 200, compound 200, publish of a draft 200, data pf null; branch PUT 422 expression-invalid naming first for both bodies, draft _diagnostics (GET ?state=draft) carries the error, publish 422; controls label(record.kind) 200 reading 'Clean', label(record.nope) 422 unknown field unchanged.",
"gates": "dispatch-gates --commands: 89 derived, 89 run, 0 NOT-MEASURED, 0 UNRUN (--ran exit 0), all exit 0; check:type-check-debt first exit 3 (queued behind the dogfood lock holder), exit 0 on re-run. Record: /mnt/project-files/pm-orders/server/28659-gates-ran.txt. check-changeset-no-major local: no major (level axis is PR-scoped, read in CI). CI check-runs on #28714: in_progress at report time.",
"dogfood_battery": "0cd20573 · os-verify-lock VERDICT command-exit 0 (held 2142s, shared box) · pnpm --filter @objectstack/dogfood test exit 0 · Test Files 369 passed, 3 skipped (372) · Tests 2981 passed, 28 skipped (3009) · no failures",
"mcp_calls": "6 — issue_read x3, pull_request_read x2 (get #28679, get_files #28681), no write tools",
"api_writes": "2 — fleet-write relay pr_create (POST /repos/objectstack-ai/objectstack/dispatches -> run 41319006331 -> PR #28714, no assignees, no labels); this os-dev-report comment (POST /repos//issues/28659/comments). Plus git push x2 (not REST).",
"open_questions": [
{
"question": "Changeset level: the order asked for patch; AGENTS.md step 3 makes a door now refusing a value it stored 'narrowed', which takes minor (check-changeset-no-major refuses narrowed without a minor). Shipped formula+lint minor, objectql patch.",
"options": [
"A keep minor + Metadata-protocol: narrowed (as shipped)",
"B patch + none (the gate would refuse it, and it misdeclares the narrowing)"
],
"recommendation": "A, because the declaration must be the measured kind and the gate enforces the pairing."
}
],
"out_of_scope_findings": [
"class: b · reach: MCP validate_expression tool (site formula) answers ok for label(record.first) on a text field where the object save door answers 422 · evidence: packages/mcp/src/mcp-http-tools.ts builds the hint from bridge.describeObject, and both bridges (packages/runtime/src/domains/mcp.ts describeObject, packages/mcp/src/stdio-data-bridge.ts describeObject) project fields to name/type/label/required, so options never reach validateExpression · Seam: spec:ExprSchemaHint.optionFields -> runtime:mcp-http-tools validate_expression · dedupe words: validate_expression optionFields, describeObject options, MCP label() parity",
"carrier: none · Studio live CEL lint (ui/packages/app-shell/src/views/metadata-admin/celAuthoring.ts lintCelPredicate) passes fields/scope only, so it cannot pre-warn label() on an optionless field; the save door's 422 still reaches Studio. Noted, not filed."
]
}
The card thread measured that the expression-invalid gate's body for formula fields is packages/lint/src/validate-expressions.ts, not authoring-rules.ts. It also measured that the "declares options" reading the evaluator uses had to be shared rather than spelled twice.
The amendment:
The file surface is packages/lint/src/validate-expressions.ts (the gate);
a new packages/objectql/src/formula-option-labels.ts (the shared declaresOptions predicate, which the evaluator's option-label reader and the gate both use);
packages/formula/src/ for the reader's call;
one docs sentence;
the tests and the changesets.
packages/lint/src/authoring-rules.ts is not touched.
Metadata-protocol:narrowed, so @objectstack/formula and @objectstack/lint ship minor and @objectstack/objectqlpatch, as the no-major gate pairs narrowed with minor. That replaces the claim's expected patch.
Clause-②:no (narrowing), as expected. The contract review is running before the queue.
domain:server#2 · os-project-manager · session_01FEkQ5pQB2Jwsce3RvocUcS
os-dev-report
{
"issue": 28659,
"status": "done",
"branch": "claude/project-thread-m6mfzr",
"pr": "#28714",
"session": "session_01FEkQ5pQB2Jwsce3RvocUcS",
"premise_still_valid": true,
"summary": "Review round 1 (contract review FAIL, comment 6559777722) fixed on head c41843d: lint's formula pass read the AUTHORED body through declaresOptions (served form), so it refused label(record.tier) on a select bound to a shared picklist with no inline options, which works at runtime once objectql resolves the list. Added declaresOptionsOnceServed (formula, exported beside declaresOptions): inline options OR typeof picklist === 'string' && picklist !== '' (R8's reading). Lint optionFieldNames now reads it; declaresOptions stays the served-form predicate objectql's label map reads, so the evaluator is unchanged for an unresolved picklist. Comments, changeset Unchanged bullet and Remedy, docs sentence ('a field that declares options or names a picklist') and the PR body list restated; the changeset names every door that runs the active authoring gate (save door, per-item publish, history restore, draft _diagnostics). Round 0: Premise re-measured on origin/main: validateExpression('value','label(record.first)', ...) ok:true, and the live showcase door saved it 200 and read pf null with one WARN. The expression-invalid gate now refuses label(record.FIELD) on a declared field that declares no options: formula's validateExpression takes an opt-in ExprSchemaHint.optionFields and refuses under a new closed-set code label-field-no-options (params {field}) in the evaluator's own fault sentence (optionlessLabelSentence, shared with optionLabelOf). 'Declares options' has one spelling, declaresOptions (formula, exported), now read by objectql's label map and by lint's formula pass, which passes optionFields. No new rule id. File surface beyond the claim: the gate body is lint/validate-expressions.ts (authoring-rules.ts unchanged), plus objectql/formula-option-labels.ts for the one-predicate reuse and one docs sentence.",
"tests": "Head c41843d. formula test exit 0 (62 files / 1593 tests), lint test exit 0 (185 / 7732), objectql test exit 0 (513 / 9901); typecheck exit 0 for all three. Round 1 adds: picklist-bound control (tier: select, picklist 'qa_tiers', no inline options; label(record.tier) and label(record.tier) + ' / ' + label(record.kind)) at os validate/build/lint and runRuntimeAuthoringRules (runtime-gate test now 14); declaresOptionsOnceServed truth table, authored/served picklist pair and validator acceptance in validate-option-label-target.test.ts (now 14). Ablation of the picklist half (ablation-replace anchor 'fieldList.filter(([, f]) => declaresOptionsOnceServed(f))' -> declaresOptionsOnceServed({ ...f, picklist: undefined }), anchor 1->0): runtime-gate 2 failed / 12 passed, exactly the two picklist controls; restored blob 511b9f3952a6 == HEAD, git diff HEAD empty. No live REST probe for the picklist control (unit door test drives the same runRuntimeAuthoringRules). Round 0: Head 0cd2057. formula test exit 0 (62 files / 1590 tests), lint test exit 0 (185 / 7730), objectql test exit 0 (512 / 9898); typecheck exit 0 for all three. New: formula validate-option-label-target.test.ts (11), lint runtime-gate.object-formula-label-target.test.ts (12, os validate/build/lint + runtime gate); expression-refusal-codes pin + sweep extended; #5017 receiver meta-test excuses two plumbing locals. Red on base (six src files at base, formula rebuilt, preflight marker absent): formula 7 failed/30 passed, lint 8 failed/4 passed; restored, git diff HEAD empty. Ablation via ablation-replace.mjs (anchor 1->0, blob e7327b51ea43->991a89c6da33, marker hit dist/index.js; DTS step exit 1 from lost narrowing, JS emitted): formula 5 failed/6 passed, lint 8 failed/4 passed; restored blob == HEAD, rebuilt, marker absent. An earlier ablation leg broke the formula build and was discarded. Live REST (showcase, scratch package): base PUT label(record.first) 200, compound 200, publish of a draft 200, data pf null; branch PUT 422 expression-invalid naming first for both bodies, draft _diagnostics (GET ?state=draft) carries the error, publish 422; controls label(record.kind) 200 reading 'Clean', label(record.nope) 422 unknown field unchanged.",
"gates": "Head c41843d: dispatch-gates --commands 89 derived, 89 run, 0 NOT-MEASURED, 0 UNRUN (--ran exit 0), all exit 0; check:type-check-debt (lock contention), check:skill-examples and check:dual-build-cjs-loads (before full build) first answered non-zero without measuring, exit 0 on re-run. Record: /mnt/project-files/pm-orders/server/28659-gates-ran.txt. PR body v2 for the seat to apply: /mnt/project-files/pm-orders/server/28659-pr-body-v2.md.",
"dogfood_battery": "c41843d6 · os-verify-lock VERDICT command-exit 0 (held 2306s, shared box) · pnpm build exit 0 · pnpm --filter @objectstack/dogfood test exit 0 · Test Files 371 passed, 3 skipped (374) · Tests 2994 passed, 28 skipped (3022) · no failures",
"mcp_calls": "6 — issue_read x3, pull_request_read x2 (get #28679, get_files #28681), no write tools",
"api_writes": "Round 1: 1 — this os-dev-report comment (POST /repos//issues/28659/comments). Plus git push x1 (not REST). PR body not patched by the dev. Round 0: 2 — fleet-write relay pr_create (POST /repos/objectstack-ai/objectstack/dispatches -> run 41319006331 -> PR #28714, no assignees, no labels); this os-dev-report comment (POST /repos//issues/28659/comments). Plus git push x2 (not REST).",
"open_questions": [],
"out_of_scope_findings": [
"class: b · reach: MCP validate_expression tool (site formula) answers ok for label(record.first) on a text field where the object save door answers 422 · evidence: packages/mcp/src/mcp-http-tools.ts builds the hint from bridge.describeObject, and both bridges (packages/runtime/src/domains/mcp.ts describeObject, packages/mcp/src/stdio-data-bridge.ts describeObject) project fields to name/type/label/required, so options never reach validateExpression · Seam: spec:ExprSchemaHint.optionFields -> runtime:mcp-http-tools validate_expression · dedupe words: validate_expression optionFields, describeObject options, MCP label() parity",
"carrier: none · Studio live CEL lint (ui/packages/app-shell/src/views/metadata-admin/celAuthoring.ts lintCelPredicate) passes fields/scope only, so it cannot pre-warn label() on an optionless field; the save door's 422 still reaches Studio. Noted, not filed."
],
"round": 2,
"head": "c41843d6"
}
Unchanged: a field bound to a picklist and a field with inline options; unknown-field and unregistered-function refusals; a stored formula still loads and reads as before until its next active save.
Activity
objectstack-fleet commentedon Nov 1, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: 错的必须被响亮拒绝并给处方 | none | P②
Triage: p3 ·
bug·domain:server·area:formula· 2026-11-01T13:00Z⛔ Not a claim.
label()on a field that declares no options is admitted at the authoring door and reads null on every row. The door refuses unknown fields and functions, but not this. p3.Generated by Claude Code
objectstack-fleet commentedon Nov 1, 2026
domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo
Claim: PM loop round 4 · card thread · 2026-11-01T13:31Z
Session:
session_0186vfjHHMsRXC9EUT9hTFMo(the seat); the card thread issession_01FEkQ5pQB2Jwsce3RvocUcSAccount:
os-project-manager(the seat's linked user; the card's assignee)Branch:
claude/project-thread-m6mfzr(the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)Worktree: the card thread's own container checkout
Domain:
domain:serverSeat:
domain:server#2Ruling-ref: the maintainer, 2026-11-01T03:46Z: 「server 车道现在就是你负责,任务有一大堆啊,都可以派,包括 p3」, with 2026-10-26T10:29Z 「p1 p2 处理完了,就可以处理 p3」. Triage p3 (6558290991) stands; offered to this seat by seat 1.
File surface:
packages/lint/src/authoring-rules.ts(theexpression-invalidgate),packages/formula/src/read-only or minimal, tests; changesetsContainer & model:
M,mode:thread,model: opus(--tiernames no path mandate: PM's call, default),effort: mediumClause-②: no (narrowing) expected — a formula whose label() argument names an optionless field, admitted before, is refused
Metadata-protocol: none (expected; the dev measures and declares)
Responsibility: the dev extends the one existing authoring gate to refuse label() on an optionless field, with red-first, controls and ablation; the seat runs the contract review owed and lands
Thread-read: none
Premise: measured at
6e11a929by the QA runner; the dev re-measures onorigin/mainfirstSerial constraints cleared: seat 1's PR #28679 (#27904) touches
packages/formula/src/; seat 2's #28660 touchesstdlib.tsin another function; the dev mergesorigin/mainif either lands firstResume-from: none — fresh from
origin/mainState: →
pm:dispatched, assigned toos-project-manager, recorded by this claim.Generated by Claude Code
objectstack-fleet commentedon Nov 1, 2026
domain:server#2 · os-project-manager · session_01FEkQ5pQB2Jwsce3RvocUcS
os-dev-report
{
"issue": 28659,
"status": "done",
"branch": "claude/project-thread-m6mfzr",
"pr": "#28714",
"session": "session_01FEkQ5pQB2Jwsce3RvocUcS",
"premise_still_valid": true,
"summary": "Premise re-measured on origin/main: validateExpression('value','label(record.first)', ...) ok:true, and the live showcase door saved it 200 and read pf null with one WARN. The expression-invalid gate now refuses label(record.FIELD) on a declared field that declares no options: formula's validateExpression takes an opt-in ExprSchemaHint.optionFields and refuses under a new closed-set code label-field-no-options (params {field}) in the evaluator's own fault sentence (optionlessLabelSentence, shared with optionLabelOf). 'Declares options' has one spelling, declaresOptions (formula, exported), now read by objectql's label map and by lint's formula pass, which passes optionFields. No new rule id. File surface beyond the claim: the gate body is lint/validate-expressions.ts (authoring-rules.ts unchanged), plus objectql/formula-option-labels.ts for the one-predicate reuse and one docs sentence.",
"tests": "Head 0cd2057. formula test exit 0 (62 files / 1590 tests), lint test exit 0 (185 / 7730), objectql test exit 0 (512 / 9898); typecheck exit 0 for all three. New: formula validate-option-label-target.test.ts (11), lint runtime-gate.object-formula-label-target.test.ts (12, os validate/build/lint + runtime gate); expression-refusal-codes pin + sweep extended; #5017 receiver meta-test excuses two plumbing locals. Red on base (six src files at base, formula rebuilt, preflight marker absent): formula 7 failed/30 passed, lint 8 failed/4 passed; restored, git diff HEAD empty. Ablation via ablation-replace.mjs (anchor 1->0, blob e7327b51ea43->991a89c6da33, marker hit dist/index.js; DTS step exit 1 from lost narrowing, JS emitted): formula 5 failed/6 passed, lint 8 failed/4 passed; restored blob == HEAD, rebuilt, marker absent. An earlier ablation leg broke the formula build and was discarded. Live REST (showcase, scratch package): base PUT label(record.first) 200, compound 200, publish of a draft 200, data pf null; branch PUT 422 expression-invalid naming first for both bodies, draft _diagnostics (GET ?state=draft) carries the error, publish 422; controls label(record.kind) 200 reading 'Clean', label(record.nope) 422 unknown field unchanged.",
"gates": "dispatch-gates --commands: 89 derived, 89 run, 0 NOT-MEASURED, 0 UNRUN (--ran exit 0), all exit 0; check:type-check-debt first exit 3 (queued behind the dogfood lock holder), exit 0 on re-run. Record: /mnt/project-files/pm-orders/server/28659-gates-ran.txt. check-changeset-no-major local: no major (level axis is PR-scoped, read in CI). CI check-runs on #28714: in_progress at report time.",
"dogfood_battery": "0cd20573 · os-verify-lock VERDICT command-exit 0 (held 2142s, shared box) · pnpm --filter @objectstack/dogfood test exit 0 · Test Files 369 passed, 3 skipped (372) · Tests 2981 passed, 28 skipped (3009) · no failures",
"mcp_calls": "6 — issue_read x3, pull_request_read x2 (get #28679, get_files #28681), no write tools",
"api_writes": "2 — fleet-write relay pr_create (POST /repos/objectstack-ai/objectstack/dispatches -> run 41319006331 -> PR #28714, no assignees, no labels); this os-dev-report comment (POST /repos//issues/28659/comments). Plus git push x2 (not REST).",
"open_questions": [
{
"question": "Changeset level: the order asked for patch; AGENTS.md step 3 makes a door now refusing a value it stored 'narrowed', which takes minor (check-changeset-no-major refuses narrowed without a minor). Shipped formula+lint minor, objectql patch.",
"options": [
"A keep minor + Metadata-protocol: narrowed (as shipped)",
"B patch + none (the gate would refuse it, and it misdeclares the narrowing)"
],
"recommendation": "A, because the declaration must be the measured kind and the gate enforces the pairing."
}
],
"out_of_scope_findings": [
"class: b · reach: MCP validate_expression tool (site formula) answers ok for label(record.first) on a text field where the object save door answers 422 · evidence: packages/mcp/src/mcp-http-tools.ts builds the hint from bridge.describeObject, and both bridges (packages/runtime/src/domains/mcp.ts describeObject, packages/mcp/src/stdio-data-bridge.ts describeObject) project fields to name/type/label/required, so options never reach validateExpression · Seam: spec:ExprSchemaHint.optionFields -> runtime:mcp-http-tools validate_expression · dedupe words: validate_expression optionFields, describeObject options, MCP label() parity",
"carrier: none · Studio live CEL lint (ui/packages/app-shell/src/views/metadata-admin/celAuthoring.ts lintCelPredicate) passes fields/scope only, so it cannot pre-warn label() on an optionless field; the save door's 422 still reaches Studio. Noted, not filed."
]
}
Generated by Claude Code
objectstack-fleet commentedon Nov 1, 2026
domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo
2026-11-01T15:04Z · claim
6558630190amended (file surface)The card thread measured that the
expression-invalidgate's body for formula fields ispackages/lint/src/validate-expressions.ts, notauthoring-rules.ts. It also measured that the "declares options" reading the evaluator uses had to be shared rather than spelled twice.packages/lint/src/validate-expressions.ts(the gate);packages/objectql/src/formula-option-labels.ts(the shareddeclaresOptionspredicate, which the evaluator's option-label reader and the gate both use);packages/formula/src/for the reader's call;packages/lint/src/authoring-rules.tsis not touched.narrowed, so@objectstack/formulaand@objectstack/lintshipminorand@objectstack/objectqlpatch, as the no-major gate pairsnarrowedwithminor. That replaces the claim's expectedpatch.no (narrowing), as expected. The contract review is running before the queue.Generated by Claude Code
fix(lint): a picklist-bound field answers label() at the authoring ga…
objectstack-fleet commentedon Nov 1, 2026
domain:server#2 · os-project-manager · session_01FEkQ5pQB2Jwsce3RvocUcS
os-dev-report
{
"issue": 28659,
"status": "done",
"branch": "claude/project-thread-m6mfzr",
"pr": "#28714",
"session": "session_01FEkQ5pQB2Jwsce3RvocUcS",
"premise_still_valid": true,
"summary": "Review round 1 (contract review FAIL, comment 6559777722) fixed on head c41843d: lint's formula pass read the AUTHORED body through declaresOptions (served form), so it refused label(record.tier) on a select bound to a shared picklist with no inline options, which works at runtime once objectql resolves the list. Added declaresOptionsOnceServed (formula, exported beside declaresOptions): inline options OR typeof picklist === 'string' && picklist !== '' (R8's reading). Lint optionFieldNames now reads it; declaresOptions stays the served-form predicate objectql's label map reads, so the evaluator is unchanged for an unresolved picklist. Comments, changeset Unchanged bullet and Remedy, docs sentence ('a field that declares
optionsor names apicklist') and the PR body list restated; the changeset names every door that runs the active authoring gate (save door, per-item publish, history restore, draft _diagnostics). Round 0: Premise re-measured on origin/main: validateExpression('value','label(record.first)', ...) ok:true, and the live showcase door saved it 200 and read pf null with one WARN. The expression-invalid gate now refuses label(record.FIELD) on a declared field that declares no options: formula's validateExpression takes an opt-in ExprSchemaHint.optionFields and refuses under a new closed-set code label-field-no-options (params {field}) in the evaluator's own fault sentence (optionlessLabelSentence, shared with optionLabelOf). 'Declares options' has one spelling, declaresOptions (formula, exported), now read by objectql's label map and by lint's formula pass, which passes optionFields. No new rule id. File surface beyond the claim: the gate body is lint/validate-expressions.ts (authoring-rules.ts unchanged), plus objectql/formula-option-labels.ts for the one-predicate reuse and one docs sentence.","tests": "Head c41843d. formula test exit 0 (62 files / 1593 tests), lint test exit 0 (185 / 7732), objectql test exit 0 (513 / 9901); typecheck exit 0 for all three. Round 1 adds: picklist-bound control (tier: select, picklist 'qa_tiers', no inline options; label(record.tier) and label(record.tier) + ' / ' + label(record.kind)) at os validate/build/lint and runRuntimeAuthoringRules (runtime-gate test now 14); declaresOptionsOnceServed truth table, authored/served picklist pair and validator acceptance in validate-option-label-target.test.ts (now 14). Ablation of the picklist half (ablation-replace anchor 'fieldList.filter(([, f]) => declaresOptionsOnceServed(f))' -> declaresOptionsOnceServed({ ...f, picklist: undefined }), anchor 1->0): runtime-gate 2 failed / 12 passed, exactly the two picklist controls; restored blob 511b9f3952a6 == HEAD, git diff HEAD empty. No live REST probe for the picklist control (unit door test drives the same runRuntimeAuthoringRules). Round 0: Head 0cd2057. formula test exit 0 (62 files / 1590 tests), lint test exit 0 (185 / 7730), objectql test exit 0 (512 / 9898); typecheck exit 0 for all three. New: formula validate-option-label-target.test.ts (11), lint runtime-gate.object-formula-label-target.test.ts (12, os validate/build/lint + runtime gate); expression-refusal-codes pin + sweep extended; #5017 receiver meta-test excuses two plumbing locals. Red on base (six src files at base, formula rebuilt, preflight marker absent): formula 7 failed/30 passed, lint 8 failed/4 passed; restored, git diff HEAD empty. Ablation via ablation-replace.mjs (anchor 1->0, blob e7327b51ea43->991a89c6da33, marker hit dist/index.js; DTS step exit 1 from lost narrowing, JS emitted): formula 5 failed/6 passed, lint 8 failed/4 passed; restored blob == HEAD, rebuilt, marker absent. An earlier ablation leg broke the formula build and was discarded. Live REST (showcase, scratch package): base PUT label(record.first) 200, compound 200, publish of a draft 200, data pf null; branch PUT 422 expression-invalid naming first for both bodies, draft _diagnostics (GET ?state=draft) carries the error, publish 422; controls label(record.kind) 200 reading 'Clean', label(record.nope) 422 unknown field unchanged.",
"gates": "Head c41843d: dispatch-gates --commands 89 derived, 89 run, 0 NOT-MEASURED, 0 UNRUN (--ran exit 0), all exit 0; check:type-check-debt (lock contention), check:skill-examples and check:dual-build-cjs-loads (before full build) first answered non-zero without measuring, exit 0 on re-run. Record: /mnt/project-files/pm-orders/server/28659-gates-ran.txt. PR body v2 for the seat to apply: /mnt/project-files/pm-orders/server/28659-pr-body-v2.md.",
"dogfood_battery": "c41843d6 · os-verify-lock VERDICT command-exit 0 (held 2306s, shared box) · pnpm build exit 0 · pnpm --filter @objectstack/dogfood test exit 0 · Test Files 371 passed, 3 skipped (374) · Tests 2994 passed, 28 skipped (3022) · no failures",
"mcp_calls": "6 — issue_read x3, pull_request_read x2 (get #28679, get_files #28681), no write tools",
"api_writes": "Round 1: 1 — this os-dev-report comment (POST /repos//issues/28659/comments). Plus git push x1 (not REST). PR body not patched by the dev. Round 0: 2 — fleet-write relay pr_create (POST /repos/objectstack-ai/objectstack/dispatches -> run 41319006331 -> PR #28714, no assignees, no labels); this os-dev-report comment (POST /repos//issues/28659/comments). Plus git push x2 (not REST).",
"open_questions": [],
"out_of_scope_findings": [
"class: b · reach: MCP validate_expression tool (site formula) answers ok for label(record.first) on a text field where the object save door answers 422 · evidence: packages/mcp/src/mcp-http-tools.ts builds the hint from bridge.describeObject, and both bridges (packages/runtime/src/domains/mcp.ts describeObject, packages/mcp/src/stdio-data-bridge.ts describeObject) project fields to name/type/label/required, so options never reach validateExpression · Seam: spec:ExprSchemaHint.optionFields -> runtime:mcp-http-tools validate_expression · dedupe words: validate_expression optionFields, describeObject options, MCP label() parity",
"carrier: none · Studio live CEL lint (ui/packages/app-shell/src/views/metadata-admin/celAuthoring.ts lintCelPredicate) passes fields/scope only, so it cannot pre-warn label() on an optionless field; the save door's 422 still reaches Studio. Noted, not filed."
],
"round": 2,
"head": "c41843d6"
}
Generated by Claude Code
objectstack-fleet commentedon Nov 1, 2026
domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo
Landing record · #28659 · PR #28714
domain:serverseat 2 ·session_0186vfjHHMsRXC9EUT9hTFMo· 2026-11-01T17:29Z86fb6458git merge-base --is-ancestor 86fb6458 origin/maingit ls-remote origin refs/heads/gh-readonly-queue/*matching 28714git grep -c declaresOptionsOnceServed origin/main -- packages/lint/src/validate-expressions.tslabel()names a field that declares no options (no inlineoptions, nopicklist) is refused at authoring with the existingexpression-invalidrule and the evaluator's own sentence, atos validate/build/lint, the save door, per-item publish, history restore and draft diagnostics.picklistand a field with inlineoptions; unknown-field and unregistered-function refusals; a stored formula still loads and reads as before until its next active save.validate_expressioncannot see field options) was blocked on this PR and is now unblocked; this seat dispatches it.Release: claim
6558630190(amended6559703599; branchclaude/project-thread-m6mfzr) · landed.pm:dispatchedremoved; assignee cleared; the card closed on merge.Generated by Claude Code