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
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
Filing gate: ① a reproducible defect, class (b): a door that contradicts its own stated contract. It is filed from #22019's dev report (PR #22031, out_of_scope_findings[0]), by domain:engine seat 1 (seat post #6367, session_017ErfyP2Rx7XWHJA27QjyUi). ⛔ Not graded or routed here; ⛔ not a claim.
What is measured (by #22019's dev, at PR #22031's head fca16e0688)
Through the real saveMetaItem in publish mode, an object saved with success when it carried either of:
The sites the build judges and the door does not (the dev's H2)
validations[].condition and .when, with null guards over then / otherwise;
fields[].requiredWhen, readonlyWhen, conditionalRequired and visibleWhen (the root verdict, the parent gate, the requiredWhen null guard and the traversal refusal);
fields[].options[].visibleWhen;
actions[].visible and actions[].disabled.
Default values are not judged by the build either; that is no gap.
Contract
content/docs/data-modeling/formulas.mdx: "the same validateExpression validator backs os build and metadata registration".
The seam runs from spec:ObjectSchema.validations[].condition / FieldSchema.requiredWhen to runtime:runtimeAuthoringRulesFor('object') (packages/lint/src/runtime-gate.ts).
Each lift narrows the runtime save door's accept set, which is BREAKING (no (narrowing), minor), so it may want its own card per pass.
packages/lint is domain:spec by the anchoring rule.
Reader who acts
Triage grades and routes it. Serial: PR #22031 (#22019) introduces the fence.
Dedupe: MCP search_issues, repo-scoped: 「object save door validation rule condition requiredWhen os build verdict bare reference saves through meta object」 → #22019 (this card's parent) and other closed save-door cards on other types. None is this.
Dedupe words: object save door validation rule predicate os build verdict · requiredWhen bare reference saves through meta object · runtime gate object write fenced expression passes
Path: ② the capabilities an end user meets in the app — validation rules and field rules on a record | 缺项 | P2
Triage: first grade, bug · priority:p2 · domain:spec · area:records · pm:blocked behind #22019 (finding removed). This is #22019's sibling and the closing card for the object door's expression passes, with an enumeration pin; one PR per pass
Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-06T19:52Z. ⛔ Not a claim, ⛔ not a dispatch.
Triage: lands in packages/lint/src/validate-expressions.ts (the StackExpressionOptions fence that PR #22031 introduces) and packages/lint/src/runtime-gate.ts (runtimeAuthoringRulesFor('object'), :550 on main) ⇒ domain:spec; rationale: the anchoring rule gives packages/lint to domain:spec, and the card says so.
The premise holds on main:StackExpressionOptions (packages/lint/src/validate-expressions.ts:1178) has its runtimeWriteType docblock. It says that on an object write exactly one pass judges, the field-formula pass. Every other pass is "fenced off an object write, deliberately and by name". These are the four groups of the grade's order:
the validation-rule predicates;
the field-rule slots (requiredWhen / readonlyWhen / conditionalRequired / visibleWhen), with their parent and null-guard gates;
the per-option visibleWhen;
the object's own actions[] predicates.
The fence's own words ("a crossing of its own, measured over the stored corpus first, not a rider on this one") match the grade's shape: one PR per pass (Part of #22032), corpus measured first, Clause-②: no (narrowing), a minor changeset with a BREAKING line, and a contract review.
The grade 6024268378 stands: bug · priority:p2 · domain:spec · area:records. The Blocked-by: #22019 line is spent. The first claim takes pass 1.
Claim: PM loop round 4 (pass 1 of this card: the validation-rule predicates, validations[].condition / .when, per triage's order 6024268378 and unlock 6025250992; Part of #22032) · 2026-10-06T21:43Z
Session: session_01GV6oYwgc1kWiUCb1YaprQ7
Account: os-warren (the seat's linked user as GET /user answers it; the card's assignee from this act)
Branch: claude/issue-22032-object-door-validation-predicates
Worktree: objectstack-issue-22032
Domain: domain:spec
Seat: domain:spec#2 (seat post #18549)
File surface (at origin/main60ccda5a; stop on breach and explain in the report):
packages/lint/src/validate-expressions.ts: lift pass 1 (the validation-rule predicates, with their null guards over then / otherwise) through the StackExpressionOptions fence (:1178) for an object write. The other three passes stay fenced and pinned.
packages/lint/src/runtime-gate.ts (runtimeAuthoringRulesFor('object'), :550) and the authoring-rule registry entry, only as far as the lift needs.
Their tests in packages/lint/src/ (for example validate-expressions.test.ts), and the metadata-protocol save-door test that pins the object write, if the door pin lives there.
.changeset/22032-*.md (@objectstack/lint, and @objectstack/metadata-protocol if it moves: minor, with a BREAKING line).
{
"issue": 22032,
"status": "done",
"branch": "claude/issue-22032-object-door-action-visible",
"pr": "https://github.com/objectstack-ai/objectstack/pull/22233",
"session": "session_01LAi5BVvQNiYzepSAcsoFLK (mode:subagent; this is the PM's id, and my identity is the branch). This is round 3, under the rulings 6053857898 and 6054240056 on claim 6053340234. The newest Claim is still 6053340234 and names this branch; I read it again before pr_create (21 comments). I posted no claim and did not write the card's assignee. I did not touch #22211 or #22220.",
"premise_still_valid": true,
"summary": "Pass 4 of #22032 is implemented as draft PR #22233 (Part of #22032), at head 6e141edcad. In runStackExpressionPasses (packages/lint/src/validate-expressions.ts) the object action loop's guard 'for (const obj of objectWrite ? [] : objects)' is now 'for (const obj of objects)'. That one guard covers both actions[].visible and the non-boolean actions[].disabled. The loops over the stack's other collections keep their guards: flows, top-level actions, sharing rules and hooks. Every expression an object carries now gets the build's verdict at the object save door. The pins are: a new lint door suite (19 tests); the enumeration pin reshaped so that all 5 object-borne sites are lifted and every other collection stays off an object write; and a protocol door block (11 tests) with code and status on each refusal. The minor changeset carries the BREAKING section, the ruling's Clause-② line, and the per-kernel table for the packaged sys_approval_request, naming #22211 and #22220; its 'after' column was re-measured at the merged head. Rider 1 is done ('admin' became 'org_admin'). Rider 2 is done: the four pending object-door changesets no longer say that a lifted pass is not judged, and pass 3's supersede sentence is deleted. P1 held at both heads.",
"premises": {
"P1": "HOLDS on the dispatch head 7d7943dd0d (round 2, with and without the lift) and on the committed head 7ab81c8cf7 with the lift committed. No corpus file moved up to the merged head 6e141edcad: main's three commits touch no *.object.ts, no examples/** and no packages/lint. Corpus A has 118 objects in 18 groups: 16 of them carry 73 actions with 58 visible and 0 disabled predicates. Corpus B (defineStack-composed) has 33 objects in 5 groups: 5 of them carry 79 actions with 57 visible and 2 disabled. At the committed head the door refuses exactly the 8 sys_approval_request visible sites in A, at the raw and parsed shapes (0 parse failures), with 0 advisories, and 0 in B. The site lists and door errors are identical to the round-2 probe, with no new site. Non-vacuity: 117 of 117 mutated sites are flagged at build and door. Positive control: build 2 and door 2. The only save the lift newly refuses is the ruled case: the publish save of the packaged sys_approval_request, 403 to 422 on a host-config kernel, and 200 to 422 under OS_METADATA_WRITABLE=object. Re-measured at 6e141edcad: no hatch gives publish 422 INVALID_METADATA (8 issues) and draft 403 NOT_OVERRIDABLE; OS_METADATA_WRITABLE=object gives publish 422 (8) and draft 200 draft."
},
"tests": "Lint at 7ab81c8cf7: exec vitest run --maxWorkers=2, 'Test Files 126 passed (126) / Tests 5748 passed (5748)'. typecheck exit 0, with check:test-typecheck 'OK ... 2 file(s) / 6 error(s)' unchanged. --listFilesOnly: the new test file is in tsconfig.test.json's program. metadata-protocol at 7ab81c8cf7, after a lint rebuild: 'Test Files 221 passed | 3 skipped (224) / Tests 28271 passed | 19 skipped (28290)', VERDICT command-exit 0. At 6e141edcad, after the merge, a full build and check:generated: the protocol door file 114 of 114, metadata-protocol typecheck exit 0, and the 7 lint runtime-gate.object-* and stored-self files 116 of 116, VERDICT command-exit 0. Consumers at 7ab81c8cf7: rest 12 files and 412 tests (every meta-object-* file, meta-publish-package-scope, rest.test.ts); objectql 5 files and 218 tests (publish-package-drafts, save-meta and publish-meta response conformance, plugin.integration, engine); runtime meta-field-overlay-lock 16 tests; all passed. Reverse verification, one-off from committed HEAD 7ab81c8cf7, in a script with trap restore EXIT INT TERM on the absolute path. scripts/ablation-replace.mjs --hold matched a two-line anchor once (1 to 0) and put back 'const ablationFence22032p4 = objectWrite;' plus 'for (const obj of ablationFence22032p4 ? [] : objects) {'. The blob went 49b139316b0f to d42d50a1acbf. Lint was rebuilt, and ablation-dist-preflight found the marker in 4 built files. Predictions were written before the run. Lint pins (source): 11 failed and 17 passed, as predicted. Red: 6 LIT, PARITY, LOCATION, stored-self, and the 2 enumeration-pin tests that read the door. Green: registry, 8 CONTROL, differential, build-flags, 6 formula. Protocol file (dist-mediated): 6 failed and 108 passed, as predicted. Red: 3 (a) saves, promotion, package publish, (d). Green: 4 (b), (c), and every other block. Restore: the blob is 49b139316b0f, equal to HEAD; git diff HEAD is 0 lines; the whole tree has 0 changed paths. Lint was rebuilt, preflight --absent found the marker absent from all 14 built files, and the suites were back to 28 of 28 and 114 of 114. ESLint, narrowed to the 9 touched TS files at 6e141edcad (--no-inline-config --format json): 9 files, 0 errors and 0 warnings, none ignored. Each is matched by eslint.config.mjs (--print-config), and the config enables no type-aware linting (no parserOptions.project), so the diff cannot move an untouched file's verdict. Control-byte self-scan over the 14 changed files: 0 hits.",
"gates": "dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 6e141edcad (merge base 3b493184e) derived 63 commands, the same 63 as before the merge. Each was run with its exit code captured before any pipe: 62 exit 0, and 'node scripts/check-empty-changeset.mjs --base origin/main' exit 1. That one is rider 2, the gate's DELIBERATE CORRECTION class: four pending changesets this PR did not add were changed under ruling 6053857898 Q2-A. It stays red by design, the PR body says so and names the four files, and there is no skip-changeset. --ran gave '63 derived, 63 run, 0 NOT-MEASURED, 0 UNRUN', exit 0, with every family carrying an exit code. The PR-body gates ran with PR_NUMBER 22233, PR_HEAD_REF and the read-back body: check-partof-closing-keyword exit 0, check-closing-target-claim exit 0, check-single-claim-paths exit 0. check-changeset-no-major --base origin/main --event (a local payload of this body) exit 0: \"declares clause-② no (narrowing)\". @objectstack/spec check:generated after the merge: 'All 15 generated artifacts are up to date'. CI on 6e141edcad when read once: 31 check-runs, 10 success, 3 skipped, 17 in_progress, 1 failure. The failure is 'Check Changeset', at its step 'Reject an empty-frontmatter changeset added by this PR': the same expected rider-2 red. It is not one of the seven required contexts. CI was not awaited.",
"mcp_calls": "0",
"api_writes": "3 REST writes this round, each one fleet-write relay dispatch (POST /repos/objectstack-ai/objectstack/dispatches) executed as objectstack-fleet[bot]. (1) pr_create: POST /repos/objectstack-ai/objectstack/pulls gave #22233, draft. The relay read 16087 bytes sent and 16087 stored, identical, and a gh api read-back is byte-equal to the file, with one session-URL footer. (2) label-write --issue 22233 --assign os-litant: POST /repos/objectstack-ai/objectstack/issues/22233/assignees. The read-back matches (assignee os-litant); no label was written, and documentation, size/l, tests and tooling are the labelers'. (3) This os-dev-report comment: POST /repos/objectstack-ai/objectstack/issues/22032/comments. The git pushes of the branch (4 commits and the merge) are not REST writes. Every other GitHub call was a GET: the rulings, the card's comment page, PR #22233, its check-runs and one job's steps.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: the seat · noted, not filed. 'Check Changeset' (check-empty-changeset) is red on #22233 because rider 2 edits four pending changesets, as ruling 6053857898 Q2-A ordered. The PR body states it; the confirmation the gate asks for is the seat's.",
"carrier: 承接者:无 · noted, not filed. authoring-rules.ts keeps pass 2's and pass 3's dated measurement comments ('The per-option visibleWhen stays fenced', 'The object's own actions[] predicates stay fenced') as their passes wrote them. Pass 3 left pass 2's alike, and the new pass-4 block says no object-borne pass is fenced any more."
],
"corpus": "See premises.P1. The harness is a one-off tsx script outside the repo, deleted after this report. It ran at 7d7943dd0d (without and with an uncommitted lift) and at 7ab81c8cf7 (lift committed). For each object it read validateStackExpressions and runAuthoringRules('build') for the build, and runRuntimeAuthoringRules with type object and the object's own group as context for the door, at the raw and parsed shapes.",
"files_changed": [
".changeset/22032-object-save-door-action-predicates.md (+38/-0, new)",
".changeset/22019-object-save-door-formula-verdict.md (+1/-1, rider 2: one Unchanged line)",
".changeset/22032-object-save-door-validation-predicates.md (+1/-1, rider 2: one Unchanged line)",
".changeset/22032-object-save-door-field-rule-slots.md (+1/-1, rider 2: one Unchanged line)",
".changeset/22032-object-save-door-option-visible-when.md (+2/-2, rider 2: one Unchanged line and the supersede sentence deleted)",
"packages/lint/src/validate-expressions.ts (+37/-16; the source change is one guard, the rest is docblocks, comments and rider 1)",
"packages/lint/src/authoring-rules.ts (+22/-6, comments only, one of them the built .d.ts doc line)",
"packages/lint/src/runtime-gate.object-action-predicate-writes.test.ts (+196/-0, new)",
"packages/lint/src/runtime-gate.object-formula-writes.test.ts (+78/-47)",
"packages/lint/src/runtime-gate.object-field-rule-writes.test.ts (+4/-3, comment only)",
"packages/lint/src/runtime-gate.object-option-visibility-writes.test.ts (+4/-2, comment only)",
"packages/lint/src/runtime-gate.object-validation-writes.test.ts (+2/-2, comment only)",
"packages/lint/src/runtime-gate.object-writes.test.ts (+6/-5, comment only)",
"packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts (+212/-13)"
],
"line_counts": "14 files, +604/-99 (703 changed lines) against the merge base 3b493184e1, under the 5000-line human-merge threshold.",
"line_budget": "n/a: no skills/** file and no line-ratcheted ledger touched.",
"deviations": [
"PR line 2 and the changeset carry the ruling's parenthetical verbatim: 'Clause-②: no (narrowing: the object save door refuses an action visible or disabled predicate os build already refuses)'. The fleet reader (scripts/pm/clause2-line.mjs) reads it as declared, value no, arm narrowing, and check-changeset-no-major agrees.",
"Rider 2 wording: the four rewritten Unchanged lines no longer list conditionalRequired among the slots that joined the door, because pass 2's own entry says it is refused at the schema step. Nothing else in those files moved, except the deleted pass-3 supersede sentence.",
"Comment-only corrections outside the four named test surfaces, so that none still calls the action predicates fenced: the headers of four sibling lint tests, the roster comment in runtime-gate.object-writes.test.ts, and the #22019, pass-1, pass-2 and pass-3 docblocks in the protocol test. All are inside the claim's test files. Pass 3 accepted the same kind of change.",
"The enumeration pin's describe was renamed and reshaped (H2). FENCED_SITES is gone, and the 5 object-borne sites are pinned as lifted beside a second half that pins the 4 other collections off an object write. The flow-write equality pin is kept.",
"Corpus B (defineStack-composed example stacks) is kept beside passes 1 to 3's corpus A, as in rounds 1 and 2.",
"Main moved twice. 3b493184e1 (PR #22196, #22205, #22185) was merged by os-regen-merge.sh as merge commit 6e141edcad: clean, no generated path taken, then a full build and check:generated. #22185 shifted the protocol.ts line numbers but not the order, so the changeset's 'after' column and the PR's line references were re-measured at 6e141edcad. Main then gained 7b926f7600 (#22204, content/docs/getting-started only), which was not merged.",
"Beyond the ruling's asks: two dev-server boots at 6e141edcad to re-measure the table's 'after' values, and door consumer suites in rest, objectql and runtime. Each boot was torn down by the PID and the port I recorded.",
"The PR-scoped gates ran locally with GITHUB_TOKEN taken from gh auth token in the environment of those read-only scripts. It was never written to disk or to a log.",
"Commit trailers use AGENTS.md's model-free pair (Claude-Session plus Co-authored-by: Claude), and the PR footer uses its session-URL form. The harness reminder's model-named trailer and its footer form were not used.",
"The contract review was not attached: it is the seat's, at CONTRACT_REVIEW_TIER.",
"Cleanup: the scratch scripts were deleted, node_modules was removed, and the worktree was removed with git worktree remove, without --force, after this report was posted."
]
}
✅ ACCEPT on the diff: PR #22233 at 6e141edcad (pass 4). Landing held for the at-tier contract review and CI
domain:spec seat 1 (#6017) · os-litant · session session_01LAi5BVvQNiYzepSAcsoFLK · 2026-10-08T07:52Z. Holder of claim 6053340234, amended by 6053857898 and 6054240056; the review of record for the report 6055322272.
Checklist (read on GitHub and in the PR's own diff):
Form: draft, base main, assignee os-litant, head repo = base repo.
Line 1 is Part of #22032. The card stays open; this is its last pass.
Line 2 is the ruling's Clause-② line, verbatim.
Size and surface: 14 files, +604 / −99 (703 lines) against 3b493184e1. check-governed-merges: not governed. Every path is inside the claim as amended.
The one source change, read in the diff:for (const obj of objectWrite ? [] : objects) → for (const obj of objects) in validate-expressions.ts. That one guard covers both actions[].visible and the non-boolean disabled. Every other added or removed line in that file is a comment, a docblock, or rider 1 ('admin' → 'org_admin'). The other collections keep their guards: flows, top-level actions, sharing rules and hooks.
The rulings, checked:
P1 holds at the committed head: only the 8 sys_approval_request sites are refused, and the corpus is unchanged.
There is no edit to sys-approval-request.object.ts, no exemption, and no validator widening.
Rider 2, read by word-diff:
In the four pending object-door changesets (22019-…formula-verdict, 22032-…validation-predicates, …field-rule-slots, …option-visible-when), each "still not judged" line now says its crossing is another entry's. Pass 3's supersede sentence is deleted.
Compiled together, the four entries and this one no longer contradict one another. Nothing else in those files moved.
Pins and their reverse verification:
A new lint door suite (19 tests), and the enumeration pin reshaped: 5 object-borne sites lifted, and 4 other collections kept off an object write.
A protocol door block (11 tests) that carries the code and status of each refusal.
Ablating the lift back in turns 11 lint and 6 protocol tests red, as predicted. Restore was proved by blob equality.
Gates: 63 derived, 63 run, 0 NOT-MEASURED. One is red: check-empty-changeset, the Check Changeset job's step "Reject an empty-frontmatter changeset added by this PR".
That red is the DELIBERATE CORRECTION class.
pr-automation.yml:937–:947 names it RED BY DESIGN for a PR that corrects somebody else's pending note and adds its own.
The workflow runs on pull_request only, never merge_group.
It is recorded on the PR as this seat's comment, so it carries the three conditions for entering the queue red. The later steps (ADR-0087 disposition, the level axis) ran past it and passed.
Out of scope: the dated pass 2 and pass 3 comments in authoring-rules.ts, noted, with no carrier.
Review of record: ACCEPT 6055367205 and the at-tier contract review PASS 6055578769, both on 6e141edcad. Check Changeset was red by design on rider 2 (6055375818).
The stack's top-level actions, flows, sharing rules and hooks are judged at their own type's door.
The four pending object-door changesets no longer say that a lifted pass is not judged.
It ships as minor with its BREAKING section and the per-kernel table for the packaged sys_approval_request.
Triage's closing condition (6024268378): the four passes, one PR each, in order, each measured over the stored corpus first. All four have landed. The card is closed by this act.
Until #22211 lands, a publish save of that packaged object under the OS_METADATA_WRITABLE=object hatch is refused, as the changeset states.
Released by this landing:#22157 (pm:queue), the option visibleWhen that reads parent, which waited on this pass's file.
Acceptance note (no card):authoring-rules.ts keeps pass 2's and pass 3's dated "stays fenced" measurement comments beneath the pass-4 block that supersedes them. That is history, not a live claim.
Activity
objectstack-fleet commentedon Oct 6, 2026
Path: ② the capabilities an end user meets in the app — validation rules and field rules on a record | 缺项 | P2
Triage: first grade,
bug·priority:p2·domain:spec·area:records·pm:blockedbehind #22019 (findingremoved). This is #22019's sibling and the closing card for the object door's expression passes, with an enumeration pin; one PR per passBlocked-by: #22019
Triage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-06T19:52Z. ⛔ Not a claim, ⛔ not a dispatch.Triage: lands in
packages/lint/src/validate-expressions.ts(theStackExpressionOptionsfence that PR #22031 introduces) andpackages/lint/src/runtime-gate.ts(runtimeAuthoringRulesFor('object'),:550onmain) ⇒domain:spec; rationale: the anchoring rule givespackages/linttodomain:spec, and the card says so.Not a duplicate of formula: the metadata save door stores a formula that calls an unregistered function (
sqrt), and every read answers null with nothing logged — the docs say the shared validator backs metadata registration #22019. The mechanism is the same, but the sites are different, and formula: the metadata save door stores a formula that calls an unregistered function (sqrt), and every read answers null with nothing logged — the docs say the shared validator backs metadata registration #22019 is in flight (PR fix(lint,objectql)!: the object save door gives the build's formula verdict, and a formula fault is logged once per object and field #22031, draft). In-flight cards are never merged. PR fix(lint,objectql)!: the object save door gives the build's formula verdict, and a formula fault is logged once per object and field #22031 scopes itself to formula fields on purpose, and fences the rest by name, so this card is a foreseen follow-up.Why p2, like formula: the metadata save door stores a formula that calls an unregistered function (
sqrt), and every read answers null with nothing logged — the docs say the shared validator backs metadata registration #22019:os buildrefuses these expressions and the save door stores them.formulas.mdxpromises the same validator at both doors. A validation condition orrequiredWhenthe evaluator cannot answer reaches users as a rule that silently does not hold.Not a new gate: each lift gives the build's existing verdict at the second door, the same reading as formula: the metadata save door stores a formula that calls an unregistered function (
sqrt), and every read answers null with nothing logged — the docs say the shared validator backs metadata registration #22019's grade6021332805. ⛔ No new rule, and no new refusal code.Shape: the closing card, with an enumeration pin. The pin is the fenced passes in
StackExpressionOptionsonce PR fix(lint,objectql)!: the object save door gives the build's formula verdict, and a formula fault is logged once per object and field #22031 has landed. Each pass is lifted in its own PR (Part of #22032), in this order:validations[].condition/.when;fields[].requiredWhen/readonlyWhen/conditionalRequired/visibleWhen;fields[].options[].visibleWhen;actions[].visible/.disabled.Each PR:
sqrt), and every read answers null with nothing logged — the docs say the shared validator backs metadata registration #22019's dev did: 0 refusals over 29 fields);Clause-②: no (narrowing), aminorchangeset with a BREAKING line, and a contract review;Stop and report if a pass's corpus measurement finds stored metadata the validator refuses. That is a migration question.
Why blocked: the fence this card lifts does not exist on
mainuntil PR fix(lint,objectql)!: the object save door gives the build's formula verdict, and a formula fault is logged once per object and field #22031 lands.objectstack-fleet commentedon Oct 6, 2026
Triage: unlocked,
pm:blocked→pm:queue. #22019 landed the fence this card lifts, and the fence names the four passes this card's pin enumeratesTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-06T20:53Z. ⛔ Not a claim, ⛔ not a dispatch.The blocker is released (read at this write):
f85a83b83b, an ancestor oforigin/main.sqrt), and every read answers null with nothing logged — the docs say the shared validator backs metadata registration #22019 closedcompleted.The premise holds on
main:StackExpressionOptions(packages/lint/src/validate-expressions.ts:1178) has itsruntimeWriteTypedocblock. It says that on anobjectwrite exactly one pass judges, the field-formula pass. Every other pass is "fenced off an object write, deliberately and by name". These are the four groups of the grade's order:requiredWhen/readonlyWhen/conditionalRequired/visibleWhen), with theirparentand null-guard gates;visibleWhen;actions[]predicates.The fence's own words ("a crossing of its own, measured over the stored corpus first, not a rider on this one") match the grade's shape: one PR per pass (
Part of #22032), corpus measured first,Clause-②: no (narrowing), aminorchangeset with a BREAKING line, and a contract review.The grade
6024268378stands:bug·priority:p2·domain:spec·area:records. TheBlocked-by: #22019line is spent. The first claim takes pass 1.objectstack-fleet commentedon Oct 6, 2026
Claim: PM loop round 4 (pass 1 of this card: the validation-rule predicates,
validations[].condition/.when, per triage's order6024268378and unlock6025250992;Part of #22032) · 2026-10-06T21:43ZSession:
session_01GV6oYwgc1kWiUCb1YaprQ7Account:
os-warren(the seat's linked user asGET /useranswers it; the card's assignee from this act)Branch:
claude/issue-22032-object-door-validation-predicatesWorktree:
objectstack-issue-22032Domain:
domain:specSeat:
domain:spec#2(seat post #18549)File surface (at
origin/main60ccda5a; stop on breach and explain in the report):packages/lint/src/validate-expressions.ts: lift pass 1 (the validation-rule predicates, with their null guards overthen/otherwise) through theStackExpressionOptionsfence (:1178) for anobjectwrite. The other three passes stay fenced and pinned.packages/lint/src/runtime-gate.ts(runtimeAuthoringRulesFor('object'),:550) and the authoring-rule registry entry, only as far as the lift needs.packages/lint/src/(for examplevalidate-expressions.test.ts), and the metadata-protocol save-door test that pins the object write, if the door pin lives there..changeset/22032-*.md(@objectstack/lint, and@objectstack/metadata-protocolif it moves:minor, with a BREAKING line).examples/**) with the build's own pass-1 verdict. If any stored metadata is refused, stop and report: that is a migration question.Container & model:
M,mode:subagent,model: opus(--tier: no path-derived mandate; a contract review is owed by triage's grade for each pass, from an isolated subagent atCONTRACT_REVIEW_TIER).Clause-②: no (narrowing)
Thread-read: 6025250992
Serial constraints cleared: none of the 8 open PRs touches
packages/lint/src/validate-expressions.tsorruntime-gate.ts, and no liveClaim:names them (scan at this stamp). PR feat(spec)!: the build doors refuse a builtin node config value its executor contract refuses, with its location #21974 (service-automation: a built-in node's config value its own contract refuses still registers, then fails every run — the built-in half of #21848's class #21898) touchesvalidate-expressions.test.ts. It ispm:blockedbehind [gate] the v18 development line is not open — ADR-0131 execution cards are blocked on this card #15193 (the 18.0 opening), so whichever lands later mergesmain.Landing question, held for an answer before this pass enqueues: ruling
6020116360(decision: before the v18 line opens, does main publish one last 17.x release to npm? #22009) makes the last 17.x release, cut frommain, "fixes only", and says "a narrowing breaking change does not go into the closing release … it lands after the opening, in 18.0" (that is why service-automation: a built-in node's config value its own contract refuses still registers, then fails every run — the built-in half of #21848's class #21898 was re-blocked,6022498227). This pass is afix(lint)!narrowing of the object save door. formula: the metadata save door stores a formula that calls an unregistered function (sqrt), and every read answers null with nothing logged — the docs say the shared validator backs metadata registration #22019's PR fix(lint,objectql)!: the object save door gives the build's formula verdict, and a formula fault is logged once per object and field #22031, the same door's formula pass and alsofix(lint)!, merged after the ruling. The PR is built now; whether it lands before the last 17.x release or after the opening is asked of triage and the maintainer, and the seat does not enqueue it until that is answered.Generated by Claude Code
36 remaining items
fix(lint)!: the object save door gives the build's object action pred…
test(lint, metadata-protocol): pin the object action predicate verdic…
chore(changeset): pass 4 of #22032, and the object-door entries made …
objectstack-fleet commentedon Oct 8, 2026
os-dev-report
{ "issue": 22032, "status": "done", "branch": "claude/issue-22032-object-door-action-visible", "pr": "https://github.com/objectstack-ai/objectstack/pull/22233", "session": "session_01LAi5BVvQNiYzepSAcsoFLK (mode:subagent; this is the PM's id, and my identity is the branch). This is round 3, under the rulings 6053857898 and 6054240056 on claim 6053340234. The newest Claim is still 6053340234 and names this branch; I read it again before pr_create (21 comments). I posted no claim and did not write the card's assignee. I did not touch #22211 or #22220.", "premise_still_valid": true, "summary": "Pass 4 of #22032 is implemented as draft PR #22233 (Part of #22032), at head 6e141edcad. In runStackExpressionPasses (packages/lint/src/validate-expressions.ts) the object action loop's guard 'for (const obj of objectWrite ? [] : objects)' is now 'for (const obj of objects)'. That one guard covers both actions[].visible and the non-boolean actions[].disabled. The loops over the stack's other collections keep their guards: flows, top-level actions, sharing rules and hooks. Every expression an object carries now gets the build's verdict at the object save door. The pins are: a new lint door suite (19 tests); the enumeration pin reshaped so that all 5 object-borne sites are lifted and every other collection stays off an object write; and a protocol door block (11 tests) with code and status on each refusal. The minor changeset carries the BREAKING section, the ruling's Clause-② line, and the per-kernel table for the packaged sys_approval_request, naming #22211 and #22220; its 'after' column was re-measured at the merged head. Rider 1 is done ('admin' became 'org_admin'). Rider 2 is done: the four pending object-door changesets no longer say that a lifted pass is not judged, and pass 3's supersede sentence is deleted. P1 held at both heads.", "premises": { "P1": "HOLDS on the dispatch head 7d7943dd0d (round 2, with and without the lift) and on the committed head 7ab81c8cf7 with the lift committed. No corpus file moved up to the merged head 6e141edcad: main's three commits touch no *.object.ts, no examples/** and no packages/lint. Corpus A has 118 objects in 18 groups: 16 of them carry 73 actions with 58 visible and 0 disabled predicates. Corpus B (defineStack-composed) has 33 objects in 5 groups: 5 of them carry 79 actions with 57 visible and 2 disabled. At the committed head the door refuses exactly the 8 sys_approval_request visible sites in A, at the raw and parsed shapes (0 parse failures), with 0 advisories, and 0 in B. The site lists and door errors are identical to the round-2 probe, with no new site. Non-vacuity: 117 of 117 mutated sites are flagged at build and door. Positive control: build 2 and door 2. The only save the lift newly refuses is the ruled case: the publish save of the packaged sys_approval_request, 403 to 422 on a host-config kernel, and 200 to 422 under OS_METADATA_WRITABLE=object. Re-measured at 6e141edcad: no hatch gives publish 422 INVALID_METADATA (8 issues) and draft 403 NOT_OVERRIDABLE; OS_METADATA_WRITABLE=object gives publish 422 (8) and draft 200 draft." }, "tests": "Lint at 7ab81c8cf7: exec vitest run --maxWorkers=2, 'Test Files 126 passed (126) / Tests 5748 passed (5748)'. typecheck exit 0, with check:test-typecheck 'OK ... 2 file(s) / 6 error(s)' unchanged. --listFilesOnly: the new test file is in tsconfig.test.json's program. metadata-protocol at 7ab81c8cf7, after a lint rebuild: 'Test Files 221 passed | 3 skipped (224) / Tests 28271 passed | 19 skipped (28290)', VERDICT command-exit 0. At 6e141edcad, after the merge, a full build and check:generated: the protocol door file 114 of 114, metadata-protocol typecheck exit 0, and the 7 lint runtime-gate.object-* and stored-self files 116 of 116, VERDICT command-exit 0. Consumers at 7ab81c8cf7: rest 12 files and 412 tests (every meta-object-* file, meta-publish-package-scope, rest.test.ts); objectql 5 files and 218 tests (publish-package-drafts, save-meta and publish-meta response conformance, plugin.integration, engine); runtime meta-field-overlay-lock 16 tests; all passed. Reverse verification, one-off from committed HEAD 7ab81c8cf7, in a script with trap restore EXIT INT TERM on the absolute path. scripts/ablation-replace.mjs --hold matched a two-line anchor once (1 to 0) and put back 'const ablationFence22032p4 = objectWrite;' plus 'for (const obj of ablationFence22032p4 ? [] : objects) {'. The blob went 49b139316b0f to d42d50a1acbf. Lint was rebuilt, and ablation-dist-preflight found the marker in 4 built files. Predictions were written before the run. Lint pins (source): 11 failed and 17 passed, as predicted. Red: 6 LIT, PARITY, LOCATION, stored-self, and the 2 enumeration-pin tests that read the door. Green: registry, 8 CONTROL, differential, build-flags, 6 formula. Protocol file (dist-mediated): 6 failed and 108 passed, as predicted. Red: 3 (a) saves, promotion, package publish, (d). Green: 4 (b), (c), and every other block. Restore: the blob is 49b139316b0f, equal to HEAD; git diff HEAD is 0 lines; the whole tree has 0 changed paths. Lint was rebuilt, preflight --absent found the marker absent from all 14 built files, and the suites were back to 28 of 28 and 114 of 114. ESLint, narrowed to the 9 touched TS files at 6e141edcad (--no-inline-config --format json): 9 files, 0 errors and 0 warnings, none ignored. Each is matched by eslint.config.mjs (--print-config), and the config enables no type-aware linting (no parserOptions.project), so the diff cannot move an untouched file's verdict. Control-byte self-scan over the 14 changed files: 0 hits.", "gates": "dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 6e141edcad (merge base 3b493184e) derived 63 commands, the same 63 as before the merge. Each was run with its exit code captured before any pipe: 62 exit 0, and 'node scripts/check-empty-changeset.mjs --base origin/main' exit 1. That one is rider 2, the gate's DELIBERATE CORRECTION class: four pending changesets this PR did not add were changed under ruling 6053857898 Q2-A. It stays red by design, the PR body says so and names the four files, and there is no skip-changeset. --ran gave '63 derived, 63 run, 0 NOT-MEASURED, 0 UNRUN', exit 0, with every family carrying an exit code. The PR-body gates ran with PR_NUMBER 22233, PR_HEAD_REF and the read-back body: check-partof-closing-keyword exit 0, check-closing-target-claim exit 0, check-single-claim-paths exit 0. check-changeset-no-major --base origin/main --event (a local payload of this body) exit 0: \"declares clause-② no (narrowing)\". @objectstack/spec check:generated after the merge: 'All 15 generated artifacts are up to date'. CI on 6e141edcad when read once: 31 check-runs, 10 success, 3 skipped, 17 in_progress, 1 failure. The failure is 'Check Changeset', at its step 'Reject an empty-frontmatter changeset added by this PR': the same expected rider-2 red. It is not one of the seven required contexts. CI was not awaited.", "mcp_calls": "0", "api_writes": "3 REST writes this round, each one fleet-write relay dispatch (POST /repos/objectstack-ai/objectstack/dispatches) executed as objectstack-fleet[bot]. (1) pr_create: POST /repos/objectstack-ai/objectstack/pulls gave #22233, draft. The relay read 16087 bytes sent and 16087 stored, identical, and a gh api read-back is byte-equal to the file, with one session-URL footer. (2) label-write --issue 22233 --assign os-litant: POST /repos/objectstack-ai/objectstack/issues/22233/assignees. The read-back matches (assignee os-litant); no label was written, and documentation, size/l, tests and tooling are the labelers'. (3) This os-dev-report comment: POST /repos/objectstack-ai/objectstack/issues/22032/comments. The git pushes of the branch (4 commits and the merge) are not REST writes. Every other GitHub call was a GET: the rulings, the card's comment page, PR #22233, its check-runs and one job's steps.", "open_questions": [], "out_of_scope_findings": [ "carrier: the seat · noted, not filed. 'Check Changeset' (check-empty-changeset) is red on #22233 because rider 2 edits four pending changesets, as ruling 6053857898 Q2-A ordered. The PR body states it; the confirmation the gate asks for is the seat's.", "carrier: 承接者:无 · noted, not filed. authoring-rules.ts keeps pass 2's and pass 3's dated measurement comments ('The per-option visibleWhen stays fenced', 'The object's own actions[] predicates stay fenced') as their passes wrote them. Pass 3 left pass 2's alike, and the new pass-4 block says no object-borne pass is fenced any more." ], "corpus": "See premises.P1. The harness is a one-off tsx script outside the repo, deleted after this report. It ran at 7d7943dd0d (without and with an uncommitted lift) and at 7ab81c8cf7 (lift committed). For each object it read validateStackExpressions and runAuthoringRules('build') for the build, and runRuntimeAuthoringRules with type object and the object's own group as context for the door, at the raw and parsed shapes.", "files_changed": [ ".changeset/22032-object-save-door-action-predicates.md (+38/-0, new)", ".changeset/22019-object-save-door-formula-verdict.md (+1/-1, rider 2: one Unchanged line)", ".changeset/22032-object-save-door-validation-predicates.md (+1/-1, rider 2: one Unchanged line)", ".changeset/22032-object-save-door-field-rule-slots.md (+1/-1, rider 2: one Unchanged line)", ".changeset/22032-object-save-door-option-visible-when.md (+2/-2, rider 2: one Unchanged line and the supersede sentence deleted)", "packages/lint/src/validate-expressions.ts (+37/-16; the source change is one guard, the rest is docblocks, comments and rider 1)", "packages/lint/src/authoring-rules.ts (+22/-6, comments only, one of them the built .d.ts doc line)", "packages/lint/src/runtime-gate.object-action-predicate-writes.test.ts (+196/-0, new)", "packages/lint/src/runtime-gate.object-formula-writes.test.ts (+78/-47)", "packages/lint/src/runtime-gate.object-field-rule-writes.test.ts (+4/-3, comment only)", "packages/lint/src/runtime-gate.object-option-visibility-writes.test.ts (+4/-2, comment only)", "packages/lint/src/runtime-gate.object-validation-writes.test.ts (+2/-2, comment only)", "packages/lint/src/runtime-gate.object-writes.test.ts (+6/-5, comment only)", "packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts (+212/-13)" ], "line_counts": "14 files, +604/-99 (703 changed lines) against the merge base 3b493184e1, under the 5000-line human-merge threshold.", "line_budget": "n/a: no skills/** file and no line-ratcheted ledger touched.", "deviations": [ "PR line 2 and the changeset carry the ruling's parenthetical verbatim: 'Clause-②: no (narrowing: the object save door refuses an action visible or disabled predicate os build already refuses)'. The fleet reader (scripts/pm/clause2-line.mjs) reads it as declared, value no, arm narrowing, and check-changeset-no-major agrees.", "Rider 2 wording: the four rewritten Unchanged lines no longer list conditionalRequired among the slots that joined the door, because pass 2's own entry says it is refused at the schema step. Nothing else in those files moved, except the deleted pass-3 supersede sentence.", "Comment-only corrections outside the four named test surfaces, so that none still calls the action predicates fenced: the headers of four sibling lint tests, the roster comment in runtime-gate.object-writes.test.ts, and the #22019, pass-1, pass-2 and pass-3 docblocks in the protocol test. All are inside the claim's test files. Pass 3 accepted the same kind of change.", "The enumeration pin's describe was renamed and reshaped (H2). FENCED_SITES is gone, and the 5 object-borne sites are pinned as lifted beside a second half that pins the 4 other collections off an object write. The flow-write equality pin is kept.", "Corpus B (defineStack-composed example stacks) is kept beside passes 1 to 3's corpus A, as in rounds 1 and 2.", "Main moved twice. 3b493184e1 (PR #22196, #22205, #22185) was merged by os-regen-merge.sh as merge commit 6e141edcad: clean, no generated path taken, then a full build and check:generated. #22185 shifted the protocol.ts line numbers but not the order, so the changeset's 'after' column and the PR's line references were re-measured at 6e141edcad. Main then gained 7b926f7600 (#22204, content/docs/getting-started only), which was not merged.", "Beyond the ruling's asks: two dev-server boots at 6e141edcad to re-measure the table's 'after' values, and door consumer suites in rest, objectql and runtime. Each boot was torn down by the PID and the port I recorded.", "The PR-scoped gates ran locally with GITHUB_TOKEN taken from gh auth token in the environment of those read-only scripts. It was never written to disk or to a log.", "Commit trailers use AGENTS.md's model-free pair (Claude-Session plus Co-authored-by: Claude), and the PR footer uses its session-URL form. The harness reminder's model-named trailer and its footer form were not used.", "The contract review was not attached: it is the seat's, at CONTRACT_REVIEW_TIER.", "Cleanup: the scratch scripts were deleted, node_modules was removed, and the worktree was removed with git worktree remove, without --force, after this report was posted." ] }Generated by Claude Code
objectstack-fleet commentedon Oct 8, 2026
✅ ACCEPT on the diff: PR #22233 at
6e141edcad(pass 4). Landing held for the at-tier contract review and CIdomain:specseat 1 (#6017) ·os-litant· sessionsession_01LAi5BVvQNiYzepSAcsoFLK· 2026-10-08T07:52Z. Holder of claim6053340234, amended by6053857898and6054240056; the review of record for the report6055322272.Checklist (read on GitHub and in the PR's own diff):
main, assigneeos-litant, head repo = base repo.Part of #22032. The card stays open; this is its last pass.Clause-②line, verbatim.3b493184e1.check-governed-merges: not governed. Every path is inside the claim as amended.for (const obj of objectWrite ? [] : objects)→for (const obj of objects)invalidate-expressions.ts. That one guard covers bothactions[].visibleand the non-booleandisabled. Every other added or removed line in that file is a comment, a docblock, or rider 1 ('admin'→'org_admin'). The other collections keep their guards: flows, top-level actions, sharing rules and hooks.sys_approval_requestsites are refused, and the corpus is unchanged.6054240056, with its "after" column re-measured at6e141edcad, and it names plugin-approvals: sys_approval_request's 8 actionvisiblepredicates readrecord.viewer, a block the service attaches on read, and the shared expression validator refuses all 8 as an undeclared field #22211 and metadata-protocol: on a host-config kernel the object save door runs the authoring gate before the package door, so a packaged object's publish save can answer 422 INVALID_METADATA where an environment kernel answers 403 NOT_OVERRIDABLE (#8184's sibling) #22220.sys-approval-request.object.ts, no exemption, and no validator widening.22019-…formula-verdict,22032-…validation-predicates,…field-rule-slots,…option-visible-when), each "still not judged" line now says its crossing is another entry's. Pass 3's supersede sentence is deleted.check-empty-changeset, theCheck Changesetjob's step "Reject an empty-frontmatter changeset added by this PR".pr-automation.yml:937–:947names it RED BY DESIGN for a PR that corrects somebody else's pending note and adds its own.pull_requestonly, nevermerge_group.Out of scope: the dated pass 2 and pass 3 comments in
authoring-rules.ts, noted, with no carrier.objectstack-fleet commentedon Oct 8, 2026
Landed: pass 4, PR #22233 →
73a0a6bf1d. The closing pass: this card closescompleteddomain:specseat 1 (#6017) ·os-litant· sessionsession_01LAi5BVvQNiYzepSAcsoFLK· 2026-10-08T09:13Z · holder of claim6053340234(amended6053857898,6054240056).73a0a6bf1d, which has one parent (b9e6e5c132) and is an ancestor oforigin/main.6e141edcad.packages/lint/src/validate-expressions.ts, which PR fix(spec): the retirement sentence names--write: it applies the edits it can prove, you apply the rest #22142 (feat(cli):os migrate meta --write— the AST codemod that rewrites authored sources for the mechanicalappliedset (v18) #9591, one diagnostic sentence) changed onmainfirst. This PR's own changed lines on73a0a6bf1dare identical to its net diff.6055367205and the at-tier contract review PASS6055578769, both on6e141edcad.Check Changesetwas red by design on rider 2 (6055375818).os build's verdict at the object save door: formula fields (formula: the metadata save door stores a formula that calls an unregistered function (sqrt), and every read answers null with nothing logged — the docs say the shared validator backs metadata registration #22019), validation-rule predicates (pass 1), the field-rule slots (pass 2), optionvisibleWhen(pass 3) and, with this pass, the object's ownactions[].visibleand non-booleandisabled.minorwith its BREAKING section and the per-kernel table for the packagedsys_approval_request.6024268378): the four passes, one PR each, in order, each measured over the stored corpus first. All four have landed. The card is closed by this act.Carried elsewhere, by ruling:
visiblepredicates readrecord.viewer, a block the service attaches on read, and the shared expression validator refuses all 8 as an undeclared field #22211:sys_approval_request's 8record.viewerpredicates, the producer.Until #22211 lands, a publish save of that packaged object under the
OS_METADATA_WRITABLE=objecthatch is refused, as the changeset states.Released by this landing: #22157 (
pm:queue), the optionvisibleWhenthat readsparent, which waited on this pass's file.Acceptance note (no card):
authoring-rules.tskeeps pass 2's and pass 3's dated "stays fenced" measurement comments beneath the pass-4 block that supersedes them. That is history, not a live claim.fix(lint)!: the object save door gives the build's validation-rule ve…
fix(lint)!: the object save door gives the build's field-rule-slot ve…
fix(lint)!: a conditional validation rule's nested then / otherwise p…
fix(lint)!: the object save door gives the build's option visibleWhen…
docs(qa): a repeatable Studio task benchmark — repair-ticket scenario…
fix(lint)!: the object save door gives the build's object action pred…
fix(lint)!: os build and the object save door refuse a select option'…