Repository navigation
plugin-grid's local EXPLAIN_BATCH_MAX_RECORD_IDS has reached its own stated expiry — the spec pin now exports the constant #6286
Description
Activity
yinlianghui-tw commented
on Aug 25, 2026 CollaboratorAuthorMore actionsTriage:
pm:queue+domain:ui, Task, S — the local declaration's own expiry condition is met (spec pin17.2.0exportsEXPLAIN_BATCH_MAX_RECORD_IDS), so this is the declaration's stated successor arriving, not a judgement call. Fix per the card: import from@objectstack/spec/security, delete the local const, keep the why-the-cap-exists half of the comment. In the same pass check the two riders the card names — whether the batch request/response types are now exported (adopt if so) and whetherWireRecordVerdictcan go the same way; each is adopt-or-report-why-not, no third option.
Generated by Claude Code
- addeddomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seat
on Aug 25, 2026 os-support-ai commented
on Aug 25, 2026 CollaboratorMore actionsClaim: PM loop round R1 (wave 4)
Session:session_011SfZeFWrhGLHmfq61xbz4q
Branch:claude/issue-6286-explain-batch-cap-from-spec
Worktree:objectui-issue-6286
Domain:domain:ui
File surface:packages/plugin-grid/src/hooks/useRecordCrudVerdicts.ts+ its tests (stop on breach; explain in the report)
Container & model:S,mode:subagent,model: opus
Clause-②: no — a module-local hand copy of a server contract constant starts being imported from the package that owns it. The value does not change, no accept set moves, no public surface widens.
Serial constraints cleared:packages/plugin-gridis free — no open PR touches it (checked by changed-file list, ⛔ not by title; the four open PRs from this round are onrenderers/action/,plugin-designer+app-shell,plugin-gantt, andexamples/schema-catalog+apps/site). In-flight siblings this wave arepackages/fields/src/widgets/(#6285),packages/components/src/renderers/overlay/(#6278) andapp-shell/.../inspectors/(#6287) — all disjoint.⚠️ Selection note, recorded because it departs from the standing Bug-first order. The maintainer's standing instruction is 「本车道应该优先处理bug」. This card is aTask, and it is being dispatched anyway because the lane's gradedpm:queueBug tier is empty — all seven were dispatched or routed this round. The Bugs that remain are eitherpm:blocked(#6272 genuinely blocked on #6154;⚠️ #6262 / #5926 / #5521 carry no machine-readableBlocked-by:line, which is a half-state for triage) or newly filed and ungraded (#6306 / #6309 / #6317 — ⛔ not claimable, single-producer rule). So this is not a Bug being passed over.Stale-premise check — run against
origin/main, ⛔ not inherited- ✅ The local declaration is still there:
EXPLAIN_BATCH_MAX_RECORD_IDS = 200inuseRecordCrudVerdicts.ts, 1 occurrence. - ⭐ That count is a reading — control on the same pathspec: 22
constdeclarations in the file, so the query resolves and the file is where the card says. ⚠️ The expiry condition itself is the card's central claim and I have NOT verified it from this seat — the card says the pin moved to@objectstack/spec@17.2.0and that@objectstack/spec/securitynow exports the constant, "verified by resolving the installed package and reading the export, not by reading the changelog". I could not re-run that here (no installednode_modulesin the PM checkout). ⛔ The dev must re-derive it before touching anything — this is the whole premise, and if the export is absent the card is not actionable.
裁决 (binding)
Import the constant from
@objectstack/spec/securityand delete the local declaration. The declaration's own doc comment names the condition under which it should stop existing ("the pin bump supersedes this declaration"), and the card's measurement says that condition is met.⭐ Keep the useful half of the comment — why the cap exists and what the server does with an over-cap request (
400 VALIDATION_FAILEDrather than silent truncation). ⛔ Delete only the part that explains why it was declared locally, since that is what stops being true.⛔ Premise fork — stop and report, do not improvise: if the installed spec does not export
EXPLAIN_BATCH_MAX_RECORD_IDS, or exports it with a different value, ⛔ do not "fix" the mismatch and ⛔ do not keep the local copy silently. A value disagreement between a client hand-copy and the server contract is a different and more serious card — report it.PM 机制假设 (verify — rebut freely)
- The card also suggests checking whether the request/response types the same comment mentions are now exported too. ⭐ If they are, say so and scope it explicitly — folding a type migration into a constant import may be right, but it must be a stated decision, ⛔ not a drift.
- The card notes
check-spec-symbol-derivation.mjsrule 1 would flag exactly this shape (a local symbol under a name the spec exports) but cannot, because both scanners skip non-exported declarations — that hole is finding(tooling): check-spec-symbol-derivation.mjs skips every non-exported declaration, so the #5652 mirrors it was cited to catch were invisible to both rules #5899's subject. ⛔ Do not change the scanner.
Verification
⭐ The two worlds are hard to tell apart here, and that is the whole risk on this card. The values agree today (
200both sides), so any assertion on the value passes before and after — a textbook ghost assertion. What actually changes is reference identity: the client stops being able to drift from the server contract. Pin that: e.g. assert the imported binding is the spec's export (identity/provenance), not that it equals200. ⛔ If no honest distinguishing assertion is available, say so rather than shipping a ghost dressed as a proof.⭐ Ablation: revert the import alone; prove the mutation on disk.
⚠️ For a pure deletion-plus-import the honest proof is a before/after count on a single-line marker in each direction — three runs in this lane today had a probe silently no-op while exiting 0 (perl -i -pematching nothing;grep -cFwith a multi-line pattern counting every closing brace). Restore undertrap … EXIT INT TERMwith absolute paths;git diff HEAD --statempty afterwards.⚠️ Repo facts: vitest from the repo root (pnpm exec vitest run PATHS) — ⛔--filter/cd packages/xhit a guard (objectui#3378) that in one observed form silently ran the wrong package and reported green; sanity-check the reported file/test count against the target's own. vitest's console is silenced here — write results to a file.type-checkis hyphenated and runstsc -p tsconfig.test.json.⚠️ pnpm lintisturbo run lint(per-packageeslint .);eslint.config.jssets noparserOptions.project, so linting is not type-aware.⚠️ In this environment you cannot read amin()width back offstyle.width(happy-dom drops the longhand) — unrelated here, but it is the kind of environment fact worth knowing before writing a DOM assertion. Changeset:node scripts/check-changeset-presence.mjs; ⛔ nevermajor.⭐ Read-channel clause — corrected, use this order
gitfirst (free; shallow clone →git fetch --deepen=500 originbeforemerge-base/A...B).- ⭐ UNAUTHENTICATED REST —
https://api.github.com/repos/objectstack-ai/objectui/issues/6286and.../comments?per_page=100. Measured working: 200, complete. ⭐ It also serves check-runs:.../commits/SHA/check-runs?per_page=100. Wrapper:python3 /home/user/pmtools/gh_issue.py 6286— prints a completeness verdict. - ⛔ Rendered HTML page — FALLBACK ONLY, it silently truncates (measured: 15 of 31 timeline items, no error).
- MCP for writes only, plus the one exempt dedup search before filing a finding.
Deliverables
- ⭐ Your OWN claim comment BEFORE the first edit. Your session id will match the PM's (subagent inside it) — say so explicitly; branch + worktree distinguish the seat. ⛔ Do not touch the assignee.
- Draft PR, first line
Fixes #6286. ⛔ Do not mark ready, enqueue, or enable auto-merge. - Terminal report as an issue comment on plugin-grid's local EXPLAIN_BATCH_MAX_RECORD_IDS has reached its own stated expiry — the spec pin now exports the constant #6286 first, then as your return message. ⛔ HTML-comment markers do not survive this sanitizer — plain-text first line
os-dev-report, then the JSON. - ⭐ Predict each measurement's direction before running it, and report every place you were wrong.
Generated by Claude Code
- ✅ The local declaration is still there:
os-support-ai commented
on Aug 25, 2026 CollaboratorMore actionsDev claim —
os-devsubagent, dispatched for this card.- Session:
session_011SfZeFWrhGLHmfq61xbz4q— the same id as the PM claim comment above, and that is expected: I run as a subagent inside the PM session, so the session id cannot distinguish the seats. What distinguishes this seat is the branch and worktree below. - Branch:
claude/issue-6286-explain-batch-cap-from-spec(pushed empty at claim time as a write-route probe — HTTP OK) - Worktree:
/home/user/objectui-issue-6286 - Base:
090927f4f3a39ba708dd8a7a0eda2196d4ff79df(freshly fetchedorigin/main) - Assignee: untouched, per dispatch.
File surface (as dispatched):
packages/plugin-grid/src/hooks/useRecordCrudVerdicts.ts+ its tests. Any breach of that surface stops the run and gets explained in the report rather than absorbed.Order of work, stated up front because the card's premise is the whole risk: re-derive the expiry condition from the resolved package on disk first —
@objectstack/spec/securitymust actually exportEXPLAIN_BATCH_MAX_RECORD_IDS, and with value200. Absent ⇒ card not actionable, report and stop. Present with a different value ⇒ a client/server contract disagreement, which is a different and more serious card — report and stop, do not "fix" it here.Read-channel:
git+ unauthenticated REST only for reads; MCP for writes.
Generated by Claude Code
- Session:
os-support-ai commented
on Aug 25, 2026 CollaboratorMore actionsos-dev-report
{
"issue": 6286,
"status": "done",
"branch": "claude/issue-6286-explain-batch-cap-from-spec",
"pr": "#6333",
"premise_still_valid": true,
"summary": "Replaced the module-localEXPLAIN_BATCH_MAX_RECORD_IDS = 200in packages/plugin-grid/src/hooks/useRecordCrudVerdicts.ts with an import from@objectstack/spec/security, keeping the why-the-cap-exists half of the doc comment on the import and dropping only the why-it-was-declared-locally half, per the binding ruling. Premise re-derived on disk from the resolved package before any edit — static (dist/security/index.d.mts re-export list plusdeclare const EXPLAIN_BATCH_MAX_RECORD_IDS = 200in the shared chunk) and at runtime through the same specifier the source now uses (typeof number, value 200, control: 43 exported keys). No value disagreement, so the premise fork did not fire. CORRECTION to the card: @objectstack/spec@17.0.0 — not 17.2.0 — is the first release whose ./security exports the constant, measured against the published tarballs of 17.0.0-rc.6 / 17.0.0 / 17.1.0 / 17.2.0 (only the rc lacks it). That is load-bearing, not trivia: plugin-grid declares "@objectstack/spec": "^17.0.0" and scripts/check-spec-range-floors.mjs judges the range MINIMUM against the published artifact, so had 17.0.0 lacked the symbol this PR would have owed a floor bump — and a consumer resolving 17.0.0 would have got undefined, which ini += undefineddegrades to one empty request and a silent fail-open for every row rather than a crash. It carries it, so no manifest change was owed and the declared file surface was not breached. Both card riders declined with evidence and filed as #6332 rather than dropped. Two environment corrections for the PM: (1) objectui'sskip-changesetlabel DOES exist (59 labels; it sits on 7 closed PRs) — the standing instruction never to apply it there is correct, but its stated premise that the label does not exist is stale; the repo's own scripts/tests/ci-cd-pipeline-doc.test.ts documents it as auto-minted by being applied (objectui#4912), pins that no workflow or gate reads it, and pins the docs page to keep denying it, which is a stronger reason than the one in the standing text and one an agent who checks and finds the label present would otherwise talk itself out of; (2) the GitHub SEARCH API is blocked for this session ("sessions are bound to their configured repositories"), so the pre-filing dedup search ran as a repository-scoped issue LIST plus local filter.",
"tests": "All final measurements at head db31e5f (final commit), quoting each tool's own verdict line, never a post-pipe $?. [1] vitest, canonical root invocationpnpm exec vitest run packages/plugin-grid: 'Test Files 88 passed (88)' / 'Tests 827 passed (827)'. Changed-file slice: 'Test Files 2 passed (2)' / 'Tests 19 passed (19)' — count sanity-checked against the target's own 15it(blocks plus my 4. [2] tsc, both projects the hyphenatedtype-checkchains, run from the package dir: SRC_EXIT=0, TEST_EXIT=0. NOT-MEASURED guard:--listFilesconfirms the new test is a real program input of tsconfig.test.json (1 hit) and correctly absent from the src project (0 hits), and that the spec declaration the import resolves to is 17.2.0's dist/security/index.d.mts. [3] eslint (plugin-grid's owneslint ., 124 files): 0 errors, 683 warnings, all pre-existing; blind-instrument guard via-f json— the new file was linted (1 entry, 0 errors, 0 warnings). [4] gates derived by enumerating each CI job's own step list, all exit 0: check:spec-symbols ('1305 files scanned against 4959 spec export names'), check:phantom-deps ('Every in-scope import is declared by the package that publishes it'), check:self-import, check:esm-specifiers, check:vi-mock-specifiers ('433 carry a mock; 651 bare (out of scope)'), check:control-bytes ('scanned 5197 tracked text file(s)'), check:pre-install-import-graph, check-changeset-presence ('3 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'), check-lint-coverage ('46/46 packages linted'), check-type-check-coverage ('41/41 packages compile their tests'). spec-range-floors is push+paths (release path), not a per-PR job — the floor question is settled by the tarball measurement above instead. ABLATION: revert the import alone, under trap ... EXIT INT TERM with absolute paths (REPO_ROOT viagit rev-parse --show-toplevel). Mutation proved on disk with a single-line marker in EACH direction (restored local decl 0 -> 1; spec import 1 -> 0) plus a blob-hash comparison against the HEAD blob (5065876b -> ff9c2bcc, differs). Result: 4 of 4 FAILED in useRecordCrudVerdicts.batchCap.test.tsx, while all 15 in rowRecordCrudVerdict.test.tsx stayed GREEN — the ghost demonstrated rather than asserted. Restored via the trap and verified byte-exact:git diff HEAD --statempty and the blob hash back to 5065876b. NO REBUILD NEEDED and the reason is stated rather than assumed: vitest aliases @object-ui/plugin-grid to src/ and the test imports the hook by relative path, so nothing reads plugin-grid/dist; the mutated file is the one the runner executes. The mocked dependency does resolve to node_modules dist, but the mutation does not touch it. PREDICTIONS I GOT WRONG (4, each changed the work): (a) predicted @objectstack/spec@17.0.0 would LACK the constant — it has it; that killed my next prediction, (b) that this PR would owe a floor bump to ^17.2.0 in packages/plugin-grid/package.json — it owes none, which also meant the dispatched file surface was never breached; (c) predicted the first ablation would run 4 of 4 red — it ran 3 of 4, and the survivor was a ghost I had written myself ('exactly cap ids is one request' passes under a cap of 200 as readily as under a cap of 3). Fixed in commit db31e5f by asserting the cap+1 side in the same case; the second ablation is 4 of 4. This is the single most valuable thing the ablation did on this card. (d) predictedspec/dist/security/index.dwould DROP OUT of plugin-grid's src tsc program when ablated, and offered it as a second provenance signal — it does not drop out (still 1), because packages/types/dist/index.d.ts carriesexport type * as Security from '@objectstack/spec/security'. A degenerate control; it is labelled as such in the PR body and offered as evidence of nothing. GHOST DISCLOSURE, stated plainly as instructed: no assertion on the VALUE can distinguish the two worlds here, so none is offered as proof. The honest distinguishing assertion available in this codebase is behavioural reference identity — stand the spec module in at a cap of 3 via vi.mock(...importOriginal) and assert the request chunking follows it, which no hand copy of 200 can produce. Because check-vi-mock-specifiers.mjs judges RELATIVE specifiers only (its own verdict counts this one among '651 bare (out of scope)'), an inert bare-specifier mock has no gate, so the file carries a control case asserting the factory ran and that the stub differs from the shipped value. The pre-existing cap assertion in rowRecordCrudVerdict.test.tsx now derives its fixture and bound from the same export instead of re-typing 200/250 — a cleanup, explicitly NOT a two-world assertion, and the ablation says so. PRE-EXISTING RED, reported so a cold reader is not misled: both tsc projects fail with TS2307 on @object-ui/types / @object-ui/i18n untilpnpm --filter '@object-ui/plugin-grid^...' buildis run in a fresh worktree. Unrelated to this change (the errors are in files it does not touch).",
"open_questions": [],
"out_of_scope_findings": [
"filed as #6332: useRecordCrudVerdicts still hand-shapes the explain request/response — the spec exports ExplainRequest and ExplainDecision but no standalone batch records[] entry type (ExplainRecordAttribution is the layer-level attribution, a different thing), and adopting the response type would turn the hook's fail-open guards into type-level dead code; the contract-first move is ExplainDecisionSchema.safeParse, which is a behaviour change needing its own card. Labelledfinding+domain:ui, no pm:queue, no assignee. Dedup: repository-scoped list of 243 open issues (control: 250 items including 7 PRs) filtered locally on WireRecordVerdict/ExplainRequest/ExplainDecision/useRecordCrudVerdicts — 1 hit, #6286 itself."
]
}
Generated by Claude Code
os-support-ai commented
on Aug 25, 2026 CollaboratorMore actionsos-dev-report
{ "issue": 6286, "status": "done", "branch": "claude/issue-6286-explain-batch-cap-from-spec", "pr": "https://github.com/objectstack-ai/objectui/pull/6333", "premise_still_valid": true, "summary": "Replaced the module-local `EXPLAIN_BATCH_MAX_RECORD_IDS = 200` in packages/plugin-grid/src/hooks/useRecordCrudVerdicts.ts with an import from `@objectstack/spec/security`, keeping the why-the-cap-exists half of the doc comment on the import and dropping only the why-it-was-declared-locally half, per the binding ruling. Premise re-derived on disk from the resolved package before any edit — static (dist/security/index.d.mts re-export list plus `declare const EXPLAIN_BATCH_MAX_RECORD_IDS = 200` in the shared chunk) and at runtime through the same specifier the source now uses (typeof number, value 200, control: 43 exported keys). No value disagreement, so the premise fork did not fire. CORRECTION to the card: @objectstack/spec@17.0.0 — not 17.2.0 — is the first release whose ./security exports the constant, measured against the published tarballs of 17.0.0-rc.6 / 17.0.0 / 17.1.0 / 17.2.0 (only the rc lacks it). That is load-bearing, not trivia: plugin-grid declares \"@objectstack/spec\": \"^17.0.0\" and scripts/check-spec-range-floors.mjs judges the range MINIMUM against the published artifact, so had 17.0.0 lacked the symbol this PR would have owed a floor bump — and a consumer resolving 17.0.0 would have got undefined, which in `i += undefined` degrades to one empty request and a silent fail-open for every row rather than a crash. It carries it, so no manifest change was owed and the declared file surface was not breached. Both card riders declined with evidence and filed as #6332 rather than dropped. Two environment corrections for the PM: (1) objectui's `skip-changeset` label DOES exist (59 labels; it sits on 7 closed PRs) — the standing instruction never to apply it there is correct, but its stated premise that the label does not exist is stale; the repo's own scripts/__tests__/ci-cd-pipeline-doc.test.ts documents it as auto-minted by being applied (objectui#4912), pins that no workflow or gate reads it, and pins the docs page to keep denying it, which is a stronger reason than the one in the standing text and one an agent who checks and finds the label present would otherwise talk itself out of; (2) the GitHub SEARCH API is blocked for this session (\"sessions are bound to their configured repositories\"), so the pre-filing dedup search ran as a repository-scoped issue LIST plus local filter. FORMATTING NOTE: this comment re-posts issue comment 5410976937 unchanged except for this sentence, in the fenced house shape used by every other os-dev-report comment sampled in this repo (5 of 5: #6278, #6075, #6167, #6259, #5306). The first copy was un-fenced, and the MCP comment path appends a 'Generated by Claude Code' footer to every report body, so an un-fenced payload breaks a parser that does json.loads on everything after the marker line. This copy is the canonical one.", "tests": "All final measurements at head db31e5fe9 (final commit), quoting each tool's own verdict line, never a post-pipe $?. [1] vitest, canonical root invocation `pnpm exec vitest run packages/plugin-grid`: 'Test Files 88 passed (88)' / 'Tests 827 passed (827)'. Changed-file slice: 'Test Files 2 passed (2)' / 'Tests 19 passed (19)' — count sanity-checked against the target's own 15 `it(` blocks plus my 4. [2] tsc, both projects the hyphenated `type-check` chains, run from the package dir: SRC_EXIT=0, TEST_EXIT=0. NOT-MEASURED guard: `--listFiles` confirms the new test is a real program input of tsconfig.test.json (1 hit) and correctly absent from the src project (0 hits), and that the spec declaration the import resolves to is 17.2.0's dist/security/index.d.mts. [3] eslint (plugin-grid's own `eslint .`, 124 files): 0 errors, 683 warnings, all pre-existing; blind-instrument guard via `-f json` — the new file was linted (1 entry, 0 errors, 0 warnings). [4] gates derived by enumerating each CI job's own step list, all exit 0: check:spec-symbols ('1305 files scanned against 4959 spec export names'), check:phantom-deps ('Every in-scope import is declared by the package that publishes it'), check:self-import, check:esm-specifiers, check:vi-mock-specifiers ('433 carry a mock; 651 bare (out of scope)'), check:control-bytes ('scanned 5197 tracked text file(s)'), check:pre-install-import-graph, check-changeset-presence ('3 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'), check-lint-coverage ('46/46 packages linted'), check-type-check-coverage ('41/41 packages compile their tests'). spec-range-floors is push+paths (release path), not a per-PR job — the floor question is settled by the tarball measurement above instead. ABLATION: revert the import alone, under trap ... EXIT INT TERM with absolute paths (REPO_ROOT via `git rev-parse --show-toplevel`). Mutation proved on disk with a single-line marker in EACH direction (restored local decl 0 -> 1; spec import 1 -> 0) plus a blob-hash comparison against the HEAD blob (5065876b -> ff9c2bcc, differs). Result: 4 of 4 FAILED in useRecordCrudVerdicts.batchCap.test.tsx, while all 15 in rowRecordCrudVerdict.test.tsx stayed GREEN — the ghost demonstrated rather than asserted. Restored via the trap and verified byte-exact: `git diff HEAD --stat` empty and the blob hash back to 5065876b. NO REBUILD NEEDED and the reason is stated rather than assumed: vitest aliases @object-ui/plugin-grid to src/ and the test imports the hook by relative path, so nothing reads plugin-grid/dist; the mutated file is the one the runner executes. The mocked dependency does resolve to node_modules dist, but the mutation does not touch it. PREDICTIONS I GOT WRONG (4, each changed the work): (a) predicted @objectstack/spec@17.0.0 would LACK the constant — it has it; that killed my next prediction, (b) that this PR would owe a floor bump to ^17.2.0 in packages/plugin-grid/package.json — it owes none, which also meant the dispatched file surface was never breached; (c) predicted the first ablation would run 4 of 4 red — it ran 3 of 4, and the survivor was a ghost I had written myself ('exactly cap ids is one request' passes under a cap of 200 as readily as under a cap of 3). Fixed in commit db31e5fe9 by asserting the cap+1 side in the same case; the second ablation is 4 of 4. This is the single most valuable thing the ablation did on this card. (d) predicted `spec/dist/security/index.d` would DROP OUT of plugin-grid's src tsc program when ablated, and offered it as a second provenance signal — it does not drop out (still 1), because packages/types/dist/index.d.ts carries `export type * as Security from '@objectstack/spec/security'`. A degenerate control; it is labelled as such in the PR body and offered as evidence of nothing. GHOST DISCLOSURE, stated plainly as instructed: no assertion on the VALUE can distinguish the two worlds here, so none is offered as proof. The honest distinguishing assertion available in this codebase is behavioural reference identity — stand the spec module in at a cap of 3 via vi.mock(...importOriginal) and assert the request chunking follows it, which no hand copy of 200 can produce. Because check-vi-mock-specifiers.mjs judges RELATIVE specifiers only (its own verdict counts this one among '651 bare (out of scope)'), an inert bare-specifier mock has no gate, so the file carries a control case asserting the factory ran and that the stub differs from the shipped value. The pre-existing cap assertion in rowRecordCrudVerdict.test.tsx now derives its fixture and bound from the same export instead of re-typing 200/250 — a cleanup, explicitly NOT a two-world assertion, and the ablation says so. PRE-EXISTING RED, reported so a cold reader is not misled: both tsc projects fail with TS2307 on @object-ui/types / @object-ui/i18n until `pnpm --filter '@object-ui/plugin-grid^...' build` is run in a fresh worktree. Unrelated to this change (the errors are in files it does not touch).", "open_questions": [], "out_of_scope_findings": [ "filed as #6332: useRecordCrudVerdicts still hand-shapes the explain request/response — the spec exports ExplainRequest and ExplainDecision but no standalone batch records[] entry type (ExplainRecordAttribution is the layer-level attribution, a different thing), and adopting the response type would turn the hook's fail-open guards into type-level dead code; the contract-first move is ExplainDecisionSchema.safeParse, which is a behaviour change needing its own card. Labelled `finding` + `domain:ui`, no pm:queue, no assignee. Dedup: repository-scoped list of 243 open issues (control: 250 items including 7 PRs) filtered locally on WireRecordVerdict/ExplainRequest/ExplainDecision/useRecordCrudVerdicts — 1 hit, #6286 itself." ] }
Generated by Claude Code
os-support-ai commented
on Aug 25, 2026 CollaboratorMore actions✅ ACCEPT — PR #6333. The card's version claim was wrong in a load-bearing way, and the ablation caught a ghost the dev had written itself.
Reviewer of record:
domain:ui@ objectui execution seat, PM sessionsession_011SfZeFWrhGLHmfq61xbz4q(os-support-ai). Verified atdb31e5fe9with a named ref, ⛔ not against the report.claim my reading local declaration gone EXPLAIN_BATCH_MAX_RECORD_IDS = 200main 1 → branch 0…and that zero is a reading control: other constdeclarations 22 → 21 — exactly one removedimported from the owner ✅ :69import { EXPLAIN_BATCH_MAX_RECORD_IDS } from '@objectstack/spec/security'the useful half of the comment kept ✅ the server-contract half survives (400 VALIDATION_FAILED, never truncated, paginate under it); only the why-it-was-local half was replacedscope 4 files, +230/−16 ⭐ The version correction is the most valuable thing here, and it is not trivia
The card says the pin is
17.2.0and that is when./securitybegan exporting the constant. Measured against the published tarballs of17.0.0-rc.6/17.0.0/17.1.0/17.2.0:17.0.0already carries it — only the rc lacks it.That matters because
packages/plugin-grid/package.jsondeclares"@objectstack/spec": "^17.0.0"(I confirmed both occurrences), andcheck-spec-range-floors.mjsjudges the range minimum against the published artifact — ⛔ not the resolved version. So:- Had
17.0.0lacked the symbol, this PR would have owed a floor bump, and the declared file surface would have been breached. ⚠️ And a consumer resolving17.0.0would have gotundefined— which ini += undefineddoes not crash. It degrades to one empty request and a silent fail-open for every row.
⭐ So "which version first exported it" was the difference between a clean 4-file change and a silent fail-open shipped to anyone on the range floor. The card asserted the resolved version; the dev checked the floor. ⛔ I did not catch this in the dispatch either — my claim comment repeated the card's
17.2.0framing.⭐ The ghost disclosure is exactly what I asked for — and better
My dispatch said: the values agree today, so any assertion on the value passes in both worlds; find a distinguishing assertion or say plainly that none exists. The report states it flatly — "no assertion on the VALUE can distinguish the two worlds here, so none is offered as proof" — and then supplies the honest one: behavioural reference identity. Stand the spec module in at a cap of 3 via
vi.mock(…importOriginal)and assert the request chunking follows it. ⭐ No hand copy of200can produce that.And it closed the hole that trick opens:
check-vi-mock-specifiers.mjsjudges relative specifiers only — its own verdict counts this one among "651 bare (out of scope)" — so an inert bare-specifier mock would have no gate at all. The file therefore carries a control asserting the factory ran and that the stub differs from the shipped value. ⛔ Without that, a silently-inert mock would have made the whole pin vacuous.The pre-existing cap assertion in
rowRecordCrudVerdict.test.tsxnow derives its fixture and bound from the same export instead of re-typing200/250— ⭐ explicitly labelled a cleanup, not a two-world assertion, and the ablation says so.⭐ Four wrong predictions, and one of them found a ghost in its own test
# prediction outcome a 17.0.0would lack the constantit has it — which killed (b) b a floor bump to ^17.2.0would be owednone owed; file surface never breached c first ablation runs 4/4 red ran 3/4 — the survivor was a ghost the dev had written itself d a second provenance signal would drop out when ablated it does not — a degenerate control, labelled as such and offered as evidence of nothing ⭐ (c) is the one. "exactly cap ids is one request" passes under a cap of 200 as readily as under a cap of 3 — it cannot fail in either world. Caught by the ablation, fixed by asserting the cap+1 side in the same case, second ablation 4/4. The report calls it "the single most valuable thing the ablation did on this card" and that is right: the ablation's job is to catch the author's own vacuous assertions, not only the reviewer's doubts.
⭐ And (d) is worth as much: a signal that looked like corroboration was measured, found degenerate (
packages/types/dist/index.d.tsre-exports the namespace, so it never drops out), and then published as evidence of nothing rather than quietly kept as a second data point.⚠️ Two environment corrections I am carrying forward — both invalidate things I have been telling devs- ⛔ The GitHub search API is blocked for this session — "sessions are bound to their configured repositories." My dispatch words have been naming MCP
search_issuesas the one exempt dedup route. It does not work. The working substitute, used here: a repository-scoped issue list plus a local filter (243 open issues, control 250 items including 7 PRs). I am correcting this in subsequent dispatches and on the seat post. ⚠️ objectui'sskip-changesetlabel DOES exist (59 labels; on 7 closed PRs). The standing instruction never to apply it here is still correct — but its stated premise, that the label does not exist, is stale, and an agent who checks and finds it present could talk itself out of the rule. ⭐ The repo's ownscripts/__tests__/ci-cd-pipeline-doc.test.tsgives a stronger reason: the label is auto-minted merely by being applied, no workflow or gate reads it, and the docs page is pinned to keep denying it. That is the version worth carrying.
Also noted: the report was re-posted in the fenced house shape because the MCP comment path appends a footer to every body, so an un-fenced payload breaks a parser doing
json.loadson everything after the marker line. Small, and exactly the kind of thing that silently rots a downstream reader.Landing
Path face clean of governed surfaces → the queue is its route. Enqueue on a by-name read with every check green.
⚠️ The reported pre-existing TS2307 on@object-ui/types/@object-ui/i18nin a fresh worktree until the dependency closure is built is unrelated to this diff and correctly flagged so a cold reader is not misled. Filed separately: #6332 (the hook still hand-shapes the explain request/response; adopting the response type would turn its fail-open guards into type-level dead code, so the contract-first move is asafeParse— a behaviour change needing its own card). Dedup was run properly given the search block.
Generated by Claude Code
- Had
- added a commit that references this issue
on Sep 28, 2026 - added a commit that references this issue
on Oct 7, 2026
Found while taking the measurement for #5899 (see PR #6284 for the full population). Filed unassigned as an observation, for PM triage. Not fixed there — out of that card's scope.
The situation
packages/plugin-grid/src/hooks/useRecordCrudVerdicts.ts:74declares the server's batch cap locally, and its own doc comment states the condition under which it should stop existing:That condition has been met. The tree's pin is now
@objectstack/spec@17.2.0, and@objectstack/spec/securityexports:Verified by resolving the installed package and reading the export, not by reading the changelog.
Why it matters, and why it is not urgent
The values agree today, so nothing is broken right now. What is broken is the guarantee: this is a hand copy of a server contract constant whose whole point is that over-cap requests are refused with
400 VALIDATION_FAILEDrather than truncated. If the server relaxes or tightens the cap and the spec follows, the local200keeps the client paginating at the old boundary with no signal anywhere — a faithful copy passes every value comparison, which is the reference-identity argumentscripts/check-spec-symbol-derivation.mjs's own header makes.The declaration is also self-documenting about this: it does not claim to be canonical, it claims to be temporary. It has simply outlived the reason.
Why nothing caught it
check-spec-symbol-derivation.mjsrule 1 flags a local symbol declared under a name the spec exports — which is exactly what this is, now that the spec exports the name. It could not see it: both scanners skip non-exported declarations, and this const is module-local. That hole is #5899's subject.Fix shape
Import the constant from
@objectstack/spec/securityand delete the local declaration (keeping the useful half of the comment — why the cap exists and what the server does with an over-cap request). Worth checking whether the request/response types the same comment mentions are now exported too, and whetherWireRecordVerdictjust below it can go the same way.Measured on
a76b18cf2against@objectstack/spec@17.2.0.Related: #5899 (the instrument hole that hid it), PR #6284 (the census), #4636 (the pin bump this declaration named as its own successor).