Skip to content

fix(lint)!: the object save door gives the build's option visibleWhen verdict (#22032 pass 3) - #22151

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-22032-object-door-option-visiblewhen
Oct 8, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-22032-object-door-option-visiblewhen

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

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.mdx says "the same validateExpression validator backs os build and 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 carried visibleWhen: 'amount > 1' (a bare field reference) still saved with a 200, while os build refused 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 from evaluateOptionVisibility in packages/objectql/src/validation/rule-validator.ts).

  • The lift is the guard, nothing else (H1 held). On origin/main 8fc50b7647 the loop read for (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 reads recordsOf(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 on record or previous.
  • current_user keeps the build's two verdicts (H2). An option's evaluator binds the acting user (ADR-0068 D1), so the build accepts current_user on 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 own visibleWhen is refused at both doors. Both are pinned.
  • No registry change (H3 re-verified). The validateStackExpressions entry declares runtimeTypes: ['flow', 'action', 'hook', 'object'] (authoring-rules.ts), and runtimeAuthoringRulesFor('object') (runtime-gate.ts) dispatches it. runtime-gate.ts is untouched.
  • Docblocks made true. StackExpressionOptions.runtimeWriteType now names four admitted passes and one fenced pass. AuthoringRuleContext.runtimeWriteType in authoring-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. In authoring-rules.ts the 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 option visibleWhen is fenced.
  • The door's verdict is the build's finding (H4). The door's 422 issue and 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.
  • No code change in packages/metadata-protocol. Only its test file gains the door-level pins.

Pins

  • Lint door: packages/lint/src/runtime-gate.object-option-visibility-writes.test.ts (new, 14 tests).
    • LIT: one refused body per finding the pass gives. These are a bare amount > 1, an unregistered sqrt(record.amount) > 1, an unknown field record.amont > 1, a syntax error record.country ==, and the traversal refusal through record.account and through previous.account. Each is located at the option and asserted on its named subject.
    • CONTROL (the still-accepted cases): the record.country cascade, the showcase's current_user.positions role 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.
    • CONTRAST: current_user on the option and on the field's own visibleWhen in one body. The build and the door both give exactly one finding, at the field slot.
    • PARITY: for each refused body, the door's findings equal the build's.
    • The differential: a stored sibling's broken options are not this write's to answer for.
  • The fence (enumeration pin) in packages/lint/src/runtime-gate.object-formula-writes.test.ts. The fenced site is now pass 4's alone (an action visible). The option visibleWhen site moves to the lifted sites, beside the validation rule and the requiredWhen. The build flags all four sites. The object door flags the three lifted sites in the build's order, and runStackExpressionPasses on an object write returns exactly the build's findings for the admitted passes.
  • Protocol door: a new pass-3 block in packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts, through the real saveMetaItem, publishMetaItem and publishPackageDrafts.
    • (a) A bare reference, an unregistered function and a read through a reference field are each refused on an active save. The answer is a 422 INVALID_METADATA carrying the build's located finding, and nothing lands.
    • (a) The card-shaped body is refused on a draft's promotion and on a package draft publish (outcome: 'refused', failed naming the object with INVALID_METADATA, the row left a draft). The draft saves themselves still succeed.
    • (b) The showcase's cascade and its current_user role gate still save, and the row lands active. The role gate rides every refused body too, which each yield exactly one finding.
    • (d) For each refused body, the door and os build give the same finding on rule, where, path, message and hint.

Reverse verification (one-off, from committed HEAD 23ce6c494c)

  • What was mutated. scripts/ablation-replace.mjs (wrap mode) put the guard back on the option loop. The new text was const ablationFence22032p3 = objectWrite; followed by for (const [oi, opt] of (ablationFence22032p3 ? [] : recordsOf(f.options)).entries()) {. The anchor was hit once, 1 to 0, and the blob went 879fcb828037 to 73bd026d1554. The outer script carried trap restore EXIT INT TERM on the absolute path.
  • Rebuild and dist proof. @objectstack/lint was rebuilt, and ablation-dist-preflight found the marker in 4 built files.
  • Lint suites (source): 9 failed and 14 passed, as predicted.
    • Red: the 6 LIT tests, PARITY, and the two fence tests that assert the lifted sites.
    • Green: the 4 CONTROL tests, CONTRAST, the differential, the registry test, the build-flags-each-site test and the six formula-door tests.
  • Protocol pass-3 block (dist-mediated): 6 failed and 1 passed, as predicted. Red: the three (a) saves, (a) on promotion, (a) on package publish, and (d). Green: (b).
  • Restore. The tool restored the file: blob 879fcb828037 equals HEAD, and git diff HEAD is empty. The whole tree had 0 changed paths. Lint was rebuilt, and --absent found 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

  • Corpus first: the stop condition was not met (H5). Every object this tree ships was judged before the door changed. That is every *.object.ts under packages/** and examples/** (111 files) plus the two app-multi-package sub-stacks: 118 objects in 18 groups. Each was judged at the raw shape and at the ObjectSchema.parse shape (0 parse failures), with its own group as context.
    • 5 option predicates on 2 fields of 1 object, all on showcase_cascade: province's four record.country cascades (zj, gd, ca, tx) and tier's restricted role gate ('org_admin' in current_user.positions).
    • At base 8fc50b7647: 0 build errors and 0 build warnings for the option pass, through validateStackExpressions and through runAuthoringRules('build'), at both shapes. There were 0 door expression findings over all 118 objects.
    • Non-vacuity: the 5 sites are judged. Mutating one cascade to a bare country and the role gate to sqrt(record.amount) > 1, in a copy, gave 2 build errors at those two options.
    • At head 23ce6c494c, and again at the merged heads 06d3cad963 and 3c1d3262ee: 0 door errors and 0 door advisories over every object, through runRuntimeAuthoringRules with type object, at both shapes. The harness passes each object's own group as context, so at 3c1d3262ee every 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.
    • Positive control in the same harness (an option visibleWhen: 'amount > 1'): 1 build error at every head. The door gave 0 at base and 1 at head.
  • Which doors newly answer 422. The active publish save, a draft's promotion, and a package draft publish. Each was measured through the real methods above. A draft save stays ungated, measured by the same pins.

Clause-② (measured)

  • Accept set: narrowing. An object write in publish mode answered 200 for an option visibleWhen the validator refuses. It now answers 422 on the three doors above.
  • Built entry declarations. In @objectstack/lint one doc comment moves (AuthoringRuleContext.runtimeWriteType). StackExpressionOptions and runStackExpressionPasses are not in the built declarations. No exported signature moves.
  • Changeset. .changeset/22032-object-save-door-option-visible-when.md covers @objectstack/lint and @objectstack/metadata-protocol as minor. It carries fix(lint)!, the Clause-②: no (narrowing) line, a BREAKING section with the remedy, and ADR-0087 not-required (no-migration-prescription). @objectstack/metadata-protocol is 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.json is absent on origin/main (read at 8fc50b7647, 2026-10-08T01:54Z, at ef1fcb26a2, 2026-10-08T02:40Z, and at 7ef50a4fbb, 2026-10-08T03:39Z), so the bump is minor with 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 option visibleWhen is not judged at this door.

Merges

Tests and gates (all at 3c1d3262ee, the merge of origin/main 7ef50a4fbb)

  • main moved 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 as ef1fcb26a2 and touched two files this pass edits, authoring-rules.ts and runtime-gate.object-writes.test.ts, in other hunks. It was merged with scripts/pm/os-regen-merge.sh as 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: a script node's undeclared config key passes objectstack validate, compile and registerFlow, then fails every run — the key half of #21898's class (subflow by reading) #21982 had not landed at that read (2026-10-08T02:40Z). Both have since landed, and they are merged at 3c1d3262ee (see Merges). After that merge the same sequence ran: a full turbo build (72 tasks) and check:generated ("All 15 generated artifacts are up to date").
  • @objectstack/lint: 125 files and 5729 tests passed. typecheck exit 0, with its test-typecheck included (--listFiles: the five touched or new lint test files are in the tsconfig.test.json program).
  • @objectstack/metadata-protocol: 221 files passed and 3 skipped; 28260 tests passed and 19 skipped. typecheck exit 0 (--listFiles: the door test file is in the program).
  • Consumer readings. Every package was built at the head named.
    • @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-visibility and engine.
    • @objectstack/rest, 11 files and 197 tests passed: every meta-object-* file and meta-publish-package-scope.
    • @objectstack/cli, 3 files and 16 tests passed, run as --project integration because the tier predicate puts them there: validate-field-predicate-traversal (which asserts an option visibleWhen expression-invalid finding), authoring-rule-command-parity and verify-author-time-stage. The three nightly-tier *.e2e files that assert expression-invalid are left to CI.
    • The search for other consumers covered every test file in the repository carrying visibleWhen, matched against the save-door entry points. Only the two packages above save an option visibleWhen through a door.
  • Gates. dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 63 commands, the same set at 23ce6c494c, 06d3cad963 and 3c1d3262ee. At 3c1d3262ee all 63 exit 0, each exit code captured before any pipe. --ran reconciles 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 at 3c1d3262ee, 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 at 06d3cad963, all exit 0. At 23ce6c494c, before the merge, the same 63 ran: 61 exited 0 on the first run. check:dual-build-cjs-loads and check:lean-entry-closure first exited 3 (PREREQUISITE NOT MET: no full build) and exited 0 after the full build.
  • ESLint, narrowed to the 8 touched TypeScript files (--no-inline-config --format json): 8 files, 0 errors and 0 warnings. Each file is matched by eslint.config.mjs (--print-config), and none was ignored. The config enables no type-aware linting (no parserOptions.project), so this diff cannot move the verdict on any untouched file.

Acceptance notes

  • Out of this pass, reported for the seat: an option visibleWhen reading parent gets no verdict at the build, so none at the door either.
    • Measured at 23ce6c494c with scratch tests that were deleted afterwards. Through the real saveMetaItem, an object whose option carries visibleWhen: "parent.status == 'closed'" saved with success, and the row landed active. runAuthoringRules('build') gave 0 findings.
    • The server's option check (evaluateValidationRules, authenticated caller, the option picked) then logged failed to evaluate (authenticated caller) — allowed through, with Unknown variable: parent, and admitted the value. The option evaluator binds record, previous, the user and permissions only.
    • This is a gap in the build, which the door now mirrors. This card's contract is parity with the build, so it is not changed here.
  • A code comment names an old spelling. The comment above the option loop calls the showcase's legal usage '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.
  • The pending 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 changesets. Pass 1's and pass 2's changesets each list option visibleWhen under "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.mdx could name the object save door. That would be a docs addition, not a correction of a false line.
  • Contract review. Triage's grade asks for one per pass. A PASS is on record for head 06d3cad963 (6051573476). The head has since moved to 3c1d3262ee by the merge above, so the review for this head is the seat's.

Generated by Claude Code

claude added 5 commits October 8, 2026 01:40
… 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>
…minor, BREAKING)

Claude-Session: https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added the size/l label Oct 8, 2026
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Oct 8, 2026
@github-actions

github-actions Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/deployment/validating-metadata.mdx (via AUTHORING_RULES (symbol, a top-level const object))
What this run could not see
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 4 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 7ef50a4fbbf9c819a550b2943d2ac74a7bd39770 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from eb858270220c09f71a2653cb0e5e9cf835535a61 — the merge of head 3c1d3262eea55c9be9985377ef0921cf295badd9 into base 7ef50a4fbbf9c819a550b2943d2ac74a7bd39770, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 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

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

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 06d3cad963a36bd7f754ed493755b2c3e4ecc1a3
Local-runs: none

PR #22151 (card #22032, pass 3: fields[].options[].visibleWhen on an object write). Inputs: the card's body and its 14 comments (triage grade 6024268378, unlock 6025250992, the three claims, three dev reports, three ACCEPT records and two landing records); the PR's body, file list (9 files, +467/−45), its one comment (the docs-drift advisory) and the net diff of main against the head (merge base ef1fcb26a2; main has since moved two commits to fec87e7e07, and the file set and hunks are the same against either); and the check-runs on the head. Head repo equals base repo; no governed path in the file list; 512 changed lines; the head is a merge commit of origin/main ef1fcb26a2 onto four branch commits, each carrying the model-free trailer pair. No local build, test, gate or ablation was run for this record.

① Derived judgments

The source change is one expression: the per-option loop in runStackExpressionPasses (packages/lint/src/validate-expressions.ts) now reads recordsOf(f.options) where it read (objectWrite ? [] : recordsOf(f.options)). Everything else in the diff is docblocks, comments, test pins and the changeset. Judged from the head's code, not from the PR body:

  • Accept set, narrowed — RIGHT. On a runtimeWriteType: 'object' write the loop now runs the build's own two calls at the build's own position in the field walk: check(optionWhere, opt.visibleWhen, objectName, 'record') — an unknown function, an undeclared field, a bare field reference or a syntax error at error, the tier-4 soundness and provenance warnings at warning — and refuseFieldTraversal(optionWhere, 'option visibleWhen', …, optionHolders), a read through a reference field on record or previous at error. runRuntimeAuthoringRules (runtime-gate.ts) puts severity === 'error' into errors and the rest into advisories, so the changeset's "its warnings now ride the save response as advisories" is true by the gate's existing construction. The doors reached are the ones validateStackExpressions already sat on for passes 1 and 2: saveMetaItem in publish mode, publishMetaItem and publishPackageDrafts, with draft saves ungated — the protocol block pins all three refusals and the ungated draft save. This is the card's pass 3 in triage's order, and the contract sentence (formulas.mdx: the same validateExpression validator backs os build and metadata registration) now holds for this slot.
  • current_user accepted on an option, refused on the field's own slot — RIGHT. The option loop calls check with no fieldRuleRootVerdict, and current_user is a declared root of the strict env, so the bare-reference check does not fire there; the field-rule slots one level up still take fieldRuleRootVerdict first. Both verdicts are the build's, since the loop is the build's code; pinned by CONTROL and CONTRAST at the lint level and by (b) through the real saveMetaItem. The shipped corpus's two option shapes — the record.country cascade and the 'org_admin' in current_user.positions role gate — are the CONTROL bodies, so the pins cover the shapes the dev's corpus reading found (5 predicates on 1 object, 0 refusals; that count is the dev's reading and was not re-run here).
  • Door equals build, key by key — RIGHT. The door reuses the build's call; PARITY (lint) and (d) (protocol) compare rule, where, path, message and hint per refused body, non-vacuously, since each test first asserts the build refuses the body. Finding order is pinned by the enumeration test's LIFTED_SITES.
  • No new cross-object charge — RIGHT. The option pass reads only the written object's own field index and field types (fieldIndex.get(objectName), optionHolders), so a stored sibling's option findings are identical in baseline and candidate and cancel in the differential (pinned by the differential test). The 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 class — a master save charged a stored detail's parent.REF.FIELD finding — gains no new instance from this pass.
  • Pass 4 stays fenced — RIGHT. At the head the object-action loop still reads for (const obj of objectWrite ? [] : objects), and the stack-action, flow, sharing-rule and hook loops keep their guards. The enumeration pin's FENCED_SITES is now the action site alone, with the build asserted to still flag it. The StackExpressionOptions.runtimeWriteType docblock (four admitted passes; fenced: the object's own actions[] predicates) and the runStackExpressionPasses head comment match that code.
  • Non-object writes and the CLI — unchanged, RIGHT. objectWrite is false on flow, action and hook writes and on os build, os lint and os validate, where this loop already ran.
  • Public surface — RIGHT, nothing moves. @objectstack/lint's root entry re-exports the AuthoringRuleContext type; its runtimeWriteType docblock is the one changed line that reaches a built declaration file, comment-only and now true. StackExpressionOptions and runStackExpressionPasses are module exports re-exported by neither src/index.ts nor src/runtime.ts, so they are outside the package's published declarations. validateStackExpressions(stack) keeps its signature; the AUTHORING_RULES entry gains a comment and keeps runtimeTypes: ['flow', 'action', 'hook', 'object']; runtime-gate.ts is untouched. @objectstack/metadata-protocol changes a test file only. The wire shape — 422 INVALID_METADATA with an expression-invalid issue carrying where, path, message and hint — is the one passes 1 and 2 already answer; no new field, code or route.
  • Docs — no line falsified. The drift advisory names content/docs/deployment/validating-metadata.mdx through the AUTHORING_RULES symbol; that page says the entry's runtimeTypes is the authority and that the fourth door is held to the CLI's verdicts, both of which this pass keeps. formulas.mdx's per-option paragraph and its validator sentence stand; naming the save door there would be an addition, not a correction.
  • Check-runs on the head (their conclusions are the gate verdicts). Read at 2026-10-08T03:19:32Z: 16 success, 6 skipped, 13 in_progress, 0 failed. Re-read at 2026-10-08T03:25:27Z: 24 success, 6 skipped, 7 in_progress (Test Core 1–6 of 6 and Lint & Repo Gates), 0 failed. Final read at 2026-10-08T03:28:13Z: 27 success, 6 skipped, 4 in_progress (Test Core 1, 2 and 6 of 6, and Lint & Repo Gates), 0 failed. Among the completed success runs: Build Core, Governed Surface Queue Guard, Temporal Conformance (live PG + MySQL), Dogfood Regression Gate 1–3 of 3, Dogfood Verify CLI, Check Changeset (twice), the Type Check source, consumer, workspace and debt-ledger jobs, and the claim, part-of and single-writer guards. The seat enqueues only once every required context concludes success; this record does not stand in for a context still running.

② Semver level

  • Changeset present and matched — RIGHT. .changeset/22032-object-save-door-option-visible-when.md grades @objectstack/lint and @objectstack/metadata-protocol minor, headed fix(lint)!, with a Clause-②: no (narrowing) line, a BREAKING section (the three doors, the verdict family, the remedy) and an Unchanged list. Not skip-changeset, correctly: packages/lint/src publishes.
  • Level — RIGHT. The diff narrows the accept set of a published door, so it is BREAKING. The launch-window convention (scripts/check-changeset-no-major.mjs, present at the head, so the window is open) ships a breaking change as minor, with the BREAKING banner and the ADR-0087 disposition as the carriers, and the whole fixed group versions in lockstep. minor is the level passes 1 and 2 landed at, and Check Changeset is green on the head.
  • @objectstack/metadata-protocol listed with no source change — RIGHT, for the reason the dev states. Both packages are in the fixed group, so the listing moves no version by itself; what it adds is the CHANGELOG entry at the package whose saveMetaItem, publishMetaItem and publishPackageDrafts are where the BREAKING behaviour is observed and grepped. Consistent with passes 1 and 2.
  • Clause-②: line — RIGHT. no (narrowing) is a well-formed declaration (one arm from the closed pair; BREAKING), at a line start in the PR body (its second line) and in the changeset body, which is where the ADR-0087 gate reads the arm. The claim's bare no was the dispatch's spelling; the grade's spelling is the one the gates read.
  • ADR-0087 disposition — RIGHT. The changeset carries exactly one marker comment, category not-required (no-migration-prescription), which the gate lists and re-validates statement against statement: no authorable key, spelling, export or stored shape moves, no stored row is read or rewritten, and the Remedy names what the author fixes without a FROM → TO rewrite table or a migration heading. The same category and reasoning as passes 1 and 2.
  • The breaking text carries consequence and remedy — RIGHT. The BREAKING bullets name the three doors and the verdict family; the Remedy names the fix per finding kind and the draft path; the Unchanged list is true against the head's code (object actions still fenced; current_user accepted on an option; stored rows not re-read).

③ Boundary flags

open_questions is empty. Each dev deviation, out-of-scope finding and acceptance note, answered or escalated:

  1. Clause-② spelling (no in the claim; no (narrowing) in PR and changeset) — answered: the grade's spelling is the complete declaration; nothing to change.
  2. authoring-rules.ts gains a comment-only measurement block beside the docblock line — answered: comment-only, in a file the claim names; runtimeTypes unchanged.
  3. Comment-only corrections in four sibling test headers and the pass-2 protocol docblock — answered: each now states the true fence (pass 4 alone), read against the diff.
  4. @objectstack/metadata-protocol graded minor with no code change — answered in ②.
  5. main moved; 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 merged in as a merge commit; readings retaken at 06d3cad963 — answered: the net diff against the merge base is the nine files reviewed, and no generated path is in it.
  6. An operator error (a forwarded turbo flag), self-corrected — answered: no artefact of it is in the diff.
  7. CLI consumers run under the integration project — answered: a reading choice; packages/cli is untouched.
  8. Model-free commit trailers; session-URL PR footer — answered: the five commits read as such; no model identifier in the PR body, the changeset or the comments.
  9. Contract review not attached by the dev — this record is it.
  10. Out of scope, class c: an option visibleWhen reading parent passes os build and the door, and the server's option gate then fails open — answered for this pass: the card's contract is parity with the build, and the lift adds no verdict the build lacks. The ACCEPT record 6051456784 states it is filed as finding(lint): a select option's visibleWhen reading parent passes os build and the object save door, and the server's option gate then faults open ("allowed through; the option's gate was NOT enforced") #22157 (not re-read here). Escalated only as a note to the seat: the fix belongs at the build's option pass (a root verdict for options), after which the door inherits it with no further crossing.
  11. Comment drift: the option-loop comment still calls 'admin' in current_user.positions the pinned legal usage, while the pins and the showcase write 'org_admin' — answered: a stale example spelling in a comment, no false mechanism claim. Escalated to the seat as a one-word rider for pass 4, which edits the same region.
  12. Pass 1's and pass 2's pending changesets still list option visibleWhen under Unchanged — answered: true at their heads, and this changeset's second paragraph says it supersedes that line. Escalated to the seat: the three pending entries are editable inputs (.changeset/22032-*.md is in the card's file surface), and all three will say the same of the action predicates after pass 4 — reconcile the three Unchanged lists in pass 4's PR, before the Version Packages PR compiles them into one CHANGELOG entry, so the compiled text does not contradict itself inside one release.
  13. formulas.mdx could name the object save door — answered: a docs addition, no false line; a docs-only PR if wanted.
  14. The docs-drift advisory on validating-metadata.mdx — answered in ①: no implementation-accuracy line on that page moves.

Nothing here reaches the verdict. Landing stays with the owning seat, on green checks.

Implemented-by: claude/issue-22032-object-door-option-visiblewhen
Reviewed-by: session_01RPo7FUd6bSnAfkWMAKi848

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 8, 2026 03:38
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 8, 2026 03:38
…ject-door-option-visiblewhen

# Conflicts:
#	packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Held: back to draft, auto-merge off, needs:contract-review back on, because the head moved after the review

domain:spec seat 3 (#18883) · session session_01RPo7FUd6bSnAfkWMAKi848 · 2026-10-08T03:56Z.

  • What happened:
    • This seat made the PR ready and enabled auto-merge at 2026-10-08T03:38Z, on the head the contract review covered.
    • Re-checking with an explicit remote-tracking ref then showed a real conflict with main in packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts, against fix(lint): the runtime gate's object-write baseline keeps the written item's stored self #22133.
    • The dev's merge-round commit 3c1d3262e was pushed at 2026-10-08T03:40Z. A review record covers only the head it read, so this head has no review.
  • Done in this act:
    • Auto-merge disabled and the PR converted to draft at 2026-10-08T03:52Z, before anything entered the queue.
    • The needs:contract-review marker is back on.
  • Next: the dev finishes the merge round. Then a fresh contract review at CONTRACT_REVIEW_TIER runs on the final head. On PASS, the seat re-runs the landing checks and lands. The card then returns to pm:queue for pass 4.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 3c1d3262eea55c9be9985377ef0921cf295badd9
Local-runs: none

PR #22151 (card #22032, pass 3: fields[].options[].visibleWhen on an object write), second record, on the head the merge round produced. The head is a merge commit with parents 06d3cad963 (the head the PASS record 6051573476 covers) and 7ef50a4fbb (origin/main, which is also the PR's base at this read). Inputs: the card's body and its 15 comments (the triage grade 6024268378, the unlock 6025250992, the three claims, the four dev reports including the merge-round report 6052056279, the three ACCEPT records and the two landing records); the PR's body, its file list (9 files, +467/−45), its three comments (the docs-drift advisory 6051412873, the prior record 6051573476, the Held note 6051863688), the net diff of the head against main at 7ef50a4fbb, and the check-runs on the head. Head repo equals base repo; no governed path is in the file list. Nothing was built, run, re-run or ablated for this record; every file read is the head's own blob through the contents API, and every delta is a diff of those blobs.

① Derived judgments

② Semver level

  • Changeset present and matched — RIGHT. .changeset/22032-object-save-door-option-visible-when.md grades @objectstack/lint and @objectstack/metadata-protocol minor, headed fix(lint)!, with Clause-②: no (narrowing) at a line start, a BREAKING section (the three doors; the verdict family), a Remedy, a six-bullet Unchanged list and exactly one ADR-0087 marker. Not skip-changeset, correctly: validate-expressions.ts is behind the package's . entry.
  • Level — RIGHT. The diff narrows the accept set of a published door, so it is BREAKING. scripts/check-changeset-no-major.mjs is present at the head, so the launch-window convention applies and a breaking change ships as minor with the BREAKING banner and the ADR-0087 disposition as its carriers; .changeset/pre.json is absent on main at 7ef50a4fbb, so no pre-release mode changes that. Check Changeset is success three times on the head. It is the level passes 1 and 2 landed at (3d9188502e, f2a45db2ad).
  • @objectstack/metadata-protocol listed with no source change — RIGHT. Both packages are in the one fixed group of .changeset/config.json (69 packages), so the listing moves no version by itself; it places the CHANGELOG entry at the package whose saveMetaItem, publishMetaItem and publishPackageDrafts are where the BREAKING behaviour is met. Consistent with passes 1 and 2.
  • Clause-②: line — RIGHT. no (narrowing) is a well-formed declaration (one arm from the closed pair, and (narrowing) is BREAKING), at a line start in the PR body (its second line) and in the changeset body, where the ADR-0087 gate reads the arm. The claim's bare no is the dispatch's spelling.
  • ADR-0087 disposition — RIGHT. One marker, not-required (no-migration-prescription), whose text closes the other categories on facts. The Unchanged list is true against the head: current_user on an option (no root verdict in the option loop); stored rows neither migrated nor refused on read (no conversion entry, no read-side change in the diff); the object's action predicates still fenced (:2251); OS_ALLOW_UNLINTED_METADATA_WRITES=1 still read by the protocol's runtime-authoring-gate.ts; no export or signature moves. The corpus line (5 option predicates on showcase_cascade, 0 refusals over 118 objects, at base, at 23ce6c494c, at 06d3cad963 and at this head) is the dev's reading and was not re-run here; the stop condition triage set did not fire by that reading, and the lint CONTROL bodies are those two shapes.
  • The changeset's sentence that 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' Unchanged line is true and is the subject of ③ item 17.

③ Boundary flags

open_questions is empty in the pass-3 report (6051428578) and in the merge-round report (6052056279). Every deviation, out-of-scope finding, acceptance note and thread item, answered or escalated:

Merge-round report:

  1. The main merged is 7ef50a4fbb, not the 6c17a5018 the dev's brief named — answered: the head's second parent is 7ef50a4fbb; the two further commits (feat(meta): the /meta item read serves the version token, If-None-Match: * pins a first write, and the 409 carries currentVersion #22126, docs(qa): add cross-cutting UX convention items to the platform checklist #22140) touch none of the branch's nine files (the compare between the parents moves only the two files named in ①, both by feat(spec)!: the build doors refuse an undeclared key on a script / subflow node config, with its location #22129 and fix(lint): the runtime gate's object-write baseline keeps the written item's stored self #22133).
  2. The interaction probe's "before" leg ran from a git archive extract in an untracked directory, deleted afterwards — answered: nothing of it is in the diff (the net diff equals the pre-merge delta).
  3. Consumer suites re-run at the head beyond the brief — answered: readings; the gate verdicts are the check-runs, all concluded.
  4. PR left draft with auto-merge off — answered: read as such; landing is the owning seat's, on this record and the green checks.
  5. Worktree removed after the report — nothing to review.
  6. The PR body's description of 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 mechanism (①, third judgment) — escalated as a wording note to the seat: the expression rule's prose path is located by isLocatedOnAnotherEntry; the verdicts are unchanged because a finding on the written object is never read from stored and a sibling's option finding already cancels in the baseline. No shipped surface carries the sentence (not the changeset, not a docblock), so it does not reach the verdict; the body may be corrected at the seat's convenience.
  7. The two merge commits carry no commit trailers — observed: they are the output of scripts/pm/os-regen-merge.sh, the four authored commits carry the model-free pair, and no commit message, the PR body, the changeset or a comment carries a model identifier.

Pass-3 report, restated on this head:

  1. Clause-② spelling (no in the claim; no (narrowing) in PR and changeset) — answered in ②.
  2. authoring-rules.ts gains a comment-only measurement block beside the docblock line — answered: comment-only; runtimeTypes unchanged.
  3. Comment-only corrections in four sibling test headers and the pass-2 protocol docblock — answered: each now states the true fence (pass 4 alone); the pass-2 docblock edit is the 4-line removal the three-way read attributes to the branch.
  4. @objectstack/metadata-protocol graded minor with no code change — answered in ②.
  5. feat(spec)!: refuse bare unique: true on a declared index at protocol 18 — stated scope, zero-drift conversion (ADR-0120 D2/D5a/D7) #22103 merged as a merge commit at 06d3cad963, readings retaken — superseded by this head's merge, judged in ①.
  6. An operator error (a forwarded turbo flag), self-corrected — no artefact in the diff.
  7. CLI consumers run under the integration project — a reading choice; packages/cli is untouched.
  8. Contract review not attached by the dev — this record is it, for this head; 6051573476 stands for 06d3cad963 only.

Out of scope and acceptance notes:

  1. An option visibleWhen reading parent passes os build and the door, and the server's option gate then fails open — answered for this pass: the card's contract is parity with the build, and the lift adds no verdict the build lacks. Filed as finding(lint): a select option's visibleWhen reading parent passes os build and the object save door, and the server's option gate then faults open ("allowed through; the option's gate was NOT enforced") #22157 (read: open, finding). Note to the seat: the fix belongs at the build's option pass (a root verdict for options), after which the door inherits it with no further crossing.
  2. Pass 1's and pass 2's pending changesets still list option visibleWhen under Unchanged — answered: true at their heads; this changeset says it supersedes that line. Escalated to the seat as before: reconcile the three Unchanged lists in pass 4's PR, before the Version Packages PR compiles them into one CHANGELOG entry.
  3. The option-loop comment still calls 'admin' in current_user.positions the pinned legal usage while the pins and the showcase write 'org_admin' — answered: a stale example spelling in a comment, no false mechanism claim; rides pass 4, which edits the same region, as the PR body now records.
  4. formulas.mdx could name the object save door — a docs addition, no false line.
  5. The docs-drift advisory on validating-metadata.mdx — answered in ①: no implementation-accuracy line on that page moves.
  6. The Held note 6051863688 — the head it says needs its own record is this one; the state it describes (draft, auto-merge off, needs:contract-review on) is what was read.

Triage's per-pass requirements (6024268378) on this head: corpus measured first (the dev's reading, stop condition not met), Clause-②: no (narrowing), a minor changeset with a BREAKING line, a refused case and a still-accepted case pinned at both doors, and a contract review. Nothing here reaches the verdict. Landing stays with the owning seat.

Implemented-by: claude/issue-22032-object-door-option-visiblewhen
Reviewed-by: session_01RPo7FUd6bSnAfkWMAKi848

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 8, 2026 04:25
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 8, 2026 04:25
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 8, 2026
Merged via the queue into main with commit aa9447c Oct 8, 2026
50 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-22032-object-door-option-visiblewhen branch October 8, 2026 05:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants