Skip to content

fix(lint): validateStackExpressions reads an object's fields through the guarded reader instead of an inline cast (#15742) - #15791

Merged
os-zhuang merged 6 commits into
mainfrom
claude/issue-15742-validate-expressions-non-record-field
Sep 7, 2026
Merged

fix(lint): validateStackExpressions reads an object's fields through the guarded reader instead of an inline cast (#15742)#15791
os-zhuang merged 6 commits into
mainfrom
claude/issue-15742-validate-expressions-non-record-field

Conversation

@claude

@claude claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Fixes #15742

buildFieldIndex in packages/lint/src/validate-expressions.ts cast every member of an object's fields: list inline, so an empty YAML list item — which deserialises to null — threw TypeError: Cannot read properties of null (reading 'name') out of the whole rule, before the .filter two calls later could drop it:

if (Array.isArray(fields)) names = fields.map(f => (f as AnyRec).name).filter((n): n is string => typeof n === 'string');

Array.isArray proves the LIST, never its MEMBERS. The list is now read through recordsOf (./object-graph.js), the single home of that coercion — no new copy, so collection-coercion-single-copy.test.ts counts what it counted before:

if (Array.isArray(fields)) names = recordsOf(fields).map(f => f.name).filter((n): n is string => typeof n === 'string');

The map branch keeps Object.keys: on that shape the author's KEY is the field name, which is what this "did you mean?" index needs, and recordsOf's map branch would let an inner name override it. That is a different question and is deliberately left where it was.

The sibling readers, and the finding this now raises

Both sibling field readers in the same file already guard, and both drop such a member in silence — no finding. Quoted from validate-expressions.ts as they stand on main:

// buildFieldTypeIndex
for (const f of fields as AnyRec[]) {
  const fn = (f as AnyRec)?.name;
  const ft = (f as AnyRec)?.type;
  if (typeof fn === 'string' && typeof ft === 'string') types[fn] = ft;
}
// fieldEntries
return (fields as AnyRec[])
  .filter((f) => f && typeof f === 'object' && typeof f.name === 'string')
  .map((f) => [f.name as string, f] as [string, AnyRec]);

recordsOf makes the third reader agree with them: an array member that is not a record carries no author-written name, so there is nothing to report about it and it is dropped whole. The crash becomes silence, not a finding — measured, not assumed: the sweep's second arm (invents no finding about the entry no author wrote) now counts validateStackExpressions for these two keys instead of skipping it, and passes with RESIDUAL_INVENTED unchanged.

The sweep rows removed

packages/lint/src/non-record-object-entry.test.ts is exact in both directions, so the rows had to go in this PR:

 const RESIDUAL_THROWS = {
-  'objects[].fields · null': ['validateStackExpressions'],
-  'objects[].fields · undefined': ['validateStackExpressions'],
 };

(the declaration keeps its real Readonly record type in the file; the type is elided here only to keep the quote free of angle brackets)

RESIDUAL_THROWS is now empty and its docblock says so as a measurement, listing the two rows that have come out (stack.datasets via #15741, objects[].fields here). RESIDUAL_INVENTED is unchanged — its one row (stack.agents · an array) is about the agent readers, not this one, and the suite stays green with it in place.

Two focused arms were added to validate-expressions.test.ts for what the rule does INSTEAD of crashing, which a crash-only sweep cannot say: the junk member is silent, and the readable siblings are still indexed (a record.amont typo on that object still draws its did-you-mean finding — the failure mode a bare try/catch repair would have produced).

Ablation (trap-guarded, blob-hash restore)

The tests import the source through a relative specifier (./validate-expressions.js from inside src/), so vitest loads the TypeScript source — there is no dist leg in this ablation and no rebuild is needed between the legs. The mutation was proven on disk before the run (anchor counts), and the restore proven by blob hash plus an empty git diff HEAD.

leg on-disk proof run result
mutate: recordsOf(fields) → the old inline cast guarded-count=0 cast-count=1; blob 6e7de66d2091df6b1b67ca86ab2924d90e40670d (HEAD blob a4cfe89a0435564cd51450666eafd7260c8c6621) sweep + expressions tests command-exit 1Tests 6 failed | 576 passed (582)
restore (git checkout HEAD -- PATH, pinned to HEAD, never a bare checkout) restored-blob=a4cfe89a0435564cd51450666eafd7260c8c6621, git diff HEAD empty full package test command-exit 0Test Files 97 passed (97), Tests 3324 passed | 5 skipped

The mutated leg reds naming the rule, in both arms and both shapes:

FAIL src/non-record-object-entry.test.ts > … > objects[].fields > with null > throws out of no rule but the ones still filed as broken
AssertionError: validateStackExpressions: Cannot read properties of null (reading 'name'): expected [ 'validateStackExpressions' ] to deeply equal []
FAIL src/non-record-object-entry.test.ts > … > objects[].fields > with undefined > …
AssertionError: validateStackExpressions: Cannot read properties of undefined (reading 'name'): expected [ 'validateStackExpressions' ] to deeply equal []

Verification

All heavy runs went through scripts/pm/os-verify-lock.sh (OS_VERIFY_LOCK_SLOT=issue-15742); exit codes captured before any pipe, verdict lines quoted from the gates themselves.

run verdict
pnpm --filter '@objectstack/lint^...' build VERDICT command-exit 0 · held the lock 228s
vitest run src/non-record-object-entry.test.ts src/validate-expressions.test.ts src/collection-coercion-single-copy.test.ts VERDICT command-exit 0Test Files 3 passed (3), Tests 588 passed (588) (586 before the two new arms)
pnpm --filter @objectstack/lint test VERDICT command-exit 0Test Files 97 passed (97), Tests 3324 passed | 5 skipped (3329)
pnpm --filter @objectstack/lint typecheck VERDICT command-exit 0check:test-typecheck: OK — @objectstack/lint's test layer compiles under packages/lint/tsconfig.test.json (so the new arms ARE typechecked)
node scripts/pm/dispatch-gates.mjs --changed --commands --repo objectstack-ai/objectstack EXIT=0, 53 commands, derived at fd82de279 after merging origin/main (the first derivation warned STALE TREE; the merge cleared it)
all 53 derived commands 50 × EXIT=0, 3 × EXIT=3 (see NOT MEASURED)
pnpm check:pm-dispatch-gates EXIT=0✓ dispatch-gates self-test: 1478 cases pass.
pnpm check:nul-bytes EXIT=0; plus a direct control-byte scan of the four changed paths (grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]', no match)
node scripts/pm/check-governed-merges.mjs --test (the 4 changed paths) EXIT=0✅ NOT governed — ordinary queue landing applies to a PR with exactly this file list.
pnpm lint (eslint . --no-inline-config, repo-wide) EXIT=0 — the whole-repo run, so no narrowing to declare

NOT MEASURED, by name

  • pnpm check:dual-build-cjs-loadsNOT MEASURED (exit 3: PREREQUISITE NOT MET — reads built output; 84 packages have no dist/, needs a whole-repo pnpm build). Its own words: "⛔ This is NOT a pass: nothing was measured."
  • pnpm check:type-check-debtNOT MEASURED (exit 3: PREREQUISITE NOT MET — needs the full turbo run build closure over ./packages/*). No ledger number is read from this run in either direction.
  • pnpm check:docs-transcript-drift — first run exit 3 (@objectstack/lint not built; the closure build is ^..., which excludes the package itself). Re-run after pnpm --filter @objectstack/lint build through the lock: EXIT=0✓ check-docs-transcript-drift: 4 declared transcript value(s) across 405 page(s) … equal what the registry derives today. Measured.
  • Local package scope is narrowed and declared: turbo ls --affected (base 7dafaaedd) names 49 packages, because @objectstack/lint is a wide dependency. Only @objectstack/lint was run locally (test + typecheck, both green). The 48 downstream packages consume @objectstack/lint's exports, and this change alters no export, signature or type — git grep validateStackExpressions -- ':!packages/lint' finds only changelogs, docs and ADR prose, no consumer test. CI runs the farm.

Out of scope, and it could not be filed

The card body flagged two more inline casts in this file (flow.nodes, graph.nodes) as unmeasured at filing time. They were measured on this branch, i.e. WITH the fix above, by calling the rule directly:

flow.nodes [null]   : THREW Cannot read properties of null (reading 'type')
flow.nodes ["str"]  : OK, 0 finding(s)
control: clean flow : OK, 0 finding(s)

This is a real, separate defect and is not repaired here: no sweep can express a flow's inner node list today (non-record-object-entry.test.ts drives collections), and for graph.nodes the producer is collectFlowGraphs in @objectstack/spec/automation, so the contract-first question of where the repair belongs needs a ruling rather than a second consumer-side guard. Attempting to file it as a new card was refused in this box (the issue-creation call was blocked by the permission classifier, and a dedup search over open + closed issues found no existing card), so the full text is handed to the PM in the os-dev-report comment for filing. #15742 is the only card this PR addresses.


🤖 Generated with Claude Code

https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk


Generated by Claude Code

…dFieldIndex

`buildFieldIndex` in `validate-expressions.ts` cast every member of an
object's `fields:` list inline (`fields.map(f => (f as AnyRec).name)`).
`Array.isArray` proves the LIST, not its MEMBERS: an empty YAML list item
deserialises to `null`, and the dereference threw out of the whole rule
before the `.filter` two calls later could drop it.

The list is now read through `recordsOf` — the single home of that
coercion — which drops a non-record array member whole and in silence,
the same disposition the two sibling readers in this file
(`buildFieldTypeIndex`, `fieldEntries`) already had. The map shape keeps
`Object.keys`: there the author's key IS the field name.

The sweep's `RESIDUAL_THROWS` rows for `objects[].fields` come out in the
same change — it is exact in both directions, so it now asserts the throw
is gone. `RESIDUAL_INVENTED` is unchanged, measured: the repaired reader
raises no finding about the dropped member.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
They were appended inside the `#15137` assignment-value describe, which
reads as a claim about that suite rather than about `buildFieldIndex`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation tests tooling labels Sep 5, 2026
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to listnot a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 5 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 ffca0df9cb09cd61410b92f3d952226b309a58c8packageMentionDocs.

Which tree this was computed on

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

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

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

@claude

claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

ACCEPT — PR #15791 (head fd82de279, 4 files: validate-expressions.ts +12/−1, the sweep −2 rows, validate-expressions.test.ts +39, changeset) reviewed against the GitHub three-dot diff, not the report.

As ruled: the inline cast at buildFieldIndex becomes recordsOf(fields).map(f => f.name) (the guarded reader; the map branch's Object.keys deliberately left — there the author's KEY is the name); the two RESIDUAL_THROWS rows for objects[].fields · null / undefined are removed so the sweep now COUNTS validateStackExpressions for those keys, RESIDUAL_INVENTED unchanged (measured); the crash becomes silence, matching the two sibling readers in the file, and the PR says so. Changeset @objectstack/lint patch present.

Measured by this seat: git merge-tree --write-tree clean against origin/main AND against PR #15789's branch (same package, disjoint files); no coercion copy introduced; governed test per the report NOT governed. NOT MEASURED here: the lint suite (the dev's locked runs Tests 588 passed (588) on the three files, 3324 passed | 5 skipped package, typecheck OK, ablation 6 failed | 576 passed → restored, repo-wide pnpm lint exit 0 are the record); the two whole-workspace-build gates by name.

The unfiled finding (flow nodes inner-list inline casts, same class, unreachable by the sweep) is filed by this seat — see the card note. Serial note: #15789 and this PR both sit in packages/lint; both merge-tree clean either order.

Flip + arm when both required jobs report success on fd82de279. On landing #15742 closes.


Generated by Claude Code

@claude

claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

CI note — Test Core (6/6) red on fd82de279 is not this PR's; one re-run requested. The failing job is @objectstack/plugin-auth src/durability-swallow-repair.test.ts:673Error: Test timed out in 10000ms on a shard whose imports alone took 499 s (Duration 302.19s (transform 79.17s, import 499.82s …)); 2009 of 2010 plugin-auth tests passed and every other package on the shard was green. This PR touches packages/lint only (validate-expressions.ts, two test files, a changeset); plugin-auth has no import edge to it, and the timed-out test exercises the admin-audit swallow repair (#12981), untouched here. The same shard is green on main 16978315c and on the other queued heads. Both required jobs are what gate the merge (TypeScript Type Check = success; Lint & Repo Gates running); the flip waits for them. Re-run of the failed job requested via the Actions API — if it reds again on this exact commit it is recorded here as a loaded-runner timeout, not re-run a second time.


Generated by Claude Code

@claude

claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

Correction: the re-run request was refused — POST /actions/jobs/101275196882/rerun → HTTP 403 Resource not accessible by integration (this seat's token cannot re-run jobs). No re-run was made and none is attempted again. The red stands on record as a loaded-runner timeout in @objectstack/plugin-auth, a package this diff does not touch; the two required jobs decide the flip.


Generated by Claude Code

@baozhoutao
baozhoutao marked this pull request as ready for review September 5, 2026 08:25
@baozhoutao
baozhoutao enabled auto-merge September 5, 2026 08:26
@claude

claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

Flipped ready + auto-merge enabled (08:26Z, method: MERGE). Required jobs on fd82de279: Lint & Repo Gates = success, TypeScript Type Check = success; git merge-tree --write-tree origin/main <branch> → EXIT=0 against the current main. The non-required Test Core (6/6) red stands as recorded above (plugin-auth shard timeout, not this diff's). Watched; on landing #15742 closes.


Generated by Claude Code

…cord-field to re-run CI on a fresh base

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
@claude

claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

Correction + action (09:2xZ) — Test Core IS a required context; the branch was merged with main to re-run it.

The 08:26Z flip note said "the two required jobs decide the flip". That was wrong. The ruleset on main (GET /repos/…/rules/branches/main, read this tick) lists seven required status checks: TypeScript Type Check, Test Core, Dogfood Regression Gate, Build Core, Temporal Conformance (live PG + MySQL), Lint & Repo Gates, Governed Surface Queue Guard — a skipped run passes, a failed one blocks. So with Test Core (6/6) red on fd82de279 this PR sat at mergeable_state: blocked with auto-merge on and never entered the merge queue; it would not have landed on its own.

What was done: no re-run means exist (403 on record above), and an empty commit is not allowed, so origin/main (3508869ff, which has moved 12 merges since the PR's base) was merged into the branch as a plain merge commit — head is now 6e6d3f8c8. The three-dot diff against main is unchanged (still the 4 reviewed files, +75/−15 in the merge's own stat is the main side); the ACCEPT stands. Auto-merge survived the push (auto_merge still set). If Test Core (6/6) reds again on the fresh head, the failure is treated as this PR's to root-cause, not a runner timeout.

Seat-wide: the "two required jobs" reading in this lane's flip notes is retired from this tick on — the flip gate is all seven contexts non-failing.


Generated by Claude Code

@claude

claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

Correction to the note above: 4 files changed, 75 insertions(+), 15 deletions(-) is the three-dot diff origin/main...6e6d3f8c8 itself — i.e. the PR's own four reviewed files, unchanged by the merge — not "the main side". Nothing else in that note changes.


Generated by Claude Code

@baozhoutao
baozhoutao added this pull request to the merge queue Sep 5, 2026
@claude

claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

Fresh head 6e6d3f8c8: all seven required contexts greenTest Core success (the earlier (6/6) red did not recur, consistent with the recorded runner timeout), TypeScript Type Check, Lint & Repo Gates, Build Core, Dogfood Regression Gate, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard all success; no other red. The PR now reads auto_merge: null / mergeable_state: unknown — in the merge queue. Watched; on landing #15742 closes.


Generated by Claude Code

@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Sep 5, 2026
@claude

claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

Merge conflict (10:31Z)mergeable_state: dirty against origin/main 53d02e91c: git merge-tree --write-tree reports CONFLICT (content): Merge conflict in packages/lint/src/validate-expressions.test.ts (the other side is #15792's 9408b7f02, which added tests to the same file; validate-expressions.ts auto-merges). GitHub dropped auto-merge on the transition. A dev is merging origin/main into the branch as a plain merge commit, keeping both sides' tests — no rebase, no force-push. The seat re-checks the three-dot diff (must still be the four reviewed files), re-runs the seven-context wait on the new head, then re-flips/re-arms.


Generated by Claude Code

…cord-field (resolve validate-expressions.test.ts with #15792)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk

Copy link
Copy Markdown
Contributor

merge-conflict-resolved

New head 408c471761acb127cd4b5e5ef1dfaced10eff102 (408c47176), previous head 6e6d3f8c8. One plain git merge origin/main merge commit on top of the published history — no rebase, no amend, no force-push; a normal git push. origin/main at merge time: 6c439f2b4.

The conflict

git merge origin/main reported exactly one:

Auto-merging packages/lint/src/validate-expressions.test.ts
CONFLICT (content): Merge conflict in packages/lint/src/validate-expressions.test.ts
Auto-merging packages/lint/src/validate-expressions.ts

One hunk, at end of file: both sides append a new top-level describe after the same last line of assignment value envelope — located findings (#15137). The other side is 9408b7f02 on main, "service-automation: refuse a structural flow condition that is neither CEL text nor an expression (#15792)", which also widened this file's @objectstack/spec/automation import to carry STRUCTURAL_CONDITION_SHAPE_REFUSAL.

Resolution — both sides kept, verified as a strict union

Not resolved by hand-editing between the markers. The three merge stages were extracted (git show :1: / :2: / :3:) and the composition was measured:

  • base (3796 lines) is a strict prefix of ours (3835 lines) — head -n 3796 ours | diff - base exit 0;
  • base is not a prefix of theirs (3922 lines): theirs is base + the import widening at line 14 + its own block;
  • so the resolved file is theirs, with our 39-line block appended (3961 lines).

Proof that nothing was dropped from either side:

diff theirs.ts resolved.ts   ->  39 added lines, 0 removed lines
diff ours.ts   resolved.ts   ->  the ONLY removed line is
  import { ASSIGNMENT_VALUE_ENVELOPE_REFUSAL, PREDICATE_SLOT_STRING_REFUSAL } from '@objectstack/spec/automation';
  (replaced by #15792's multi-line form, which keeps both names and adds STRUCTURAL_CONDITION_SHAPE_REFUSAL)

Both blocks are present in the merged file:

3820:describe('structural condition shape (#15662)', () => {          <- #15792
3924:describe("validateStackExpressions — a non-record entry ... (#15742)", () => {   <- this PR

packages/lint/src/validate-expressions.ts took the auto-merge as-is — no hand edit. This PR's recordsOf(fields) read survives (now line 152), and #15792's structuralConditionRefusal import plus its checkStructuralCondition arm arrived unchanged. Nothing in that file was chosen by this seat.

Verification (exit codes captured before any pipe; verdict lines quoted)

1. Scope unchanged — still this PR's 4 files. git diff origin/main...HEAD --stat, exit 0:

 ...-validate-expressions-non-record-field-entry.md | 11 ++++++
 packages/lint/src/non-record-object-entry.test.ts  | 28 ++++++++--------
 packages/lint/src/validate-expressions.test.ts     | 39 ++++++++++++++++++++++
 packages/lint/src/validate-expressions.ts          | 12 ++++++-
 4 files changed, 75 insertions(+), 15 deletions(-)

Identical line counts to the pre-merge diff, so the merge added no content of its own to the PR's delta.

2. No conflict markers. grep -n '^' with the three marker patterns over packages/lint/src/validate-expressions.test.ts → no match, exit 1. A control-character scan of the same file (grep -naP over the check:nul-bytes class) also returns no match, exit 1.

3. Dependency closure build (fresh worktree, pnpm install --offline exit 0), through the lock as OS_VERIFY_LOCK_SLOT=issue-15742:

os-verify-lock: VERDICT command-exit 0 · held the lock 161s (2m41s) · waited 0s

4. Vitest, the conflicted file plus this PR's sweep filepnpm --filter @objectstack/lint exec vitest run src/validate-expressions.test.ts src/non-record-object-entry.test.ts through the same lock slot:

 Test Files  2 passed (2)
      Tests  592 passed (592)
os-verify-lock: VERDICT command-exit 0 · held the lock 10s · waited 0s

Both sides' new arms are inside that run: the file is the merged one, and STRUCTURAL_CONDITION_SHAPE_REFUSAL resolving at all is what makes it load.

5. Typecheckpnpm --filter @objectstack/lint typecheck (tsc --noEmit && pnpm check:test-typecheck), same lock slot:

check:test-typecheck: OK — @objectstack/lint's test layer compiles under packages/lint/tsconfig.test.json; 2 file(s) / 6 error(s) / 2 pinned signature(s) held in test-typecheck-debt.json
os-verify-lock: VERDICT command-exit 0 · held the lock 17s · waited 0s

6. Mergeable again. After a fresh git fetch origin main (which had moved on again, to d701e657d): git merge-tree --write-tree origin/main HEAD → exit 0, tree 1da0a2a19. Clean against the newest main, not only against the one merged.

Not measured, by name

  • pnpm --filter @objectstack/lint test (the full 97-file package suite) — not re-run after the merge; the two files the merge touched were run instead. CI runs the suite.
  • The dispatch-gates command family, check:pm-dispatch-gates, check:nul-bytes as gate runs, and repo-wide pnpm lint — not re-run for this merge; they were green on 6e6d3f8c8 and the merge added no new content to the PR's delta (reading 1). CI runs the farm.

Labels, assignee, draft state and auto-merge untouched.


Generated by Claude Code

@claude

claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

Conflict resolved on record (10:4xZ) — head 408c47176 (plain merge of origin/main 6c439f2b4, both sides' tests kept as a strict union; validate-expressions.ts auto-merged untouched). Seat re-check: the three-dot diff is unchanged — the same 4 files, +75/−15; git merge-tree --write-tree origin/main 408c47176 → EXIT=0 against d701e657d (dev's reading). The seven-context wait runs on the new head; re-flip is not needed (the PR stayed ready), auto-merge is re-enabled when all seven read non-failing.


Generated by Claude Code

@claude

claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

CI red on 408c47176Test Core (6/6) is NOT this PR's; standing down on it once, with the one allowed re-run.

Same test as on fd82de279: packages/plugins/plugin-auth/src/durability-swallow-repair.test.ts:673Error: Test timed out in 10000ms — on a shard whose vitest import phase read 470.90s (Duration 283.67s (transform 76.25s, setup 0ms, import 470.90s, tests 345.99s …)); Tests 1 failed | 2009 passed (2010), every other package on the shard green, check-test-completeness: OK (8 of 8 … 9192 test(s) declared and all accounted for). The other six required contexts on this head are success.

Why it is not this PR's, read from the tree: the PR changes packages/lint only; packages/plugins/plugin-auth/package.json carries no @objectstack/lint dependency and nothing under plugin-auth/ imports it; Test Core (6/6) is green on origin/main at fd3333738, d701e657d, cc5b3dd0c and 5a21d73af. The failure's shape — a dynamic await import('./admin-import-users.js') as the test's first statement, charged to the test's own 10 s budget on a cold shard — is filed as #15852 for the owning lane; making it robust means editing plugin-auth, outside this card's ruling, so it is not pushed here.

Action: ONE re-run of the failed jobs on run 33961266872 (MCP rerun_failed_jobs), as the seat's standing rule allows after a standing-down note. If shard 6 reds a second time on this head, the seat asks the maintainer rather than re-running again. Auto-merge is re-enabled only when all seven required contexts read non-failing.


Generated by Claude Code

@claude

claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

Third red — no more re-runs; this needs a maintainer call.

The re-run of Test Core (6/6) on 408c47176 (job 101296736161, started 11:08Z) failed on the SAME test again: src/durability-swallow-repair.test.ts:673 Error: Test timed out in 10000ms, Tests 1 failed | 2009 passed (2010), Duration 291.39s (transform 73.19s, setup 0ms, import 463.09s, tests 376.45s …), Cached: 16 cached, 44 total. Three runs on this PR's two heads, three identical timeouts; four recent main commits green on the same shard. Test Core is a required context, so this PR cannot enter the merge queue until shard 6 is green on its head.

What the seat now believes, stated as a hypothesis: the PR changes packages/lint, so @objectstack/lint's own test task is uncached on this PR and runs on shard 6 alongside plugin-auth (Cached: 16 on every PR run); the extra load is what pushes that test's first-statement dynamic import('./admin-import-users.js') over its 10 s budget. Main's runs have lint cached, so the shard is lighter. That would make the red "caused" by this PR only through scheduling, not through any code path — plugin-auth has no @objectstack/lint edge.

Options for the maintainer (os-zhuang / hotlong):

Until one of these happens, #15791 stays ready and un-armed (auto-merge is not re-enabled on a red required context) and the seat keeps it watched. Card #15742 is pm:dispatchedpm:awaiting-maintainer for this decision.


Generated by Claude Code

This was referenced Sep 6, 2026

Copy link
Copy Markdown
Contributor

os-dev-report

Resumed patch round — merged head pushed, CI green, queue armed

The previous seat was cut off by a rate limit after merging origin/main: the merge
commit 2829da4e4 existed on disk but had never been pushed, so CI was still judging the
pre-merge tip. Nothing was redone — the worktree was verified clean on that same commit
and that commit was pushed.

  • push: 408c47176..2829da4e4, plain git push -u origin, fast-forward, no force
    (the old tip 408c47176 is HEAD's first parent; git merge-base --is-ancestor exits 0).
  • PR diff unchanged: 4 files, +75 / -15, three-dot against merge base b1b978c8d.

1. The #15852 robustness fix IS on the merged tree

Confirmed by ancestry and by reading both trees — not recalled.

Landing commit f1e91595f, "test(plugin-auth): load the batch-6 admin endpoint graphs at
module top, not inside each clocked case (#15914)".

  • git merge-base --is-ancestor f1e91595f HEAD exits 0 — on the merged tree.
  • The same test against the old pushed tip exits 1 — it was not on what CI last ran.
    That negative reading is controlled: the repo is not shallow
    (--is-shallow-repository false) and a control commit of at least equal depth, known to
    be in that ref's history, exits 0 on the same ref.

This is exactly why the old head was red. The only failing job on 408c47176 was
Test Core (6/6), and its log names the case and the line:

FAIL src/durability-swallow-repair.test.ts  ... batch 6 ...
  admin-import-users :: the run-level audit row
  a refused run-level row is reported, and says the per-row trail survived
Error: Test timed out in 10000ms.
  at src/durability-swallow-repair.test.ts:673:5

Reading line 673 on each tree closes the loop:

  • old pushed tip — that it() body carries
    const OBJ = await import('./admin-import-users.js');, the in-body dynamic import that
    charged the cold transform-and-import of admin-import-users.ts (which pulls in
    @objectstack/rest) to the case's own 10 000 ms testTimeout;
  • merged tree — that case has moved to line 696 and its body has no import at all.
    runAdminCreateUser and runAdminImportUsers are imported at module top; the only
    remaining await import( matches in the file are inside the explanatory comment.

Every other check on the old head was already green, including the other five required
contexts.

2. Gate verdicts at the merged head

Heavy runs went through scripts/pm/os-verify-lock.sh
(OS_VERIFY_LOCK_SLOT=issue-15742-lint). Exit codes captured before any pipe; verdicts
quoted from the gates' own output.

run verdict
pnpm --filter '@objectstack/lint^...' build VERDICT command-exit 0 — held 199s, waited 270s
pnpm --filter @objectstack/lint test VERDICT command-exit 0Test Files 100 passed (100), Tests 3483 passed | 5 skipped (3488)
pnpm --filter @objectstack/lint typecheck VERDICT command-exit 0check:test-typecheck: OK — @objectstack/lint's test layer compiles under packages/lint/tsconfig.test.json
dispatch-gates.mjs --changed --commands --repo objectstack-ai/objectstack EXIT=0, 55 commands (the pre-merge round derived 53)
the 55 derived commands 55 x EXIT=0 — zero NOT MEASURED remaining (see below)
pnpm exec turbo run build --filter='./packages/*' --filter='./packages/*/*' VERDICT command-exit 0Tasks: 72 successful, 72 total
check-governed-merges.mjs --test on the final 4-path list EXIT=0NOT governed — ordinary queue landing applies to a PR with exactly this file list.
control-byte scan of the 4 changed paths grep -naP exit 1, no match

The stale-tree warning, measured rather than waved through

The derivation printed STALE TREE: origin/main had moved 14 commits past this branch's
merge base, and 11 files the derivation reads had changed across that range. Rather than
accept "a well-formed answer about a tree nobody is on", the same 4 paths were re-derived
in a throwaway detached worktree at origin/main (f2f6684cd).

Both trees derive the identical 55-command set — zero added, zero dropped. The drift
did not touch this card's gate families. The scratch worktree was removed.

The three exit 3 gates were driven to a real measurement

exit 3 is PREREQUISITE NOT MET — not a pass and not a finding. None was left there.

  • pnpm check:docs-transcript-drift — exit 3 (@objectstack/lint not built; the closure
    build is ^..., which excludes the package itself). After
    pnpm --filter '@objectstack/lint...' build: EXIT=04 declared transcript value(s) across 403 page(s) ... equal what the registry derives today.
  • pnpm check:dual-build-cjs-loads — exit 3 (reads built output, 84 packages had no
    dist/). After the full 72-task workspace build: EXIT=0104 published require entry point(s) across 67 package(s) load; 620 emitted CommonJS file(s) parse.
  • pnpm check:type-check-debt — exit 3 (needs the full turbo run build closure). After
    the same build: EXIT=05 ledger entr(ies) re-measured in 172.1s, 55 raw tsc error(s) total, none above its recorded number, surplus: none.

3. CI on the new head 2829da4e4 — all six required contexts green

required context conclusion
Lint & Repo Gates success
TypeScript Type Check success
Test Core success
Dogfood Regression Gate success
Build Core success
Temporal Conformance (live PG + MySQL) success

Test Core (6/6) — the shard that timed out on the old head — is success on this one,
which is the fix confirmed end-to-end rather than argued. Nothing in the run is a failure;
the only non-success conclusions are skipped (Build Docs, Console Pin Gate,
Packed-tarball smoke (opt-in)), which are path-filtered off for this diff.

4. Queue action

The diff touches no governed surface (verified against GOVERNED_SURFACES on the final
file list, not recalled), the PR was already non-draft, and GitHub reported
mergeable_state: clean. Auto-merge is armed, so the merge queue rebuilds this PR onto
current main, re-runs the subscribing workflows on that rebuilt generation, and lands it
only if the required set passes.

One thing for whoever watches it land: origin/main moved 14 commits past this branch's
merge base while this round ran. That is the queue's job to integrate, not a reason to
re-merge locally — but if the queue ejects the entry on a collateral failure, that drift is
the first place to look.


Generated by Claude Code

Merged via the queue into main with commit 434ca2d Sep 7, 2026
34 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-15742-validate-expressions-non-record-field branch September 7, 2026 12:25
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/s tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

validateStackExpressions throws on a non-record entry of an object's fields: list — an inline cast the asArray sweeps could not see

3 participants