Skip to content

fix(formula)!: the unknown-field check judges every member spelling of record / previous, not only the dot (#22428) - #22494

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-22428-field-existence-every-spelling
Oct 9, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-22428-field-existence-every-spelling

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Fixes #22428
Clause-②: yes (narrowing)

What changed

validateExpression's field-existence pass (checkFieldExistence in packages/formula/src/validate.ts) found the members an expression reads on record / previous with a dot-only regex. It now reads them through the AST member reader that the relationship-traversal analysis uses. Every spelling that names a member gets the dot spelling's verdict, at every slot the pass judges:

There is no lint arm. @objectstack/lint's slot walk is unchanged, so os build and the object save door give the shared validator's verdict, as they already did for the dot spelling.

One reader, one parse

relationship-traversal.ts now has one lower-level reader, readRootMembers(ast, roots). It walks a parsed AST once and reports each read of a member of the named roots in source order, with the member, the next segment when there is one, and whether the read goes deeper. Both questions fold over it:

  • analyzeRelationshipTraversals(source, root) is parseCelToAst plus traversalsOf(readRootMembers(ast, [root])). Its signature and answers are unchanged (equivalence below).
  • checkFieldExistence reads readRootMembers(ast, ['record', 'previous']).
  • validateExpression parses once, lazily, and hands the same AST to the field-existence pass and to the traversal-conflict arm. That arm used to parse a second time through analyzeRelationshipTraversals.

readRootMembers and traversalsOf are module exports only. src/index.ts does not change, and dist/index.d.ts has zero mentions of either.

Measured on the as-is reuse question (dispatch mechanism assumption 2): analyzeRelationshipTraversals could answer existence as written (first segments = bareFields plus the keys of traversals; leaves = traversals.get(block)), but only one root per call, so two parses per expression, and its sets drop source order. That is why the reader was extracted rather than reused as it was.

What gets no verdict (dispatch mechanism assumption 5)

  • A computed key (record[record.kind], record[someVar]) names no member before evaluation. The reader reports no read for it, so there is no finding. This is pinned (validate-field-existence-spellings.test.ts, "gives a computed key no verdict"; validate-attached-on-read.test.ts, record.viewer[record.status]).
  • A method call on a member (record.name.startsWith('A')) still judges the member.
  • Three shapes the regex misread as a member of the root are no longer refused, because none of them names one: text inside a string literal (record.name == 'record.typo'), a root name after another root (vars.record.x), and a method call on the root itself (record.size()). Measured: the regex reported typo, x and size as unknown fields, and the AST reader reports nothing for each. This is a small widening inside the narrowing, and the changeset states it.

Premise, measured at both doors before the change (origin/main 8b713fad78, which includes 9af0005d55)

Probe: an object with fields name and tier, the expression on one slot. "build" is validateStackExpressions. "door" is runRuntimeAuthoringRules({ type: 'object' }) (the gate saveMetaItem runs). Counts are expression-invalid errors.

spelling (zz_typo undeclared) option visibleWhen build / door requiredWhen build / door validation condition build / door
record.zz_typo == 'a' (control) 1 / 1 1 / 1 1 / 1
record['zz_typo'] == 'a' 0 / 0 0 / 0 0 / 0
previous['zz_typo'] == 'a' 0 / 0 0 / 0 0 / 0
record.?zz_typo.orValue('') == 'a' 0 / 0 0 / 0 0 / 0
record[?'zz_typo'].orValue('') == 'a' 0 / 0 0 / 0 0 / 0
has(record.zz_typo) 1 / 1 1 / 1 1 / 1

After the change, every row reads 1 / 1 in all three slots. Every spelling of the declared name reads 0 / 0 before and after.

Two premise details came out differently from the card:

  • has(record.x) was already refused, because the regex saw the dot inside has(...). It is pinned so it stays refused.
  • A bare record.?zz_typo == 'a' is refused at compile (invalid-cel: cel-js finds no == overload for an optional of dyn against a string). The optional spelling that passed was record.?zz_typo.orValue(...), and that is the one the pins use.

Pins

  • packages/formula/src/validate-field-existence-spellings.test.ts (new, 31 tests):
    • 6 spellings × 2 roots refused, with code: 'unknown-field' and params;
    • the same 12 on a declared field, accepted;
    • did-you-mean in a bracket spelling;
    • one finding per name across spellings and roots, in source order;
    • a comprehension body;
    • a computed key gets no verdict;
    • a string literal is not a read;
    • no field list means no verdict.
  • packages/formula/src/validate-attached-on-read.test.ts: the pin that held "index read unjudged" is replaced. The leaf is now judged in 6 non-dot spellings, with the declared leaf as control in 4. A computed key, a method call and a third segment stay unjudged.
  • packages/formula/src/relationship-traversal.test.ts: 5 tests on readRootMembers:
    • multi-root source order;
    • leaf and deeper;
    • the three non-reads;
    • a method receiver;
    • analyzeRelationshipTraversals equals the fold of its reads.
  • packages/lint/src/runtime-gate.object-field-existence-spellings.test.ts (new, 36 tests): 3 slots (option visibleWhen, requiredWhen, validation condition) × 6 spellings. Each is refused at the build's rule table (runAuthoringRules('build')) and at the object door (runRuntimeAuthoringRules), and each assertion checks the named subject, the location and the severity. A declared field in every spelling publishes clean at both doors.

Reverse verification (committed fix first, HEAD 57efbdd5f6)

The mutation went through scripts/ablation-replace.mjs. One anchor (for (const read of readRootMembers(tree, FIELD_EXISTENCE_ROOTS)) {, 1 hit, then 0) was replaced with the old regex reader: the dot-only head regex plus the sticky dot-only second-segment regex. Blob 3fc4e3033d47 became 383b80a635d8. @objectstack/formula was rebuilt, and scripts/ablation-dist-preflight.mjs showed the marker in dist/index.js and dist/index.mjs. The lint pins resolve formula through dist/.

suite mutant (regex reader) restored
formula: validate-field-existence-spellings + validate-attached-on-read 13 failed / 28 passed 41 passed
lint: runtime-gate.object-field-existence-spellings 12 failed / 24 passed 36 passed

The direction was as predicted:

  • Red: every bracket, previous[...], .? and [?] row (3 slots × 4 in lint), the non-dot leaf spellings, did-you-mean in a bracket, source order, the comprehension case and the string-literal case.
  • Green: the dot and has() rows, and every control.

Restore was proven by ablation-replace (blob equal to HEAD, git diff HEAD empty) and by ablation-dist-preflight --absent (marker absent from all 6 built files, tree clean).

Disclosed: the first mutant run failed its DTS build on an unused constant (TS6133 FIELD_EXISTENCE_ROOTS). The JS bundles were emitted and carried the marker, and the red counts were the same. The run was repeated with the constant referenced, so the mutant build exited 0. The numbers above come from that clean run.

Equivalence of the extracted reader

The BASE version of relationship-traversal.ts was run against this branch's version over every string literal that mentions record/previous member access in the repository's packages/** and examples/** TypeScript: 1,174 files and 2,264 expressions, each analysed for both roots, 4,528 analyses in total. 2,664 analyses parsed and 1,322 were non-empty. The two versions differed 0 times, comparing traversals, bareFields and multiHopFields with their insertion order.

Population census (dispatch mechanism assumption 4)

  • Text census, over all tracked files (git grep): CEL in metadata that spells a record/previous member with a bracket, .? or [?] exists only in test fixtures. Each one names a declared field, has no field list in its hint, or goes through a path that does not run this check. That is an ObjectQL engine fixture registered straight into the registry, where the dot form of the same key also passes. No example, app template, platform object, plugin object or skills/** file uses such a spelling.
  • Differential run of validateStackExpressions:

Local verification

All readings below are at 8d07b21aac unless noted. That head is this branch with origin/main 35ef501e13 merged in. The last commit touches only the changeset file.

  • Dispatch gates, derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (no paths): 62 commands. 62 run, all exit 0. Each exit code was captured before any pipe. Reconciled with --ran: "62 derived famil(ies) accounted for — 62 run, 0 NOT-MEASURED". Three families answered PREREQUISITE NOT MET on the first pass at 57efbdd5f6 (check:docs-transcript-drift, check:dual-build-cjs-loads, check:lean-entry-closure). After pnpm --filter '@objectstack/objectql...' --filter '@objectstack/lint...' build, the final pass measured all three, exit 0. Families the tool places outside the runnable union (CI jobs, type-check lanes, the artifact-roster block) are CI's.
  • Typecheck: pnpm --filter @objectstack/formula run typecheck and pnpm --filter @objectstack/lint run typecheck exit 0. tsc -p tsconfig.test.json --listFiles includes all four touched or new test files.
  • Tests, at 501184a648. Its packages/ tree is byte-identical to 8d07b21aac.
    • pnpm --filter @objectstack/formula exec vitest run: 46 files, 1315 passed.
    • pnpm --filter @objectstack/lint exec vitest run: 131 files, 5979 passed.
    • Both were also green before the merge, at 57efbdd5f6.
  • eslint, narrowed to the 6 changed .ts files with --no-inline-config --format json: 6 files, 0 errors, 0 warnings. The population is from eslint's own config: --print-config resolves a config for each file, and none is ignored. Invariance for untouched files: eslint.config.mjs enables no type-aware linting (no parserOptions.project, no typed rules), so this diff cannot move the verdict on a file it does not touch. The full pnpm lint run is CI's.
  • CJS: require('./packages/formula/dist/index.js') loads, and validateExpression and analyzeRelationshipTraversals are functions.

Acceptance notes

  • The pending changeset of feat(spec): ObjectSchema.attachedOnRead declares per-caller read attachments, and the shared validator judges record.BLOCK.LEAF against them #22425 (.changeset/22386-validator-attached-on-read-leaf.md) says index access on a read attachment stays unjudged. This PR makes that false for unreleased text. This PR's changeset says so ("an earlier entry in this release ... describes the check before this change"), following the convention .changeset/22402-option-gate-fails-closed.md uses. The other changeset is not edited.
  • A comprehension variable that shadows a root (list.exists(record, record.x == 1)) is read as the root. The regex did the same, and so does analyzeRelationshipTraversals, so no verdict moves. It is noted, not filed: no producer in the repository writes it.
  • packages/objectql/src/validation/rule-validator.ts has its own source reading of which columns a lock predicate reads. Its comment near line 1313 treats record['x'] as reading every field, which is conservative. It was not touched or measured here.
  • The suite-duration ratchet that landed on main (scripts/check-test-suite-ceilings.mjs) has no ceilings table yet in scripts/test-shard-timings.json, so it is NOT MEASURED for @objectstack/formula / @objectstack/lint. The new lint file ran 36 tests in about 12 s locally with 2 workers on a shared box.

Generated by Claude Code

claude added 4 commits October 9, 2026 13:22
…he AST member reader

checkFieldExistence matched a dot-only regex, so record['x'], previous['x'],
record.?x and record[?'x'] reached the evaluator with no existence verdict at
every record-scoped slot. It now reads members through readRootMembers, the
one AST member reader relationship-traversal analysis is folded from, and the
attached-on-read leaf check rides the same reads.

Claude-Session: https://claude.ai/code/session_01Bw3y2DWhT9RPnrmDsNqEVG
Co-authored-by: Claude <noreply@anthropic.com>
…t for the narrowing

Claude-Session: https://claude.ai/code/session_01Bw3y2DWhT9RPnrmDsNqEVG
Co-authored-by: Claude <noreply@anthropic.com>
…eld-existence-every-spelling

Claude-Session: https://claude.ai/code/session_01Bw3y2DWhT9RPnrmDsNqEVG
Co-authored-by: Claude <noreply@anthropic.com>
… the census line

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

github-actions Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

16 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json e148ca984258c7c0d162eefd04b1f0f77c3fa9fd.

⛔ 5 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • 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 — 9 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 e148ca984258c7c0d162eefd04b1f0f77c3fa9fd → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 17f3da6b0f2effdc4ed74ff580449c064e19c17a — the merge of head 82af2c170387753f34bd476f52bafc63128b41ab into base e148ca984258c7c0d162eefd04b1f0f77c3fa9fd, 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 17f3da6b0f2effdc4ed74ff580449c064e19c17a && git checkout 17f3da6b0f2effdc4ed74ff580449c064e19c17a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e148ca984258c7c0d162eefd04b1f0f77c3fa9fd 82af2c170387753f34bd476f52bafc63128b41ab && git checkout -B drift-repro e148ca984258c7c0d162eefd04b1f0f77c3fa9fd && git merge --no-ff 82af2c170387753f34bd476f52bafc63128b41ab

node scripts/docs-audit/affected-docs.mjs --json e148ca984258c7c0d162eefd04b1f0f77c3fa9fd

⚠️ 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 e148ca984258c7c0d162eefd04b1f0f77c3fa9fd → pass the list as
args.docs, on the commit named under Which tree this was computed on.

claude added 2 commits October 9, 2026 14:49
…eld-existence-every-spelling

Claude-Session: https://claude.ai/code/session_01Bw3y2DWhT9RPnrmDsNqEVG
Co-authored-by: Claude <noreply@anthropic.com>
… in the BREAKING paragraph only

Claude-Session: https://claude.ai/code/session_01Bw3y2DWhT9RPnrmDsNqEVG
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 82af2c170387753f34bd476f52bafc63128b41ab
Local-runs: none

Inputs, all read in this act at 2026-10-09T15:28Z, and nothing else: card #22428 (body and all 7 comments — triage 6076113360, unlock 6080500613, claim 6081526953, round-0 report 6082880893, claim amendment 6082932577, patch rounds 6083268166 and 6083726436); PR #22494 (body, 7-file list, its one docs-drift comment, no reviews); the net diff of the head against its merge base with origin/main (e148ca984258c7c0d162eefd04b1f0f77c3fa9fd, the current main tip — 7 files, +507 / −92, identical to the API file list); the 34 check-runs on the head. Source on origin/main and at the head was read with git show / git grep only.

Check-runs on the head, as read: 34 of 34 completed; 31 success, 3 skipped (Build Docs, Console Pin Gate, Packed-tarball smoke (opt-in) — path-filtered or opt-in), 0 in_progress, 0 failures. The required seven are all success: Lint & Repo Gates, TypeScript Type Check, Test Core (and its six shards), Dogfood Regression Gate (and its three shards), Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard. Check Changeset (job changeset-check, which runs check-empty-changeset, check-adr-0087-registration --base and check-changeset-no-major --base on a PR) is success.

① Derived judgments

Accept set. The surface is validateExpression's field-existence pass (checkFieldExistence, packages/formula/src/validate.ts), hence every door that runs it: os build / os validate / os lint through @objectstack/lint's slot walk (validate-expressions.ts check(...) hands fields for every object-bound predicate slot in both record and flattened scope, and the formula-field value slot does the same) and the object save door (metadata-protocol's runtime-authoring-gate.ts calls runRuntimeAuthoringRules for an active item). The lint slot walk is untouched by the diff.

  1. NARROWING — an undeclared member of record / previous spelled ['f'], ["f"], .?f or [?'f'] is refused with unknown-field where it was accepted. RIGHT: the card's defect and the triage direction; pinned in formula (6 spellings × 2 roots, with declared-field controls) and at both doors × 3 slots × 6 spellings in lint.
  2. NARROWING — an undeclared leaf of a declared attachedOnRead block in those spellings (record.viewer['can_actt'], record.viewer.?can_actt, record['viewer'].can_actt, previous.?viewer.can_actt) is refused where it was accepted. RIGHT: checkAttachedLeaf now takes the same read (read.leaf) instead of a sticky dot-only regex, so the feat(spec): ObjectSchema.attachedOnRead declares per-caller read attachments, and the shared validator judges record.BLOCK.LEAF against them #22425 check judges the second segment after every head spelling; a method on the block (no leaf), a computed key and a third segment stay unjudged as before — pinned.
  3. WIDENING — text inside a string literal (record.name == 'record.typo') no longer yields unknown-field. RIGHT: a literal is not a read; the regex scanned text, the reader walks the AST.
  4. WIDENING — a root name in member position (vars.record.x) no longer yields unknown-field. RIGHT: only a bare identifier is a root (rootNameOf requires an id node), and no lint slot reaches the record through another root — every check(...) site binds record / previous as explicit bare roots (its [#8116] note), and in a record-scoped slot vars is still refused as an undeclared bare reference. The widening exists at the existence pass only.
  5. WIDENING — a method call on the root itself (record.size()) no longer yields unknown-field for size. RIGHT: it reads the root's value, not a member. A misspelt method name does not slip through: celEngine.compile runs cel-js check(), and validateExpression refuses an unregistered receiver method as cel-unknown-function on the !compiled.ok branch, which runs before the existence pass (pinned on main, validate.test.ts "a receiver call", record.x.nosuchmethod('a')).
  6. UNCHANGED — a computed key (record[someVar], record[record.name]) gets no verdict, before and after. RIGHT and pinned; asMember admits only a string-literal key.
  7. UNCHANGED — a method call on a member (record.name.startsWith('A')) still judges name. RIGHT, pinned.
  8. UNCHANGED — has(record.x) was already refused on base (the regex saw the dot) and a bare record.?x == 'a' is refused at compile. RIGHT: the first is pinned so it stays refused; the PR and the report record both as premise corrections to the card, and the optional spelling the pins use is the .orValue(...) one that did pass.
  9. UNCHANGED — the relationship-traversal arm. analyzeRelationshipTraversals(source, root) is now traversalsOf(readRootMembers(ast, [root])) with the same signature and answers, and validateExpression's conflict arm folds the same AST instead of parsing a second time. RIGHT: the fold is pinned equal to the old answer in relationship-traversal.test.ts, the dev measured 0 differences on 4,528 analyses, and every consumer on main (lint validate-expressions.ts :1451 / :1909, objectql rule-validator.ts :3782 / :3870 / :4104) calls the unchanged export.
  10. UNCHANGED — the new early return on a null AST is unreachable on the path that calls it: checkFieldExistence runs only after celEngine.compile accepted the source, and compile and parseCelToAst share buildEnv, DEFAULT_LIMITS and rewriteNullableTernary (compile only adds check()), so an accepted source always parses. RIGHT: no hidden widening.
  11. UNCHANGED — refusal text, params (field, objectName, suggestion, block, leaves), one finding per name (seen), the did-you-mean (nearest). RIGHT. The order of several findings is now AST order, pinned as source order on &&; no consumer keys on order.
  12. UNCHANGED, pre-existing — a comprehension variable that shadows a root (list.exists(record, record.x == 1)) is read as the root by the old regex, the new reader and the traversal analysis alike. Not moved by this diff; carried to ③.

Public surface.

  1. @objectstack/formula's barrel src/index.ts is byte-identical at the head and re-exports relationship-traversal by name (analyzeRelationshipTraversals, findTraversalConflicts, DEFAULT_TRAVERSAL_ROOT and three types); the package exports map is . only. So readRootMembers, traversalsOf and RootMemberRead are module-internal, as the PR says. RIGHT: no new public export, and no changeset claim is owed for one.
  2. ExprSchemaHint.fields / .attachedOnRead change doc comments only; the type is unchanged. isRootId, RECORD_REF_RE and SECOND_SEGMENT_RE were module-private and are removed; checkFieldExistence / checkAttachedLeaf are private and change signature. RIGHT: nothing published moves.
  3. @objectstack/lint gains one test file; its published source is unchanged. RIGHT: no lint changeset is owed.
  4. None of the 7 paths is a governed surface (Governed Surface Queue Guard success); the two merges of origin/main add nothing to the net diff. No hand-written page under content/docs states the dot-only rule (grep on main), so no page advertises the old narrower claim.

② Semver level

  • Changeset .changeset/22428-field-existence-every-spelling.md: "@objectstack/formula": minor, summary fix(formula)!: …, a BREAKING paragraph stating both directions (the narrowing of 1–2, the widening of 3–5), a Remedy, and an adr-0087 marker reading not-required (no-migration-prescription). This matches what the diff publishes: formula is the only package whose published source moves, and the fixed group carries the rest in lockstep.
  • Level: yes takes at least minor; (narrowing) is BREAKING, carried by the banner, the ! and the arm rather than by the level, under the launch-window convention check-changeset-no-major.mjs records (no major before GA). minor is the right level. The ADR-0087 category fits: no authorable key, export or stored shape moves, nothing is rewritten on read, and the body carries no FROM / TO block — the remedy is "declare the field or fix the typo", the same shape 22402-option-gate-fails-closed.md ships under this category. The Check Changeset run on the head is the gate verdict for all three changeset gates.
  • Clause-②: line — yes (narrowing) in the changeset, on PR body line 2, and in claim amendment 6082932577; all three agree. RIGHT: the diff both narrows (1–2) and widens (3–5) the accept set; lifting a false refusal still widens what the build and the door accept, and yes (narrowing) is the gate's own spelling for a diff that widens AND narrows (its self-test fixture), read as breaking. The triage's Clause-②: no was superseded by the amendment on the evidence of 3–5.
  • The earlier pending entry 22386-validator-attached-on-read-leaf.md ("Index access … stay unjudged") is now false for unreleased text; this changeset says so in the form 22402-… established, and the other file is correctly left unedited.

③ Boundary flags

Round-0 report (6082880893) — open_questions: none.

  • deviation: origin/main merged before pr_create, message amended before the first push, no force-push — fine (discipline §10; the branch is unshared).
  • deviation: save-door pins sit at @objectstack/lint's runRuntimeAuthoringRules, not at metadata-protocol saveMetaItem — answered: that is the gate the door runs (runtime-authoring-gate.ts calls runRuntimeAuthoringRules for an active item), and the door-to-validator seam is already pinned by the standing object-save-door entries; acceptable.
  • deviation: showcase census loaded objects / actions / flows / views / pages directly (connector plugins unbuilt) — answered: disclosed in PR and changeset as that subset; the tracked-file text census covers the rest.
  • deviation: first ablation mutant failed its DTS build (TS6133), rerun clean — answered: disclosed, counts identical on both runs.
  • deviation: PR footer in AGENTS.md's session-URL form — correct.
  • deviation and out-of-scope note: has(record.x) already refused on base; bare record.?x refused at compile — answered: premise corrections, pinned (judgment 8).
  • out-of-scope: a comprehension variable that shadows a root — ESCALATED to the seat, non-blocking: pre-existing and unchanged by this diff (all three readers agree), 0 in-repo producers; whether it is filed as an authoring-trap card is the seat's call under Prime Directive 10, not a condition of this PR.
  • out-of-scope: the 22386-… changeset sentence now false before release — answered in ②.
  • out-of-scope: rule-validator.ts reads record['x'] as every field for a lock predicate — answered: conservative (fail-closed), untouched, not a defect of this card.
  • PR acceptance note: suite-duration ceilings not yet measurable for formula / lint — answered: Test Core on the head is success.

Claim amendment (6082932577): Clause-② corrected to yes (narrowing); a contract review at tier owed before the queue — this record is that review.

Patch round 1 (6083268166):

  • open question 1 (which Clause-② spelling fits widens-and-narrows): answered — option B adopted in round 2; the changeset reads Clause-②: yes (narrowing), verified in the diff.
  • open question 2 (the three-shape bullet under Unchanged): answered — option B adopted in round 2; the bullet is gone and the widening lives in the BREAKING paragraph, verified in the diff.
  • deviation: check:dual-build-cjs-loads NOT MEASURED at 071050a — resolved in round 2, and Lint & Repo Gates is success on this head.
  • deviations: the first gate pass read the committed HEAD and was rerun; worktree recreated — fine.

Patch round 2 (6083726436) — open_questions: none.

  • deviation: origin/main (e148ca9842) merged as 7d3edf6, bringing AGENTS.md / .claude/** edits into the branch's history — answered: the net diff against main is the 7 paths and touches no governed surface; the queue guard concurs.
  • deviations: standing union re-run after a turbo-cached build; worktree recreated — fine.

Also read: all 7 branch commits end with the model-free trailer pair; no model identifier in the PR title, body, changeset or commits; the branch matches the Claim: and the PR's own card-claims check is success; the PR is draft with auto-merge unarmed, the right state before this record.

Implemented-by: claude/issue-22428-field-existence-every-spelling
Reviewed-by: session_01Bw3y2DWhT9RPnrmDsNqEVG

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 9, 2026 15:32
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 9, 2026 15:32
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 9, 2026
Merged via the queue into main with commit 94379f4 Oct 9, 2026
36 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-22428-field-existence-every-spelling branch October 9, 2026 16:13
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

2 participants