Repository navigation
fix(lint)!: the object save door gives the build's option visibleWhen verdict (#22032 pass 3) - #22151
Conversation
… verdict Lift the object-write guard on the per-option visibleWhen loop in runStackExpressionPasses, so an object write runs the build's option pass (check on the record scope plus the reference-traversal refusal) at the build's own position. The object's own action predicates stay fenced. Claude-Session: https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848 Co-authored-by: Claude <noreply@anthropic.com>
…e option site from fenced to lifted Claude-Session: https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848 Co-authored-by: Claude <noreply@anthropic.com>
…n verdict Claude-Session: https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848 Co-authored-by: Claude <noreply@anthropic.com>
…minor, BREAKING) Claude-Session: https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848 Co-authored-by: Claude <noreply@anthropic.com>
…ject-door-option-visiblewhen
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 4 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin eb858270220c09f71a2653cb0e5e9cf835535a61 && git checkout eb858270220c09f71a2653cb0e5e9cf835535a61
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 7ef50a4fbbf9c819a550b2943d2ac74a7bd39770 3c1d3262eea55c9be9985377ef0921cf295badd9 && git checkout -B drift-repro 7ef50a4fbbf9c819a550b2943d2ac74a7bd39770 && git merge --no-ff 3c1d3262eea55c9be9985377ef0921cf295badd9
node scripts/docs-audit/affected-docs.mjs --json 7ef50a4fbbf9c819a550b2943d2ac74a7bd39770
|
Contract reviewServed-tier: PR #22151 (card #22032, pass 3: ① Derived judgmentsThe source change is one expression: the per-option loop in
② Semver level
③ Boundary flags
Nothing here reaches the verdict. Landing stays with the owning seat, on green checks. Implemented-by: VERDICT: PASS Generated by Claude Code |
…ject-door-option-visiblewhen # Conflicts: # packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts
Held: back to draft, auto-merge off,
|
Contract reviewServed-tier: PR #22151 (card #22032, pass 3: ① Derived judgments
② Semver level
③ Boundary flags
Merge-round report:
Pass-3 report, restated on this head:
Out of scope and acceptance notes:
Triage's per-pass requirements ( Implemented-by: VERDICT: PASS |
Part of #22032
Clause-②: no (narrowing)
This is pass 3 of #22032: a field option's
visibleWhen. Pass 4 (the object's own action predicates) stays fenced, and the card stays open for it.What changes
The object save door gives the build's verdict on a field option's
visibleWhen.formulas.mdxsays "the samevalidateExpressionvalidator backsos buildand metadata registration". After pass 2 the object door ran the whole field walk except the per-option loop, which kept its own guard. So an object whose option carriedvisibleWhen: 'amount > 1'(a bare field reference) still saved with a 200, whileos buildrefused it at error. The server's option check cannot evaluate such a predicate and fails open (logged, allowed through), so the gate it declares is never enforced (read fromevaluateOptionVisibilityinpackages/objectql/src/validation/rule-validator.ts).origin/main8fc50b7647the loop readfor (const [oi, opt] of (objectWrite ? [] : recordsOf(f.options)).entries())(validate-expressions.ts:2072), under a[#22032] FENCED on an object write (pass 3 …)comment. It now readsrecordsOf(f.options). On an object write the loop runs at the build's own position in the field walk, so the door gets both of the option's checks:check(optionWhere, opt.visibleWhen, objectName, 'record');refuseFieldTraversal(optionWhere, 'option visibleWhen', …), the refusal of a read through a reference field onrecordorprevious.current_userkeeps the build's two verdicts (H2). An option's evaluator binds the acting user (ADR-0068 D1), so the build acceptscurrent_useron an option and refuses it on the field-rule slots one level up. The door now gives both verdicts as the build does. The showcase's role gate,'org_admin' in current_user.positions, still saves on an option, and the same text on the field's ownvisibleWhenis refused at both doors. Both are pinned.validateStackExpressionsentry declaresruntimeTypes: ['flow', 'action', 'hook', 'object'](authoring-rules.ts), andruntimeAuthoringRulesFor('object')(runtime-gate.ts) dispatches it.runtime-gate.tsis untouched.StackExpressionOptions.runtimeWriteTypenow names four admitted passes and one fenced pass.AuthoringRuleContext.runtimeWriteTypeinauthoring-rules.ts, the one line that reaches a built.d.ts, names the per-option pass. The function-head comment and the field walk's two comments move with it. Inauthoring-rules.tsthe registry entry gains a[#22032, pass 3]measurement comment, as passes 1 and 2 added theirs. Comments in four sibling test files are corrected so that none of them still says optionvisibleWhenis fenced.runAuthoringRules('build', …)give the same rule (expression-invalid), location (object 'fx_option' · field 'province' option 'zj' visibleWhen), path, message and hint. The pins compare these key by key.packages/metadata-protocol. Only its test file gains the door-level pins.Pins
packages/lint/src/runtime-gate.object-option-visibility-writes.test.ts(new, 14 tests).amount > 1, an unregisteredsqrt(record.amount) > 1, an unknown fieldrecord.amont > 1, a syntax errorrecord.country ==, and the traversal refusal throughrecord.accountand throughprevious.account. Each is located at the option and asserted on its named subject.record.countrycascade, the showcase'scurrent_user.positionsrole gate, a grant check plus role gate (current_user.can(…) && …), and a reference compared as a value (record.account != null). Each is clean at the door and at the build.current_useron the option and on the field's ownvisibleWhenin one body. The build and the door both give exactly one finding, at the field slot.packages/lint/src/runtime-gate.object-formula-writes.test.ts. The fenced site is now pass 4's alone (an actionvisible). The optionvisibleWhensite moves to the lifted sites, beside the validation rule and therequiredWhen. The build flags all four sites. The object door flags the three lifted sites in the build's order, andrunStackExpressionPasseson an object write returns exactly the build's findings for the admitted passes.packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts, through the realsaveMetaItem,publishMetaItemandpublishPackageDrafts.INVALID_METADATAcarrying the build's located finding, and nothing lands.outcome: 'refused',failednaming the object withINVALID_METADATA, the row left a draft). The draft saves themselves still succeed.current_userrole gate still save, and the row lands active. The role gate rides every refused body too, which each yield exactly one finding.os buildgive the same finding on rule, where, path, message and hint.Reverse verification (one-off, from committed HEAD
23ce6c494c)scripts/ablation-replace.mjs(wrap mode) put the guard back on the option loop. The new text wasconst ablationFence22032p3 = objectWrite;followed byfor (const [oi, opt] of (ablationFence22032p3 ? [] : recordsOf(f.options)).entries()) {. The anchor was hit once, 1 to 0, and the blob went879fcb828037to73bd026d1554. The outer script carriedtrap restore EXIT INT TERMon the absolute path.@objectstack/lintwas rebuilt, andablation-dist-preflightfound the marker in 4 built files.879fcb828037equals HEAD, andgit diff HEADis empty. The whole tree had 0 changed paths. Lint was rebuilt, and--absentfound the marker gone from all 14 built files. Both suites went green again: lint 23 of 23, and the protocol file 99 of 99.Measurements
*.object.tsunderpackages/**andexamples/**(111 files) plus the twoapp-multi-packagesub-stacks: 118 objects in 18 groups. Each was judged at the raw shape and at theObjectSchema.parseshape (0 parse failures), with its own group as context.showcase_cascade:province's fourrecord.countrycascades (zj,gd,ca,tx) andtier'srestrictedrole gate ('org_admin' in current_user.positions).8fc50b7647: 0 build errors and 0 build warnings for the option pass, throughvalidateStackExpressionsand throughrunAuthoringRules('build'), at both shapes. There were 0 door expression findings over all 118 objects.countryand the role gate tosqrt(record.amount) > 1, in a copy, gave 2 build errors at those two options.23ce6c494c, and again at the merged heads06d3cad963and3c1d3262ee: 0 door errors and 0 door advisories over every object, throughrunRuntimeAuthoringRuleswith typeobject, at both shapes. The harness passes each object's own group as context, so at3c1d3262eeevery one of those saves is an update and also runs the stored-universe pass that finding(lint): the runtime gate's object-write baseline drops the written object's stored self, so a master save is refused (422) for a stored detail's violation — the gate's "never charged to someone else's write" contract #22118 added.visibleWhen: 'amount > 1'): 1 build error at every head. The door gave 0 at base and 1 at head.Clause-② (measured)
visibleWhenthe validator refuses. It now answers 422 on the three doors above.@objectstack/lintone doc comment moves (AuthoringRuleContext.runtimeWriteType).StackExpressionOptionsandrunStackExpressionPassesare not in the built declarations. No exported signature moves..changeset/22032-object-save-door-option-visible-when.mdcovers@objectstack/lintand@objectstack/metadata-protocolasminor. It carriesfix(lint)!, theClause-②: no (narrowing)line, a BREAKING section with the remedy, and ADR-0087not-required (no-migration-prescription).@objectstack/metadata-protocolis listed as passes 1 and 2 listed it, although no code moves there: its save, promotion and package-publish doors are where the BREAKING behaviour can be seen..changeset/pre.jsonis absent onorigin/main(read at8fc50b7647, 2026-10-08T01:54Z, atef1fcb26a2, 2026-10-08T02:40Z, and at7ef50a4fbb, 2026-10-08T03:39Z), so the bump isminorwith the BREAKING banner, as in passes 1 and 2. The changeset says it supersedes the earlier 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 entries' line that optionvisibleWhenis not judged at this door.Merges
06d3cad963mergesorigin/mainef1fcb26a2(PR feat(spec)!: refuse bare unique: true on a declared index at protocol 18 — stated scope, zero-drift conversion (ADR-0120 D2/D5a/D7) #22103), throughscripts/pm/os-regen-merge.sh. It was clean.3c1d3262eemergesorigin/main7ef50a4fbb(parents06d3cad963and7ef50a4fbb), throughscripts/pm/os-regen-merge.sh. That brought PR feat(spec)!: the build doors refuse an undeclared key on a script / subflow node config, with its location #22129 (build: ascriptnode's undeclared config key passesobjectstack validate,compileandregisterFlow, then fails every run — the key half of #21898's class (subflowby reading) #21982), PR feat(spec)!: retire the flat string-list arm of manifest.permissions — the structured ADR-0025 block is the only form (#13458) #22094, PR fix(scripts): in Changesets pre mode the doc image and install pins follow the newest GA, not the prerelease #22134, PR fix(lint): the runtime gate's object-write baseline keeps the written item's stored self #22133 (finding(lint): the runtime gate's object-write baseline drops the written object's stored self, so a master save is refused (422) for a stored detail's violation — the gate's "never charged to someone else's write" contract #22118, the gate's object-write baseline keeps the written item's stored self), PR feat(meta): the /meta item read serves the version token, If-None-Match: * pins a first write, and the 409 carries currentVersion #22126 and PR docs(qa): add cross-cutting UX convention items to the platform checklist #22140.validate-expressions.tsauto-merged. PR feat(spec)!: the build doors refuse an undeclared key on a script / subflow node config, with its location #22129's edit is in the flowscript/subflowregion; the option loop's lift is unchanged.packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts, one hunk. Both sides had added a new top-leveldescribeblock at the same place, after the pass-2 block. It was resolved by stacking: the pass-3 block first, then finding(lint): the runtime gate's object-write baseline drops the written object's stored self, so a master save is refused (422) for a stored detail's violation — the gate's "never charged to someone else's write" contract #22118's stored-self block, and every line of both was kept. Against7ef50a4fbbthe file shows only this branch's lines; against06d3cad963it shows only finding(lint): the runtime gate's object-write baseline drops the written object's stored self, so a master save is refused (422) for a stored detail's violation — the gate's "never charged to someone else's write" contract #22118's 114 lines.origin/main's side of every generated path main moved, and that left nothing to commit.7ef50a4fbbis 9 files, +467 / −45.wherestrings, so an option finding is always judged that way.runRuntimeAuthoringRules, typeobject) over 20 cases, against this branch's lint source before the merge (06d3cad963) and after it (3c1d3262ee). The cases are a create, an update over a stored self that is broken and over one that is clean, and a stored sibling that is broken, each for three refused bodies, plus two still-accepted bodies.3c1d3262ee: lint 48 of 48 across the three files, and the protocol file 103 of 103.recordandprevious, neverparent, so the stored universe has nothing extra to offer it.Tests and gates (all at
3c1d3262ee, the merge oforigin/main7ef50a4fbb)mainmoved during the run (H6). PR feat(spec)!: refuse bare unique: true on a declared index at protocol 18 — stated scope, zero-drift conversion (ADR-0120 D2/D5a/D7) #22103 landed asef1fcb26a2and touched two files this pass edits,authoring-rules.tsandruntime-gate.object-writes.test.ts, in other hunks. It was merged withscripts/pm/os-regen-merge.shas a merge commit. The merge was clean, and no generated path was taken from either side. After the merge:pnpm install --frozen-lockfile, a full turbo build (72 tasks), and@objectstack/spec check:generated("All 15 generated artifacts are up to date"). finding(lint): the runtime gate's object-write baseline drops the written object's stored self, so a master save is refused (422) for a stored detail's violation — the gate's "never charged to someone else's write" contract #22118 and build: ascriptnode's undeclared config key passesobjectstack validate,compileandregisterFlow, then fails every run — the key half of #21898's class (subflowby reading) #21982 had not landed at that read (2026-10-08T02:40Z). Both have since landed, and they are merged at3c1d3262ee(see Merges). After that merge the same sequence ran: a full turbo build (72 tasks) andcheck:generated("All 15 generated artifacts are up to date").@objectstack/lint: 125 files and 5729 tests passed.typecheckexit 0, with its test-typecheck included (--listFiles: the five touched or new lint test files are in thetsconfig.test.jsonprogram).@objectstack/metadata-protocol: 221 files passed and 3 skipped; 28260 tests passed and 19 skipped.typecheckexit 0 (--listFiles: the door test file is in the program).@objectstack/objectql, 8 files and 282 tests passed:publish-package-drafts-response-conformance,save-meta-response-conformance,publish-meta-response-conformance,plugin.integration,engine-field-predicate-fault,engine-option-permission-predicate,validation/rule-validator.option-visibilityandengine.@objectstack/rest, 11 files and 197 tests passed: everymeta-object-*file andmeta-publish-package-scope.@objectstack/cli, 3 files and 16 tests passed, run as--project integrationbecause the tier predicate puts them there:validate-field-predicate-traversal(which asserts an optionvisibleWhenexpression-invalidfinding),authoring-rule-command-parityandverify-author-time-stage. The three nightly-tier*.e2efiles that assertexpression-invalidare left to CI.visibleWhen, matched against the save-door entry points. Only the two packages above save an optionvisibleWhenthrough a door.dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 63 commands, the same set at23ce6c494c,06d3cad963and3c1d3262ee. At3c1d3262eeall 63 exit 0, each exit code captured before any pipe.--ranreconciles 63 derived, 63 run, 0 NOT-MEASURED and 0 UNRUN, with an exit code recorded for each. The printed artifact-roster block names 51 families, and all 51 ran at3c1d3262ee, each with exit 0. That is 47 in the same battery, one (check:engine-double-contract) already among the 63, and the three PR-scoped ones (check-partof-closing-keyword,check-closing-target-claim,check-single-claim-paths) run with this PR's number and this body. The same 63 also ran at06d3cad963, all exit 0. At23ce6c494c, before the merge, the same 63 ran: 61 exited 0 on the first run.check:dual-build-cjs-loadsandcheck:lean-entry-closurefirst exited 3 (PREREQUISITE NOT MET: no full build) and exited 0 after the full build.--no-inline-config --format json): 8 files, 0 errors and 0 warnings. Each file is matched byeslint.config.mjs(--print-config), and none was ignored. The config enables no type-aware linting (noparserOptions.project), so this diff cannot move the verdict on any untouched file.Acceptance notes
visibleWhenreadingparentgets no verdict at the build, so none at the door either.23ce6c494cwith scratch tests that were deleted afterwards. Through the realsaveMetaItem, an object whose option carriesvisibleWhen: "parent.status == 'closed'"saved with success, and the row landedactive.runAuthoringRules('build')gave 0 findings.evaluateValidationRules, authenticated caller, the option picked) then loggedfailed to evaluate (authenticated caller) — allowed through, withUnknown variable: parent, and admitted the value. The option evaluator bindsrecord,previous, the user and permissions only.'admin' in current_user.positions. The showcase now writes'org_admin' in current_user.positions, and the pins use that spelling. The comment is not changed here: per the contract review, it rides pass 4.visibleWhenunder "Unchanged", which was true at their heads. This pass's changeset says it supersedes that line rather than editing them, the same way pass 2 left pass 1's changeset alone. Per the contract review, reconciling those lines rides pass 4.formulas.mdxcould name the object save door. That would be a docs addition, not a correction of a false line.06d3cad963(6051573476). The head has since moved to3c1d3262eeby the merge above, so the review for this head is the seat's.Generated by Claude Code