Repository navigation
fix(lint): one-line verdicts for the flow-credential, AI skill, flow-filter-token, managed-API, org-axis, retired-permission, seed and semantic-role rules, and a bounded field-no-consumers clause; os explain RULE_ID carries their reasoning - #22828
Conversation
…and a bounded field-no-consumers clause Each finding of flow-credential-literal, ai-skill-surface-mismatch, ai-skill-tool-unresolved, flow-filter-token-unknown, object/managed-api-method-unaffordable, org-axis-cross-org-bu-grant, permission-retired-lifecycle-residue, seed-insert-mode-duplicates-on-replay, seed-value-outside-state-machine and semantic-role-field-unprovisioned now prints one verdict sentence of at most 200 characters; the reasoning moves into ten new RULE_EXPLANATIONS entries behind `os explain RULE_ID`. field-no-consumers names the first other object declaring the same field name and counts the rest. Rule ids, severities, paths, hints and what each rule accepts or refuses are unchanged. Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF Co-authored-by: Claude <noreply@anthropic.com>
…emantic-role provenance verdict validate-expressions.test.ts holds this finding and the expression site's unprovisioned-anchor warning to the same object, column and reason; the shortened verdict now keeps both. Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF Co-authored-by: Claude <noreply@anthropic.com>
…rdicts Clause-②: no. Names every door that prints the new text, measured. Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check28 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not 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
Coarse fallback — 4 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 2a42b2adc3fdfdaad9de22c7233fa57725db637e && git checkout 2a42b2adc3fdfdaad9de22c7233fa57725db637e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c4e7fa5a31c627b57dccc410bbbacbf8abff5ea4 82555d5ca82618e2521aab54dbc1d625706658c3 && git checkout -B drift-repro c4e7fa5a31c627b57dccc410bbbacbf8abff5ea4 && git merge --no-ff 82555d5ca82618e2521aab54dbc1d625706658c3
node scripts/docs-audit/affected-docs.mjs --json c4e7fa5a31c627b57dccc410bbbacbf8abff5ea4 |
Landing pre-checks at
|
Part of #22161
Clause-②: no
Stage 2 of the card, slice 10: the last 10
packages/lintrule ids from PR #22799's "Remaining for later slices" list, one whole source file each, plus stage 1'sfield-no-consumers(the card's own subject), whose "a consumer of the same name on …" clause listed every object sharing the field name. The card stays open: thepackages/cliids, the fenced ids and the hint call remain (below).What changes
messageof one verdict sentence, followed byDid you mean "…"?where the rule already offered the nearest name. Every finding the rules' own suites fire is now 194 characters or fewer; the longest of each id was 222 to 441 before. The runtime publish gate's suite (the whole@objectstack/metadata-protocolsuite) fires two of the 10, at 181 or fewer (390 and 235 before). Thepackages/cliunit tier andos validateon the four example apps fire none of the 10.field-no-consumersnames the first other object and counts the rest.a consumer of the same name on "showcase_account" (and 9 more) does not count, where it listed all ten. Onos validateofexamples/app-showcaseits longest finding drops from 341 to 199 characters (that 341-character finding now prints 143). Itsinertarm is byte-identical, so the CLI pins and stage 1's pending changeset that quote it are untouched. Its explanation is unchanged: the verdict cut names, not reasoning, and the explanation already says "Verdicts are per object".wherealready names: the skill (ai-skill-tool-unresolvednow opens on the tool), the managed object, the sharing rule's object and the semantic-role object.seed-value-outside-state-machinenames the first three declared states, then(and N more).flow-credential-literalkeeps a short route in the verdict (move it to a connector's auth.credentialRef, ordrop itfor a connector input). Measured at the base:os validateandos buildprint the hint as theirfix:line (authoringFindingDetailLines), butos lint's text face prints it only under--fix(commands/lint.ts,flags.fix && issue.fix). The module comment said both text faces "leave the hint to--json", which is no longer true foros validate; it now says what was measured. The route by auth variant (bearer, a headerapi-key,api-keywithparamName) stays the hint's, unchanged.semantic-role-field-unprovisionedkeepsADR-0015and the word "storage".validate-expressions.test.ts(the cross-site convergence pin) holds this finding and the expression site's unprovisioned-anchor warning to the same object, column and reason. The first cut dropped both and that pin went red on the whole-suite run; the verdict was fixed, not the pin, which is outside this slice's files.RULE_EXPLANATIONS(packages/lint/src/rule-explanations.ts), 10 new entries, soos explain RULE_IDprints it and the CLI'srule:line ends with the pointer for these ids. No CLI source changes:explainPointer()andos explainresolve any key the table holds (packages/cli/test/explain-rule-id.test.tsiterates every key; it ran in the unit tier below).node bin/run.js explain org-axis-cross-org-bu-grantprints the entry. The 121 entries already in the table are byte-equal (the diff ofrule-explanations.tsis additions only, +266 / -0).path,hint(thefix:line) and what each rule accepts or refuses are unchanged. No condition, branch, skip or dedupe moved, and no export was added, removed or renamed: every hunk in the eleven rule files is amessageexpression, a comment, a verdict helper, or the removal of a value that fed only the old message. The plumbing hunks, each message-only:lint-flow-credential-literals.ts:SERVEDbecomes one clause;STEER_HEADERandSTEER_QUERYbecome oneSTEER_CONNECTOR(both arms now give the same short route; the hint keeps routing by shape);STEER_INPUTis shortened; the query arm's position readsurl query parameter.validate-managed-api-methods.ts: the import ofdescribeManagedApiMethodConflicts(@objectstack/spec/data) goes; it fed only the message. That spec sentence alone measured about 190 characters at three verbs, so no verdict built on it fits; see Acceptance notes for what that leaves inpackages/spec.validate-seed-state-machine.ts:atMostThree()(new, module-private) bounds the known-state list.validate-field-consumers.ts: thesharedClauseexpression namessharedWith[0]and counts the rest..changeset/22161-lint-slice-10-one-line.md:@objectstack/lintpatch, naming every door that prints the new text (below).Clause-②: no, as line 2 says: every rule id these entries key already had an exported constant on the root barrel (index.tsis untouched), and slice 1's contract review (② of its record) readsRULE_EXPLANATIONSentries as data in an existing export.packages/cliandpackages/metadata-protocoltests that assert these ids' text pin fragments the new verdicts keep (auth.credentialRefandevery member who can read flowsforflow-credential-literal; the unchangedinertarm offield-no-consumers), and they ran green unedited (below).The verdict forms, the longest of each arm the rule suites fired (the last line is
os validateon the showcase app):Shared prose: written once (the dispatch's route, measured)
readable by every member who can read flows)flow-credential-literal, all three positionsSERVEDin the rule file (verdict); the explanation's first paragraph (reasoning)The dispatch expected shared paragraphs across ids, as slices 3 to 9 found. Measured: none of the 10 explanations shares a paragraph with another. The two AI rules judge different references (an agent's
skills, a skill'stools) against different universes; the two seed rules answer different questions (replay idempotence, the state-machine exemption);org-axis-cross-org-bu-grant's siblingorg-axis-permission-inheritanceis not converted (174 at its longest) and has no entry to share with. So the table holds the one in-file shared clause.The explanation module imports nothing, so each fact it writes out is held to its source by the rule's own test: the credential positions and the three region kinds by firing each; the surface defaults, the
botharm and the skipped unresolved skill by the existing cases; the three tool sources, the wildcard and the four headless paths by each one silencing the rule; the flow template functions toFLOW_TEMPLATE_DATE_FUNCTIONSandFLOW_TEMPLATE_VALUE_FUNCTIONS, and the placeholders by each one staying silent; the verb table toMANAGED_WRITE_VERB_AFFORDANCE, verb by verb through the rule; the business-unit recipients to theShareRecipientTypepartition the suite pins, each fired; the retired keys and their residue value to the spec's capture; the declared-state union by each of its three sources silencing the rule; and the external-object provenance by the existing cases.Census (taken first, before any edit, at the base
23419bafb7)Method: slice 4 to 9's scratch preload (
NODE_OPTIONS=--import, never committed), which patchesArray.prototype.pushto record every finding-shaped object (rule+message), deduped by (rule, message) per process, with the push site for the 11 ids and the other ids in their files. Lengths aremessagealone; the printed line addswhereand:. Rows fromos lint's own printer (commands/lint.ts:761, which pusheswhere: message) are named as such. Positive control in every run:field-no-consumers, plussort-field-unknownandsearchable-field-unknownin the runtime-gate suite, recorded in every run.packages/lintsuite, base 137 files / 6,416 tests, after the CLI, metadata-protocol and example closure build at the base: all 10 ids fire, every one over 200 at its longest; each longest equals PR fix(lint): one-line verdicts for the nav, object-reference, capability, action-name, mapping-target, view-container and interface-page rules;os explain RULE_IDcarries their reasoning #22799's list.field-no-consumers: 14 messages, longest 167.packages/cliunit tier, the whole project (281 files, 4,174 tests): fires none of the 10, before and after;field-no-consumersfires 7 messages, the longest 142 asos lint's printer row, all on the unchangedinertarm.@objectstack/metadata-protocolsuite (228 files, 225 run, 28,129 passed), which reads@objectstack/lintfrom its builtdist/, before on the base-built dist and after on the rebuilt one: firesflow-credential-literal(3 messages,runtime-authoring-gate.flow-credential-literal.test.ts) andpermission-retired-lifecycle-residue(2); none of the other 9.os validate(the built CLI) onapp-crm,app-todo,app-showcaseandapp-multi-package, exit 0 each, before and after: none of the 10 fire;field-no-consumersfires on all four, and every app prints the same number ofrule:lines before and after (9 / 7 / 70 / 3).packages/clie2e files that name these ids (flow-credential-literal-doors.e2e.test.ts,rule-line-explain-pointer.e2e.test.ts,starter-field-consumers.e2e.test.ts,generate-scaffold-gates.e2e.test.ts; in no per-PR vitest project, run here underOS_TEST_TIERS=nightly) and the one integration file (union-fold-command-parity.test.ts).dist/rebuilt from the slice atdfec085942(markerholds a credential-shaped literal1 hit each indist/index.js,index.cjs,runtime.js,runtime.cjs; the oldholds a literal value that reads as a credential0 in each;why business-unit trees stay inside one organization1 indist/rule-explanations.jsand.cjs). Every after-run except the final lint suite read that dist; the commits after it add the changeset and theorigin/mainmerge, which touches nopackages/lintfile.os validateflow-credential-literalai-skill-surface-mismatchai-skill-tool-unresolvedflow-filter-token-unknownobject/managed-api-method-unaffordableorg-axis-cross-org-bu-grantpermission-retired-lifecycle-residueseed-insert-mode-duplicates-on-replayseed-value-outside-state-machinesemantic-role-field-unprovisionedfield-no-consumersThe cli unit tier column is not drawn: it fires
field-no-consumers' unchangedinertarm alone (7 · 64–142), before and after. The cli integration file firesfield-no-consumersonly (2 · 64–140, before and after).org-axis-permission-inheritance(not converted): lint suite 4 · 167–174 before and after. A message count that falls after (ai-skill-tool-unresolved,semantic-role-field-unprovisioned, showcase'sfield-no-consumers) is two findings whose old texts differed only in a name the verdict no longer repeats or a list it now cuts; a count that rises is a case this slice added. Every push site of the 11 ids fires before and after (one each,semantic-role-field-unprovisioned's helper reached from its two roles).Doors that print the new text
Read from the registry (
authoring-rules.ts):lintFlowCredentialLiteralsandvalidateFlowFilterTokensare CLI and runtime on['flow'],validateManagedApiMethodson['object'],validateRetiredPermissionResidueon['permission'];validateOrgAxisRedLines,validateFieldConsumers,validateSemanticRolesand the two seed rules are CLI only; the two AI rules are members of the reference-integrity suite on its default['flow']. The CLI's callers:commands/validate.ts,compile.ts(os buildis its alias),lint.ts,verify.tsthroughjudgeAuthorTimeRuleson thevalidateset, andinit.tsthroughvalidateScaffoldon thebuildset (one caller,commands/init.ts:1314;commands/generate.tscalls no authoring rule). Measured at the runtime door withrunRuntimeAuthoringRuleson the rebuilt dist (scratch probe): aflowwrite answersflow-filter-token-unknowninerrorsandflow-credential-literalinadvisories; anobjectwrite answersobject/managed-api-method-unaffordableinerrors; apermissionwrite answerspermission-retired-lifecycle-residueinadvisories; anagent,skillorsharing_rulewrite runs no rule.os validate,os build(andos compile, whichos devruns at startup and, under--verbose, per watch recompile),os lint(where: message), the scaffold checkos initrunsrule:line gains theos explainpointer for the 10;os validate --jsonerrorsandos build --jsonauthor-timeissuescarry the newmessageos verifyai-skill-surface-mismatch,flow-filter-token-unknown,object/managed-api-method-unaffordable,org-axis-cross-org-bu-grantflowwriteflow-filter-token-unknown(error),flow-credential-literal(warning)messageand theOS_ALLOW_UNLINTED_METADATA_WRITESrefusal log line; the 2xxadvisoriesentry and the[Protocol] authoring advisorylog lineobjectwriteobject/managed-api-method-unaffordable(error)messageand the refusal log linepermissionwritepermission-retired-lifecycle-residue(warning)advisoriesentry and the advisory log lineorg-axis-cross-org-bu-grant, both seed ids,semantic-role-field-unprovisioned,field-no-consumers(CLI-only rules); both AI ids (run there only for aflowwrite, whose snapshot carries no agents or skills)hint; every other rule idEvery row is named in the changeset.
field-no-consumersacross doors (the dispatch's item 6, measured onexamples/app-showcase)os validate: 51 findings, each printed once (verdict,fix:,rule:with the pointer); the longest printed verdict line 391 → 253 characters.os build: 37 of them, each once, inside the 50-entry advisory cap (… and 20 more author-time warning(s) not shown (50 of 70)); longest printed verdict line 257 → 253.os dev --compile --fresh: the startup compile (stdio: 'inherit') prints the same 37, once; a watch recompile prints its output only under--verbose, else only stderr on failure (commands/dev.ts,startWatchRecompile). Nothing is printed again at serve.Give … a consumerandRoots scannedappear 0 times in theos validate,os buildandos devoutput, before and after: stage 1 moved that text into the explanation. Nopackages/cliprinter was edited.Tests
The rule suites import the rule source; the CLI, runtime-gate, example and e2e runs read the rebuilt
@objectstack/lintdist. Every heavy run went throughscripts/pm/os-verify-lock.sh; its VERDICT lines are quoted.field-no-consumers'inert,carrier-only, one-name and counted arms). Exact pins hold one verdict per arm; explanation pins hold, per id, thatexplainRule(id)exists and names what the verdict stopped saying, and the source-held facts above. Every refusal assertion, rule id, severity,path,whereand hint pin is unchanged. The pins that read the old text moved to the new verdict: the credential route fragments tomove it to a connector's auth.credentialRef(and the hint'stype: 'bearer', credentialRef, newly pinned beside the existingheaderNameone); the AI tool suite's/references tool "…"/matcher to/^Tool "…"/andtool familytoTool family.pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2: Test Files 137 passed (137), Tests 6,455 passed (6,455); VERDICT command-exit 0 (base 137 / 6,416; no new file, 39 new cases).pnpm --filter @objectstack/lint build(VERDICT command-exit 0,check-dts-emitted6/6), and after the mergeturbo run build --filter=@objectstack/lint(4 tasks, VERDICT command-exit 0).pnpm --filter @objectstack/lint run typecheckat the final head: VERDICT command-exit 0;check:test-typecheckOK, 2 files / 6 errors / 2 pinned signatures held.tsc -p packages/lint/tsconfig.test.json --listFileslists all eleven touched test files.pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2: Test Files 281 passed (281), Tests 4,174 passed (4,174), before and after; VERDICT command-exit 0 both. It includestest/explain-rule-id.test.ts. The integration tier is declared to CI (no file in it asserts these ids' text, and the diff touches no spawn entry); its one file naming these ids ran as the census extra: Test Files 1 passed (1), Tests 9 passed (9), before and after.OS_TEST_TIERS=nightly): Test Files 4 passed (4), Tests 56 passed (56), before and after; VERDICT command-exit 0 both.flow-credential-literal-doors.e2e.test.tsasserts the message carriesauth.credentialRefat every door; it does.@objectstack/metadata-protocolsuite, before and after: Test Files 225 passed | 3 skipped (228), Tests 28,129 passed | 19 skipped (28,148), both runs; VERDICT command-exit 0.runtime-authoring-gate.flow-credential-literal.test.tsassertsevery member who can read flowsandauth.credentialRef; both are kept.packages/lint/src:git grep lint/src -- packages/spec/srcnames two that walk it,src/shared/retired-key-migrate-sentence.test.ts(judges everyos migrate meta --from Nsentence) andsrc/identity/position-delegatable-enforcer.pin.test.ts(readssecurity-*rule-id lines); the other hits name single files outside this slice or are comments. No new text carries the migrate marker (the residue explanation namesos migrate metawithout--from), and no rule-id line moved.pnpm --filter @objectstack/spec test:repoat the final head: Test Files 55 passed (55), Tests 971 passed (971); VERDICT command-exit 0. Both pins are in itsvitest.repo-tests.jsonlist.#plus digits in any printed message or explanation (rule-explanations.test.tsrefuses one in every paragraph); the citations stay in//comments and test names.92a7bebbb4, throughscripts/ablation-replace.mjswrap mode under the verify lock, plus a shelltraprestoringHEADby absolute path). The rule suites import the rule source, so there is no dist leg.seed-insert-mode-duplicates-on-replay's pre-slice message tail was restored verbatim through the anchor of its new tail: anchor x1 → x0, blob2e2c47f4e718→9acef22a58d3. Predicted before the run: exactly two cases red, the id's exact-text pin and the 200-character bound pin; the suite's other assertion on the message (toContain('replay')) holds on the old text too. Observed:src/validate-seed-replay-safety.test.tsTest Files 1 failed (1), Tests 2 failed | 7 passed (9); the two red cases are exactly those two (the bound pin read 222 characters). Restored: blob2e2c47f4e718== blob at HEAD,git diff HEADempty,git status --porcelainempty.npx eslint --no-inline-config --format jsonover the 23 changed.tsfiles gave 23 files in the report, 0 errors, 0 warnings. The population iseslint.config.mjs'sfiles: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}']block plus thepackages/**/*.{ts,tsx,mts,cts}blocks, which all 23 are in (the changeset is not a linted kind).eslint.config.mjsnever enables type-aware linting (noparserOptions.project, no typed@typescript-eslintrules; the config's own note says so), so no untouched file's verdict can move. A control-character scan of every changed file found no match.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackwith no paths, at the final head82555d5ca8(merge baseb7cd1af9df, after one merge oforigin/mainthroughscripts/pm/os-regen-merge.sh; the merge touched nopackages/lintfile, and its step 2 tookorigin/main's side of the generated artifacts main moved, none of which this branch edits), derived 62 commands. I ran 60 of them sequentially at that head, each with its own log and its exit code captured before any pipe: all 60 exited 0. Two are NOT MEASURED, as the dispatch directs:pnpm check:dual-build-cjs-loads(it needs every package'sdist/, and the dispatch excludes a whole-workspace build) andpnpm check:type-check-debt(--re-measure, which builds every package); the non-re-measuring half,check:type-check-coverage, ran and exited 0.--ranover the exit-coded record:Run reconciliation — 62 derived, 60 run, 2 NOT-MEASURED, 0 UNRUN.The artifact-roster block the no-path run prints (51 families) ran too: the 48 that need no pull request all exited 0,check:published-readme-exportsonly after onePREREQUISITE NOT MET(exit 3:client-react,embedder-openai,knowledge-ragflowandorganizationshad nodist/; I built exactly those four with their closure under the lock, VERDICT command-exit 0, and it then exited 0). The four rostered under a directory this diff touches (check-changeset-fixed,check:authz-resolver,check:error-code-casing,check:filter-alias-parity) are among the 48. The three PR-context guards (check-closing-target-claim,check-partof-closing-keyword,check-single-claim-paths) run against this PR once it exists, and their verdicts go to the card report (this body is written once). The long ones on the shared box:check:query-options-erasure249s,check:slot-lookup135s,check-comment-mask-corpus112s.Remaining for this card
packages/lint: no author-time id outside the fenced files fires over 200 characters in the lint suite after this slice. Measured over the final census: 28 ids still exceed 200; 26 are PR fix(lint): one-line verdicts for the widget, dataset, security-posture and visibility rules;os explain RULE_IDcarries their reasoning #22448's fenced ids, and 2 arelint-startup-registry-verdict.ts's (startup-open-vocabulary-verdict779,startup-verdict-assertive-wording731), which PR fix(lint): one-line verdicts for the widget, dataset, security-posture and visibility rules;os explain RULE_IDcarries their reasoning #22448 records as the repo gatecheck:startup-registry-verdict's, not an author's.react-prop-deprecatedfires in no suite, as PR fix(lint): one-line verdicts for the flow-trigger, react-page, dataset-measure and validation-rule compile rules;os explain RULE_IDcarries their reasoning #22700 notes.os explain RULE_IDcarries their reasoning #22448 lists them (26):lint-flow-patterns.ts(15),validate-flow-template-paths.ts(3),validate-component-props.ts(2),validate-page-field-bindings.ts(2),validate-expressions.ts(1,expression-invalid),validate-filter-tokens.ts(1,filter-token-unknown),validate-form-layout.ts(1),validate-print-page-blocks.ts(1).packages/cli, as PR fix(lint): one-line verdicts for the widget, dataset, security-posture and visibility rules;os explain RULE_IDcarries their reasoning #22448 lists them:hook-body/not-lowerable,hook-body/unparseable,hook-body/bundled-fallback,hook-body/extraction-failed(packages/cli/src/lint/hook-body-lowering.ts);docs/uncollected-directory,docs/duplicate-name,docs/nav-target,docs/frontmatter-tags(packages/cli/src/utils/collect-docs.ts);picklist-reference-unverified(packages/cli/src/utils/picklist-references.ts). Also theaction-governance.tsboot-log lines (packages/objectql), stage 1's own later slice.field-no-consumers' hint lists every carrier site, unbounded; onos validateofexamples/app-showcaseitsfix:line runs to 629 characters (16 carrier sites onshowcase_account.website). Unchanged here, as every slice leaves hints. None of the other 10 ids' hints points at text the verdicts cut: each was read against the cut text and is self-contained. The nearest case:seed-value-outside-state-machine's hint says "The exemption lets a seed be born mid-lifecycle", and the verdict still names that exemption (seed writes skip that rule).field-no-consumersprinters (item 6): no second paragraph exists outsidepackages/lint;os buildandos devprint each finding once, inside the 50-entry advisory cap. Nothing owed to thedomain:clihalf from this measurement.Acceptance notes
describeManagedApiMethodConflictskeeps no caller outside its own test. Its docblock (packages/spec/src/data/managed-api-affordance.ts) calls it "the one sentence both consumers say", the registry's strip warning and this lint finding. Measured at the base: objectql'sreconcileManagedApiMethodsalready spells its own sentence and never called it, so the lint verdict was its only caller, and this slice's verdict cannot carry it under 200 characters. The export is a published@objectstack/specsymbol and outside this slice's files; not removed and not filed (an observation about a docblock and a now-uncalled export). Carrier: none.field-no-consumersonshowcase_client_brief.projectat 199, whose one named object is 27 characters long.field-group-shadowed(validate-semantic-roles.ts, not one of this slice's ids) lists the group's members unbounded; the suites fire it at 168 at its longest. Observation only.os lintprinter rows prefixwhere:to the message, soflow-credential-literal's connector-input row prints at 233 in the e2e run while its message is 181; slice 9 recorded the same shape (188 against 175).Generated by Claude Code