Repository navigation
qa(checklist): clauses for the four defects 18.0 round 3 found outside the ledger, plus two RUNNER rules — a pin scores only the layer it drives, and every run keeps an off-clause probe budget #24802
Description
Filing gate: ③ a task the maintainer directed — the maintainer's words are quoted verbatim below.
Reader: a checklist-author dispatch from the 18.0 QA wave's dispatch session (session_01CfRDy7SuTfUqaTu2AEH2Hi): one ledger PR under docs/qa/platform-checklist/ (area files and RUNNER.md).
Dedupe (open AND closed), objectstack: "checklist RUNNER rule a pin scores only the layer it drives unit pin cannot score full stack clause" → 21 hits, none on this (nearest: #23950 and #21735, clause re-point families; #24173, a RUNNER wording fix); "QA runner off-clause exploratory probe budget findings beside the clauses checklist" → 92 hits, none on this (nearest: the anchor #24579, which collects re-points, not new clauses or rules).
Directed-by
In Claude Code session session_01CfRDy7SuTfUqaTu2AEH2Hi on 2026-10-20, the maintainer asked why round 3 (580fd4ed) found four defects that rounds 1 and 2 did not. The session's answer ended with this proposal, verbatim:
- 补清单条款: 把这 4 个点补进测试清单:批量接口的空字符串 id、warning 规则的日志措辞、@ 提及的收件人要按读权限过滤、group 模式下没有当前组织的成员写入。按立卡门的规定,清单缺项是去补清单本身,不是立卡,可以用 checklist-author 技能来做。
- RUNNER 加一条: 用自动测试判 walled 这类条款时,要核对测试走到了哪一层,单元级测试只能判单元级的条款。
- 每轮留探测时间: 以后每轮都给 runner 留一段清单之外的探测时间。这一轮 4 张卡里有 2 张就是这样找到的。
The maintainer answered, verbatim: 「同意建议的改进」
Why these were missed
All four defective code paths were already present at round 1's subject 5b14b7f3. The rounds missed them for two reasons.
- No clause asked. rest: POST /api/v1/batch answers 500 for an update or delete op whose id is the empty string, where a missing id answers 400 VALIDATION_FAILED #24781 (an empty-string id on
POST /api/v1/batchanswers 500) and objectql(validation): an unevaluable severity:'warning' rule logs "write rejected: … fails closed" twice while the write is accepted 201 and persists #24782 (an unevaluablewarningrule logs "write rejected … fails closed" while the write lands) sit beside clauses that pass. Both were found by a runner probing past its clauses. - A pin was read as covering a layer it never reaches. group posture: a member whose session has no active organization gets 500 ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED on a tenant-scoped create, where isolated answers 403 naming the missing organization #24784:
packages/plugins/plugin-security/src/no-active-organization-write-refusal.test.tshas agroupCONTROL that "lands" at the plugin-security layer. On a full walled boot the engine refuses the same write with a 500. Rounds 1 and 2 scored the walled legs from that pin, because a walled boot was refused at the time.
Scope
- Clauses, one per defect. Each gets a
QA-source:citation, arevisionbump and ahistoryrow. Each stays red until its card's fix lands, and itsknownGapsnames that card.api-backend.batch-transactional-discovery, negative: anupdateordeleteop whose id is the empty string answers 400VALIDATION_FAILED, exactly as a missing id does, and never a 5xx. Card: rest: POST /api/v1/batch answers 500 for an update or delete op whose id is the empty string, where a missing id answers 400 VALIDATION_FAILED #24781.records-forms.validation-rule-type-matrix, negative: an unevaluableseverity: 'warning'rule lets the write land, and no log line beside it reports a rejection. Card: objectql(validation): an unevaluable severity:'warning' rule logs "write rejected: … fails closed" twice while the write is accepted 201 and persists #24782. Round 3 authored such a rule on a runtime-created object; that recipe is in QA run · surface:api (116/130) · 580fd4ed · 2026-10-20 · 62 PASS / 54 PARTIAL / 0 FAIL / 0 BLOCKED / 14 NOT-RUN #24779. It also closes the round-2 fixture gap "no way to author a non-evaluable CEL through the doors".access-security.no-active-org-session-semantics, walled leg: ongroup, a member whose membership set is non-empty and whose session has no active organization never gets a 5xx on a tenant-scoped create, and nothing is stamped with a NULLorganization_id. Card: group posture: a member whose session has no active organization gets 500 ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED on a tenant-scoped create, where isolated answers 403 naming the missing organization #24784. Which answergroupshould give (land, or 4xx) is group posture: a member whose session has no active organization gets 500 ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED on a tenant-scoped create, where isolated answers 403 naming the missing organization #24784's to decide; the clause asserts only no 5xx and no NULL stamping. The item'sautomated.refshould say the pin'sgroupCONTROL stops at the plugin-security layer.records-forms.record-discussion-mentions, a clause for collaboration: authorization / tenancy-wall defect in record-discussion mention notifications under a walled posture (18.0 round-3 walled pass) — detail withheld pending maintainer #24785's fix. Under RUNNER rule 2 this clause lands only after collaboration: authorization / tenancy-wall defect in record-discussion mention notifications under a walled posture (18.0 round-3 walled pass) — detail withheld pending maintainer #24785's fix PR has merged. If the fix PR is still open when this PR is ready, this PR omits the clause and its report says so.
- RUNNER rule — a pin scores only the layer it drives. Before a pin scores a clause whose oracle is a public door's answer, the runner names the layer the pin reaches: a plugin unit, the engine, or HTTP. A pin that stops above the layer the clause's oracle sits at scores that clause
partial, naming the missing layer. group posture: a member whose session has no active organization gets 500 ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED on a tenant-scoped create, where isolated answers 403 naming the missing organization #24784 is the worked instance. Place it beside rule 6 (pin evidence), not inside the walled-boot entry. - RUNNER rule — an off-clause probe budget. Each run keeps time after its clauses for probing the selector's doors past the clauses. Examples: boundary values (empty strings, missing or unknown ids), the opposite severity or posture of an asserted path, recipients outside the asserted scope. A finding that passes the filing gate is filed with
QA-source: … · observed beside the clauses. The run record says how long the probes took and what they covered. In round 3 this budget found rest: POST /api/v1/batch answers 500 for an update or delete op whose id is the empty string, where a missing id answers 400 VALIDATION_FAILED #24781 and objectql(validation): an unevaluable severity:'warning' rule logs "write rejected: … fails closed" twice while the write is accepted 201 and persists #24782.
Not in scope: the round-3 re-points and accuracy findings in #24579 comment 6284472874. Those belong to the anchor lane.
Gates: node scripts/check-platform-checklist.mjs, plus whatever node scripts/pm/dispatch-gates.mjs --commands derives for the touched paths.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 20, 2026
Claim: maintainer-directed dispatch (18.0 QA wave, round-3 follow-up)
Session:
session_01CfRDy7SuTfUqaTu2AEH2HiAccount:
os-warren(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-24802-ledger-probe-rulesWorktree: none — the author runs in its own cloud session checked out at
origin/maina43405d596or laterDomain:
domain:devx(self-applied on the filing side, SKILL exception two: the card's body carries the three gate answers; triage may re-route)Seat: domain:devx#1 (nominal, the absent-line default — this is the 18.0 QA wave's dispatch session acting on the maintainer's direct instruction quoted on the card, not a lane seat)
File surface:
docs/qa/platform-checklist/areas/api-backend.json,areas/records-forms.json,areas/access-security.json,docs/qa/platform-checklist/RUNNER.md, andcoverage.jsononly if the checker requires it (stop on breach; explain in the report)Container & model:
M,mode:cloud,model: opus(TIER_DEFAULT —dispatch-gates.mjs --tieron this surface reads "no path-derived mandate · floor sonnet · default opus · ceiling fable")Clause-②: no — the internal QA ledger and its runner protocol; nothing published
Responsibility: n/a — not a defect card
Thread-read: none
Serial constraints cleared: the anchor #24579 (
domain:devx, in flight) edits the same area files andRUNNER.mdin rounds; round 10 (#24788) merged and no round PR is open now, so whichever lands second merges main. Therecord-discussion-mentionsclause waits on #24785's fix PR merging (RUNNER rule 2).Directed-by: the maintainer, verbatim 「同意建议的改进」, in Claude Code session
session_01CfRDy7SuTfUqaTu2AEH2Hi, 2026-10-20, answering the proposal quoted on the card.Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
os-dev-report
{ "issue": 24802, "status": "done", "branch": "claude/issue-24802-ledger-probe-rules", "pr": "https://github.com/objectstack-ai/objectstack/pull/24811", "session": "session_01MRkuLGosywLTY6EMcP6fGK", "premise_still_valid": true, "summary": "Three clauses landed, each red until its card's fix merges, each with QA-source, a revision bump, a history row and a knownGaps line naming the card. (a) api-backend.batch-transactional-discovery rev 6, acceptance[5]: an empty-string id on update/delete in POST /api/v1/batch answers 400 VALIDATION_FAILED like a missing id, never 5xx (#24781). (b) records-forms.validation-rule-type-matrix rev 5, acceptance[5]: an unevaluable warning rule lets the write land, and no log entry says it was rejected (#24782); fixtures.requires[2] carries the scratch-object recipe and closes round 2's fixture gap (QA run #24616 negative[0]). (c) access-security.no-active-org-session-semantics rev 6, acceptance[7]: the walled group leg asserts no 5xx and no NULL organization_id (#24784), and automated.ref now says the pin's group CONTROL stops at the plugin-security layer (stub terminal for the engine). RUNNER: rule 6 gains 'a pin scores only the layer it drives' (partial, naming the missing layer; #24784 as the instance), and a new rule 8 adds the off-clause probe budget; the run-record body contract gains the probe line. Clause (d) for #24785 is NOT written: fix PR #24794 is still an open draft (re-checked before the PR), so it waits for that merge, as stated in the PR body.", "tests": "Live on a stock showcase at a43405d596 (= branch base = origin/main), after pnpm build --concurrency=2 (76/76, os-verify-lock VERDICT command-exit 0). (a) red as expected: update with id '' -> 500 DATABASE_ERROR, delete with id '' -> 500 INTERNAL_ERROR, no-id controls -> 400 VALIDATION_FAILED 'Operation ... requires an id'; log '[REST] Unhandled error: Error: Update requires an ID or options.multi=true'. (b) red as expected: qa_vr_probe write 201, re-read 1 row, two log entries naming qa_unevaluable_warn both say 'write rejected' (the first, a multi-line entry with a CEL caret, also says 'fails closed'). The verify text now says to read each entry to its end. Error twin qa_vr_probe_err 400 VALIDATION_FAILED, fields[0] {_record, rule_violation}, 0 rows: negative[0] green. (c) not live-tested: it needs a walled boot, so per the dispatch it is written from knownGaps (evidence: #24783, #24784). Server stopped by process group, port freed. No ablation: the diff changes no tested behaviour.", "gates": "On head 1143504b: node scripts/check-platform-checklist.mjs exit 0 (OK — 16 areas, 328 items; 0 line citations survive; 943/953 symbol anchors, unchanged from base). dispatch-gates --commands derived 13, all exit 0: check-ci-filter-parity, check-closing-keyword-parity (+ --self-test), check-comment-mask-corpus, lint check:doc-formula-expressions, check:cross-package-test-inputs, check:doc-authoring, check:driver-memory-census, check:gitlink-declared, check:nul-bytes, check:platform-checklist, check:refd-timer-probe, check:watch-hint-literal. dispatch-gates --ran exit 0: 13 derived, 13 run, 0 NOT-MEASURED (the ran list carried no exit codes, so that zero is declared; the exit codes are in the PR body). coverage.json untouched. CI check-runs on #24811: in_progress at report time.", "mcp_calls": "10 — issue_read x6 (24802 get + comments, 24781, 24782, 24784, 24779), pull_request_read x2 (24794 twice), search_issues x2; no write tool", "api_writes": "2 — repository_dispatch to the fleet-write relay (pr_create #24811 with assignees [os-warren] and labels [skip-changeset], read back identical, run 39386966089 success); post-stamped.mjs issue comment on #24802 (this report)", "open_questions": [], "out_of_scope_findings": [ "class: a · reach: named producer .claude/hooks/guard-main-checkout-bash.sh · evidence: a read-only curl command run from the worktree was blocked as a write into the primary checkout. The hook read a greater-than sign inside a double-quoted JSON -d argument ('\"condition\":\"int(record.title) > 1\"') as a redirect, target '1\"'. Its header promises precision-first and fail-open on unresolved shapes. Worked around by moving the request into a scratchpad .mjs file · dedupe words: guard-main-checkout-bash quoted redirect false positive, greater-than inside string literal", "carrier: none · noted, not filed — RUNNER environment fact: 'setsid nohup pnpm exec objectstack dev ... &' puts the serve grandchild in the group led by the setsid child, not $!, so kill -TERM -- -$! leaves the server listening. Stop the group of the listener's PGID, read with ps -o pgid. The round-3 record #24779 already proposes the setsid fact for RUNNER's table (anchor #24579 lane)" ] }Generated by Claude Code