Skip to content

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

Open
claude[bot] wants to merge 5 commits into
mainfrom
claude/issue-15742-validate-expressions-non-record-field
Open

fix(lint): validateStackExpressions reads an object's fields through the guarded reader instead of an inline cast (#15742)#15791
claude[bot] wants to merge 5 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 d701e657d74dc9d5cd17cfbfa7c96db9a55a435dpackageMentionDocs.

Which tree this was computed on

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

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

⚠️ 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

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

2 participants