Skip to content

fix(scripts): in Changesets pre mode the doc image and install pins follow the newest GA, not the prerelease - #22134

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-22131-docs-image-tags-pre-mode
Oct 8, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-22131-docs-image-tags-pre-mode

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #22131

Clause-②: no

Refs #22085 (the parent; this PR carries its Q3 half). Ruling record: comment 6049734955 on #22085, quoted as the dispatch carried it:

Q3 → B. During the next line, the documented image and install versions follow the newest GA, not the prerelease. In pre mode sync-docs-image-tags and check-docs-image-tag pin the newest GA (read from the CHANGELOG, the same way the GA is already judged); outside pre mode nothing changes. ⛔ Not taken: A (self-hosting and upgrade documents reading 18.0.0-next.N while npm latest and the published image stay 17.7.0, so a production user copying the documents is led into the breaking prerelease line).

What changed

Two files, both under scripts/, nothing published:

  • scripts/check-docs-image-tag.mjs gains expectedVersion(root), the ONE function that answers which version the doc surfaces must pin.
    • No .changeset/pre.json: packages/cli/package.json's version (VERSION_SOURCE, kept with its name), as before.
    • .changeset/pre.json in mode pre or exit: the newest GA in packages/spec/CHANGELOG.md, overall (not of the CLI's major).
    • It reads through the existing readers only: SPEC_CHANGELOG, gaVersions and newestGaOfMajor from check-release-section-coverage.mjs (unchanged), and readPre from check-changeset-no-major.mjs (unchanged). No second GA reader and no third pre.json reader.
    • Every pre-mode state it cannot read throws instead of falling back: a pre.json that is present but does not parse, a mode Changesets never writes, no spec CHANGELOG, or a CHANGELOG with no GA heading.
    • main() judges against it. The STALE detail, the scope line and the red footer name the source used. A pre-mode red also prints why the expectation is not packages/cli's prerelease. Outside pre mode the gate's output is byte-identical to main's.
  • scripts/sync-docs-image-tags.mjs gains syncRepo({ root }), which main() runs: the target comes from the gate's expectedVersion(), followed by the existing syncSurfaces(). The rewriter and the gate therefore read one value. A pre-mode run logs why the docs did not follow packages/cli.

Not touched: content/docs/**, release.yml, cut-rc.yml, lint.yml, the root version script, .changeset/**, #21988.

Premises measured on origin/main 8fc50b7647

  1. The root version script (package.json:20) is changeset version && sync-protocol-version && sync-template-versions && sync-docs-image-tags && sync-release-index-currency. Holds.
  2. VERSION_SOURCE, SURFACES and PROSE_CLAIMS are as the card states, and the rewriter imports them from the gate. Holds.
  3. check-release-section-coverage.mjs exports SPEC_CHANGELOG, gaVersions (ascending, GA-only, end-anchored) and newestGaOfMajor. Holds.
    • On a pre-mode tree the answer has to be the newest GA overall. After the first next cut the CHANGELOG's top heading is ## 18.0.0-next.0 and there is no GA of 18, so newestGaOfMajor(versions, 18) is null.
    • @objectstack/spec and @objectstack/cli are in one fixed group in .changeset/config.json, so the spec CHANGELOG's newest GA is also the CLI's.
  4. Pre mode is .changeset/pre.json. Refined: Changesets writes it in two modes, and both are the prerelease line.
    • changeset pre exit only rewrites the mode to "exit", so the tree stays on 18.0.0-next.N.
    • The next changeset version then computes the GA and deletes pre.json. Measured in @changesets/apply-release-plan 8.1.1, which removes the file when the mode is exit; replayed below.
    • That exit commit reaches main before the Version Packages PR does. Keying on "pre" alone would expect 18.0.0-next.N in that window while the docs read the GA, which reds this required gate on every PR in between, the exiting PR included.
    • So exit counts as pre mode here. The self-tests pin this case.
  5. Other consumers: no change in meaning except the rc lane below.
    • release.yml:569 and cut-rc.yml:605 import SURFACES only. Re-run after the change, the import prints the same 3 paths and exits 0; the two new imports are side-effect-free, and check:entry-guard is green.
    • lint.yml:2649 and lint.yml:2672 run the two gates. Outside pre mode the output is identical.
    • Correction to the dispatch's reading of cut-rc.yml: it does not refuse pre mode. It refuses unless pre.json is mode pre with tag rc (cut-rc.yml:232-236). So it never runs on the next line, but it does run in pre mode on an rc line.
    • Under this change, an rc cut's version pass leaves the doc surfaces at the newest GA instead of stamping X.Y.Z-rc.N. That follows the ruling's text ("in pre mode") and its reason, a production reader led into a prerelease, which applies to an rc as well.
    • Its allowlist permits the doc surfaces to change and does not require it (git add -A -- … "${DOCS_SURFACES[@]}"), so a cut that leaves them unchanged passes. The self-test pins the rc case: CLI 17.8.0-rc.0, expected 17.7.0.

The pin, replayed

Each run used a throwaway git worktree add --detach under the session scratch directory, then pnpm install --frozen-lockfile --offline --ignore-scripts and pnpm run version. All trees were removed afterwards. PR #22084's two files were copied from its head a630de657d: .changeset/pre.json ({"mode": "pre", "tag": "next"}) and .changeset/22080-v18-line-opens.md.

Tree 1: this branch (a4b1b5106d) plus PR #22084's two .changeset files. pnpm run version exit 0. packages/cli went from 17.7.0 to 18.0.0-next.0, and the spec CHANGELOG's top heading is now ## 18.0.0-next.0, above ## 17.7.0.

✓ sync-docs-image-tags: all 9 concrete pin(s) across 3 surface(s) already at the newest GA in packages/spec/CHANGELOG.md (pre mode) 17.7.0 — nothing rewritten.
  Pre mode: .changeset/pre.json is in mode "pre" (tag "next"), so the documented versions follow the newest GA, 17.7.0, and not packages/cli/package.json's 18.0.0-next.0. ...
$ git diff -- docker/README.md content/docs/deployment/self-hosting.mdx content/docs/upgrading.mdx | wc -l
0
pins on the 3 surfaces: 7 x ghcr.io/objectstack-ai/objectstack:17.7.0, 1 x OS_CLI_VERSION=17.7.0, 1 x @objectstack/cli@17.7.0
$ pnpm check:docs-image-tag      # exit 0
check-docs-image-tag: OK (3/3 enumerated surface(s) read, 9 concrete pin(s) compared against the newest GA in packages/spec/CHANGELOG.md (pre mode) 17.7.0, ...)

Tree 1, continued: the exit window and the GA pass. These are the states premise 4 is about.

$ pnpm exec changeset pre exit   # .changeset/pre.json → {"mode": "exit", "tag": "next"}; packages/cli still 18.0.0-next.0
$ pnpm check:docs-image-tag      # exit 0, compared against the newest GA in packages/spec/CHANGELOG.md (pre mode) 17.7.0
$ pnpm run version               # exit 0; .changeset/pre.json deleted; packages/cli 18.0.0
✓ sync-docs-image-tags: 9 pin(s) across 3 surface(s) → 18.0.0 (lockstep with packages/cli/package.json).
$ pnpm check:docs-image-tag      # exit 0, compared against packages/cli/package.json 18.0.0

Tree 2: this branch on plain main, without pre.json. pnpm run version exit 0. packages/cli went from 17.7.0 to 17.8.0 (the 58 pending changesets), and the surfaces move to the CLI's version, as they do today.

✓ sync-docs-image-tags: 9 pin(s) across 3 surface(s) → 17.8.0 (lockstep with packages/cli/package.json).
pins on the 3 surfaces: 7 x ghcr.io/objectstack-ai/objectstack:17.8.0, 1 x OS_CLI_VERSION=17.8.0, 1 x @objectstack/cli@17.8.0
$ pnpm check:docs-image-tag      # exit 0, compared against packages/cli/package.json 17.8.0

Control tree: main at 8fc50b7647, without this change, plus PR #22084's two files. This is the state the ruling rejects (⛔ A):

✓ sync-docs-image-tags: 9 pin(s) across 3 surface(s) → 18.0.0-next.0 (lockstep with packages/cli/package.json).
pins on the 3 surfaces: 7 x ghcr.io/objectstack-ai/objectstack:18.0.0-next.0, 1 x OS_CLI_VERSION=18.0.0-next.0, 1 x @objectstack/cli@18.0.0-next.0

Self-tests and the ablation

New batteries, each case with its positive control:

  • In the gate, Pre mode: the expectation is the newest GA (#22131) has 18 cases. The roster floor goes from 11 to 12, and the run has 112 assertions.
  • In the rewriter, Control I: in pre mode the target is the newest GA, from the gate's one function has 10 cases and runs syncRepo(). The roster floor goes from 8 to 9, and the run has 45 assertions.

The three Done-when cases, and what each is controlled against:

Case Expected result Positive control
Pre mode, CLI 18.0.0-next.0, newest GA 17.7.0 expects 17.7.0 The same tree's CLI reads 18.0.0-next.0, and the CHANGELOG has no GA of 18
The same tree without pre.json expects the CLI's 18.0.0-next.0 Case 1 answers differently, and pre.json is the only difference between the two trees
Pre mode, docs already at the GA gate green, rewriter writes nothing (byte-identical, mtime unchanged) The same docs against the CLI's version are 3 STALE in the gate and 3 rewrites in the rewriter

Further cases cover mode exit and an rc pre mode, which both expect the GA. Pins moved onto the prerelease in pre mode are STALE, and the finding names the pre-mode source. Each of the four unreadable states is refused, and the rewriter refuses before any write.

Ablation, at a4b1b5106d. The change was committed first. node scripts/ablation-replace.mjs rewrote the pre-mode test in expectedVersion() (if (!existsSync(join(root, PRE_STATE))) { became if (true) {), which is the old logic: pre.json ignored.

  • The mutation landed: the anchor count went 1 → 0, the marker count during the mutation was 1, and the blob went e27045098c80 → 97307ba4e54a.
  • node scripts/check-docs-image-tag.mjs --self-test exited 1 with 13 failures, every one a new pre-mode case.
  • node scripts/sync-docs-image-tags.mjs --self-test exited 1 with 6 failures, every one a new pre-mode case.
  • The controls that do not depend on the branch stayed green.
  • The restore was proven by blob hash: after the restore the blob is e27045098c80, equal to HEAD:scripts/check-docs-image-tag.mjs, and git diff HEAD is empty.

Gates, at a4b1b5106d

node scripts/pm/dispatch-gates.mjs --commands was derived from this worktree over the actual diff (2 paths, +548/-39) and gave 31 commands, the same 31 the dispatch named. All 31 were run and every one exited 0.

  • pnpm check:pm-dispatch-gates passed 1976 cases (856s).
  • pnpm check:docs-image-tag (112 assertions, then OK on the live tree) and pnpm check:docs-image-tag-sync (45 assertions) passed.
  • check:entry-guard, check:nul-bytes, check:scripts-symbol-anchors and check:declaration-mirrors passed.
  • dispatch-gates --ran: 31 derived, 31 run, 0 NOT-MEASURED, 0 UNRUN.

Lint was a proven narrowing to the changed files, not the repo-wide pnpm lint, which CI runs:

  • Population, read from eslint's own config: isPathIgnored is false for both files.
  • Count, from --format json: 2 results, 0 errors and 0 warnings, with --no-inline-config.
  • Invariance: this repo's eslint config never enables type-aware linting (no parserOptions.project), so this diff cannot change the verdict on any untouched file.

No changeset is needed. The root package is private and no package's files[] ships scripts/. Labelled skip-changeset.

Acceptance notes

None of these blocks this PR, and none is a defect. Each is wording that this change makes slightly inaccurate, in a file outside this PR's declared surface. Carrier for each: none.

  • scripts/check-changeset-no-major.mjs's entry-guard comment says "Nothing imports this file today". check-docs-image-tag.mjs now imports readPre. The guard already makes the import inert, as check:entry-guard confirms.
  • release.yml's post-version comment ("check:docs-image-tag — the 3 doc surfaces against packages/cli's NEW version") and lint.yml's step name "Docs image tags track packages/cli's version" are true outside pre mode only. In pre mode the expectation is the newest GA.
  • cut-rc.yml's staging comment (around line 497) lists "the 3 doc surfaces" among a cut's paths. An rc cut now leaves them unchanged (premise 5).
  • dispatch-gates now routes .changeset/pre.json to check:docs-image-tag, because the gate spells it. It does not route packages/spec/CHANGELOG.md, which is read only through an import from a gate module. That file is release-owned and only the version pass writes it.

Generated by Claude Code

claude added 2 commits October 8, 2026 01:53
…s pre mode

expectedVersion() is the one function that answers which version the doc
surfaces must pin: packages/cli/package.json outside pre mode, as before,
and in pre mode (.changeset/pre.json in mode pre or exit) the newest GA in
the spec CHANGELOG, read through check-release-section-coverage.mjs's
gaVersions/newestGaOfMajor, with pre.json read by readPre. Unreadable
pre-mode states are refused rather than falling back. Messages name the
source used; outside pre mode the output is byte-identical.

Claude-Session: https://claude.ai/code/session_01VF48aw8RPG6wzDnMgp6rtw
Co-authored-by: Claude <noreply@anthropic.com>
…xpectedVersion()

syncRepo() is what main() runs: the target from check-docs-image-tag.mjs's
expectedVersion(), then syncSurfaces() over it, so in Changesets pre mode
the version pass leaves the doc surfaces at the newest GA instead of
stamping the prerelease, and outside pre mode it moves them to
packages/cli's version as before. The self-test runs syncRepo() over
pre-mode fixture trees, each case with its positive control.

Claude-Session: https://claude.ai/code/session_01VF48aw8RPG6wzDnMgp6rtw
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added the size/l label Oct 8, 2026
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 8, 2026
@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 8, 2026 02:56
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 8, 2026 02:56
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 8, 2026
Merged via the queue into main with commit ec8f37c Oct 8, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-22131-docs-image-tags-pre-mode branch October 8, 2026 03:27
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 9, 2026
… verdict (objectstack-ai#22032 pass 3) (objectstack-ai#22151)

Part of objectstack-ai#22032
Clause-②: no (narrowing)

This is pass 3 of objectstack-ai#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 `[objectstack-ai#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 `[objectstack-ai#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 objectstack-ai#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 objectstack-ai#22032 entries' line that option `visibleWhen` is not judged
at this door.

## Merges

- **`06d3cad963`** merges `origin/main` `ef1fcb26a2` (PR objectstack-ai#22103),
through `scripts/pm/os-regen-merge.sh`. It was clean.
- **`3c1d3262ee`** merges `origin/main` `7ef50a4fbb` (parents
`06d3cad963` and `7ef50a4fbb`), through `scripts/pm/os-regen-merge.sh`.
That brought PR objectstack-ai#22129 (objectstack-ai#21982), PR objectstack-ai#22094, PR objectstack-ai#22134, PR objectstack-ai#22133
(objectstack-ai#22118, the gate's object-write baseline keeps the written item's
stored self), PR objectstack-ai#22126 and PR objectstack-ai#22140.
- `validate-expressions.ts` auto-merged. PR objectstack-ai#22129's edit is in the flow
`script` / `subflow` region; the option loop's lift is unchanged.
- One conflict:
`packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts`,
one hunk. Both sides had added a new top-level `describe` block at the
same place, after the pass-2 block. It was resolved by stacking: the
pass-3 block first, then objectstack-ai#22118's stored-self block, and every line of
both was kept. Against `7ef50a4fbb` the file shows only this branch's
lines; against `06d3cad963` it shows only objectstack-ai#22118's 114 lines.
- No generated path was touched by this branch. The rerun of the script
took `origin/main`'s side of every generated path main moved, and that
left nothing to commit.
- This branch's own changes are the same before and after the merge: for
each of the 9 files, the added and removed lines against the merge base
are identical. The delta against `7ef50a4fbb` is 9 files, +467 / −45.
- **The interaction with objectstack-ai#22118, measured.** objectstack-ai#22118 adds a third,
stored-universe pass on an update into a context collection. A finding
whose path names no entry is judged as one on the written item. The
expression rule's paths are `where` strings, so an option finding is
always judged that way.
- A probe ran the real gate (`runRuntimeAuthoringRules`, type `object`)
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.
- The verdicts are identical, case for case. Re-saving a broken option
is still refused, including over a broken stored self, because re-saving
a row is writing it. A clean save over a broken stored self passes. A
broken sibling is never charged.
- The pass-3 pins (differential, PARITY, LIT, CONTROL, CONTRAST) and
objectstack-ai#22118's pins all pass at `3c1d3262ee`: lint 48 of 48 across the three
files, and the protocol file 103 of 103.
- This matches the code. The option pass reads only the written object's
own field index, and binds `record` and `previous`, never `parent`, so
the stored universe has nothing extra to offer it.

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

- **`main` moved during the run (H6).** PR objectstack-ai#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"). objectstack-ai#22118 and objectstack-ai#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 objectstack-ai#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](https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/l skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants