Repository navigation
objectql(validation): an unevaluable severity:'warning' rule logs "write rejected: … fails closed" twice while the write is accepted 201 and persists #24782
Description
Filing gate: ① a product defect with a named location and a live reproduction (an operator-facing log line that states the opposite of what the server did).
Reader: the packages/objectql validation lane (src/validation/rule-validator.ts), next PR touching the rule-validator advisory branch.
Dedupe (open AND closed): "validation rule warning severity unevaluable logs write rejected but write persists" → 22 hits, none on this (nearest: #22726, warning hits not surfaced to the client — closed, different; #13889, warning-rule log volume on machine paths — closed, different; #24720, a parent-reading rule refused as unevaluable — open, error severity, different).
reach: public HTTP entry, measured twice — POST /api/v1/data/OBJECT on a stock showcase boot with a runtime-authored object answers 201 while the server log says the write was rejected.
Path: records-forms.validation-rule-type-matrix
QA-source: #24779 · records-forms.validation-rule-type-matrix · negative[1] (the clause passed — the warning rule does not block; the defect is the log beside it)
Checklist item priority: P1
type: Bug
What happens
rule-validator.ts states the contract in its header: "severity still governs blocking: an unevaluable warning/info rule is logged, never thrown — advisory rules stay advisory." The write path does exactly that: the write answers 201 and the row persists. But the log lines it prints for that rule say the opposite:
WARN Validation rule 'qa_unevaluable_warn' predicate failed to evaluate (…) — write rejected: a rule that cannot be evaluated fails closed, it is never skipped
WARN Validation rule 'qa_unevaluable_warn' (warning): Validation rule 'qa_unevaluable_warn' could not be evaluated (…) — write rejected.
The first is the predicate evaluator's fail-closed sentence (~L4517; the when-predicate twin ~L4699 carries the same words), emitted before severity is consulted; the second is the advisory branch's per-write warn, which embeds the unevaluable violation's own message, also ending "write rejected". An operator reading the log for a disputed write is told it was refused when it was stored — twice.
Reproduction (stock showcase, objectstack dev --seed-admin, framework 580fd4ed)
- Admin cookie session.
PUT /api/v1/meta/object/qa_vr_probewith{"name":"qa_vr_probe","label":"QA","sharingModel":"public_read_write","fields":{"title":{"type":"text"}},"validations":[{"type":"script","name":"qa_unevaluable_warn","severity":"warning","message":"x","condition":"int(record.title) > 1"}]}→ 200.POST /api/v1/data/qa_vr_probe{"title":"abc"}→ 201, row persisted (correct).- Server log: the two lines above, both saying "write rejected".
Expected: a log line that says the advisory rule could not be evaluated and the write proceeded (advisory). Actual: "write rejected … fails closed", twice. Control: the same rule at severity:"error" → 400 VALIDATION_FAILED naming the rule — there the sentence is true.
Fix direction
Decide the verb after severity: the fail-closed sentence belongs to the error-severity path only; the advisory path should say the rule was skipped as advisory (one line, not two). Pin the log text for both severities.
Full evidence chain: run record #24779 (records-forms.validation-rule-type-matrix block in the runner A evidence comment).
Activity
qa(checklist): clauses for three defects round 3 found outside the le…
qa(checklist): record the a43405d re-measure of the batch and validat…
objectstack-fleet commentedon Oct 20, 2026
Still reproduces at 307ce4c · QA-source: #24841 · records-forms.validation-rule-type-matrix · observed beside the clauses (two 201 writes, row persisted, two "write rejected: a rule that cannot be evaluated fails closed" lines)
Generated by Claude Code
objectstack-fleet commentedon Oct 21, 2026
Claim: PM loop round 21 (p3 only, by the maintainer's order 「你后续只处理 p3」) · 2026-10-21T03:29Z · p3,
domain:engine(triage's grade), filed from QA run record #24779 (records-forms.validation-rule-type-matrixnegative[1]); re-reproduced at307ce4c4(6288115226, QA-source #24841). An unevaluableseverity: 'warning'(or'info') validation rule never blocks: the write answers 201 and the row persists. But the server log tells the operator twice that the write was rejected and that the rule "fails closed".Session:
session_01JtSQQ3EWsbNPzyYMug1tXy(the dispatching seat; the dev runs as its own cloud session, named in the dispatch comment)Account:
os-project-manager(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-24782-advisory-unevaluable-logWorktree:
objectstack-issue-24782Domain:
domain:engineSeat:
domain:engine#1(seat post #6367)File surface:
logger.warninpackages/objectql/src/validation/rule-validator.tswhose sentence says "write rejected" or "fails closed", and whether each is reached before or after the rule'sseverityis known. The predicate evaluator (:4517), itswhen-predicate twin (:4699), the relationship-traversal refusal (:4479), and the advisory branch's per-write line inevaluateValidationRules(:3640), which embeds the unevaluable violation's own message;qa/,docs/qa/platform-checklist/andcontent/docs/.packages/objectql/src/validation/rule-validator.ts, its log lines only:error-severity rule, whose write is refused;warning/inforule logs ONE line. It names the rule, its severity and the evaluation fault, and says the write proceeded because the rule is advisory;severity: 'warning'的校验规则没有受众概念 —— UI 级劝导在 seed/bootstrap 等机器写入路径上照样求值并打爆启动日志,且按写入次数而非按行计数 #13889 seed / boot aggregation keeps its shape.errorcontrol's 400VALIDATION_FAILEDbody unchanged. Ablation: revert, and the pins go red.patchfor@objectstack/objectql.⛔ Not in scope:
ValidationError/ 400VALIDATION_FAILEDresponse, and the unevaluable violation's ownmessageas anerrorrule returns it to the caller;severity: 'warning'rule's hit is only logged — the data write answer carries nowarnings, so no client can show the advisory #13889 calls UI-level #22726): an unevaluable rule stays out ofadvisories;requiredWhen,readonlyWhenand the other field-level conditional evaluators, which refuse on every severity;readonlystrip (stripReadonlyFields/staticReadonlyInsertSubject), held bydomain:servicesseat 2's group posture: a member's insert whose organization_id names another organization of its membership set is silently stored in the active organization (the engine's read-only strip), where #21666 ruled an explicit value meets the wall and is never rewritten #24822 (6305533754).Container & model:
S,mode:cloud(log lines in one module, and their pins),model: TIER_DEFAULT(this run'sdispatch-gates.mjs --tier: "no path-derived mandate")Clause-②: no (log text only: no response, status, code, outcome or export moves)
Responsibility: an operator reading the log for a disputed write is told it was refused when it was stored | the predicate evaluator logs its fail-closed sentence before the rule's severity is consulted, and the advisory line repeats it | every operator of an object with an unevaluable
warning/inforule, on every write doorThread-read: none
Serial constraints cleared: the 17 open PRs' file lists and the
pm:dispatchedclaims were read at this claim, onorigin/maind68aab6fde. No open PR touchespackages/objectql/src/validation/.domain:servicesseat 2's #24822 (claim 6305515833, region note 6305533754) editsrule-validator.ts's static-readonlystrip only (stripReadonlyFields~:2099,staticReadonlyInsertSubject~:2472), which this claim leaves alone; the regions are disjoint. Whichever PR lands second mergesmain.Generated by Claude Code
objectstack-fleet commentedon Oct 21, 2026
Dispatch ·
domain:engineseat 1 ·session_01JtSQQ3EWsbNPzyYMug1tXy· 2026-10-21T03:32Z:mode:cloud. The dev session issession_01YJ2vytxktPaEAmkEavY9KP. It builds at the default tier on branchclaude/issue-24782-advisory-unevaluable-log, under claim 6307306950.rule-validator.ts, and whether it is reached before or after the rule'sseverityis known;qa/and docs.warning/inforule logs one line, saying it could not be evaluated and the write proceeded as advisory;errorpath's refusal and its log stay as they are;patchchangeset for@objectstack/objectql.packages/objectqlare named for the seat to declare before the queue.severity: 'warning'rule's hit is only logged — the data write answer carries nowarnings, so no client can show the advisory #13889 calls UI-level #22726 advisory events, the static-readonlystrip (group posture: a member's insert whose organization_id names another organization of its membership set is silently stored in the active organization (the engine's read-only strip), where #21666 ruled an explicit value meets the wall and is never rewritten #24822's region), objectui.Generated by Claude Code
objectstack-fleet commentedon Oct 21, 2026
os-dev-report
{ "issue": 24782, "status": "done", "branch": "claude/issue-24782-advisory-unevaluable-log", "pr": "https://github.com/objectstack-ai/objectstack/pull/25051", "session": "session_01YJ2vytxktPaEAmkEavY9KP", "premise_still_valid": true, "summary": "Premise held at d68aab6fde: checkPredicate, checkConditional and traversalScopeRefusal logged the fail-closed sentence before severity was known, and the advisory per-write line embedded the unevaluable violation's own message ending in 'write rejected.'. Seam: RuleContext gains a per-rule advisory flag that the evaluateValidationRules loop sets from the top-level rule's severity, and a conditional's branch inherits it. The evaluators log through failClosedLogger(ctx), which is the write's logger for an error rule and none for an advisory one. The loop reports an unevaluable advisory rule once through unevaluableAdvisoryMessage: rule, severity, constraint.fault, 'write proceeded: the rule is advisory'. The #13889 seed fold takes the same text and keeps its shape. No new export, and no change to the violation, the ValidationError body, which rules block, or the #22726 advisories. Card assignee untouched (os-project-manager); the PR assignee was set the same way at pr_create.", "tests": "Head 2e42d14e4 (origin/main merged). New rule-unevaluable-advisory-log.test.ts: 8/8 pass. warning and info script conditions, a conditional when-predicate and the traversal refusal each log exactly one line naming the rule, severity and fault, saying 'write proceeded', with no 'write rejected' or 'fails closed'. None of them refuses the write, and each returns no advisories. The seed aggregation folds the advisory text with zero per-row lines. CONTROL: the same three slots at error throw ValidationError (rule_violation, constraint.reason unevaluable, message '... — write rejected.') and log exactly one fail-closed line. Engine-level pin in advisory-write-answer.test.ts: createData stores the row, answers no warnings key, and logs one advisory line (file 22/22). Ablation: rule-validator.ts restored to d68aab6fde on disk, failClosedLogger grep count 6 to 0. Result 'Tests 6 failed | 24 passed (30)': the 6 failures are exactly the advisory pins, and the 3 error controls stayed green. Restored by checkout HEAD: blob 51aa0041 equals the HEAD blob, git diff HEAD empty, porcelain empty. objectql resolves its own src, so no dist rebuild was involved. pnpm --filter @objectstack/objectql test: 453 files / 8533 tests passed, VERDICT command-exit 0. typecheck: exit 0 before and after the merge (check:test-typecheck OK, 233 errors / 64 signatures held). pnpm --filter @objectstack/core test:repo: 6 files / 82 tests passed, exit 0. Full turbo run build on the merged head: 76 packages, exit 0.", "gates": "Local: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) printed 70 lines; the repo was deepened with --shallow-since=2026-07-13 first. All 70 ran on final head 2e42d14e4, each exit captured before any pipe, all exit 0. On the first sweep at 3148214b, check:dual-build-cjs-loads and check:type-check-debt exited 3 (PREREQUISITE NOT MET, no dist). The full build was done for them, and both exit 0 on the final head. dispatch-gates --ran with ':: exit N' annotations: '70 derived famil(ies) accounted for — 70 run, 0 NOT-MEASURED (a DERIVED zero — all 70 recorded an exit code and none of them is 3)'. CI: the PR's check-runs were in progress at report time and were not waited on (os-dev contract).", "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm", "mcp_calls": "3 — mcp__github__issue_read (get, get_comments on #24782), mcp__github__pull_request_read (get on #25051); no write tool", "api_writes": "2 relay actions, as objectstack-fleet[bot] — pr_create (POST /repos/objectstack-ai/objectstack/pulls draft + POST /repos/objectstack-ai/objectstack/issues/25051/assignees, read back: 6798 bytes sent/stored identical, assignee os-project-manager); comment (POST /repos/objectstack-ai/objectstack/issues/24782/comments, this report). Plus git push to the branch (not REST): empty-branch probe, two WIP commits, the merge commit.", "line_budget": "none set. Diff vs origin/main: packages/objectql/src/validation/rule-validator.ts +52/-12; packages/objectql/src/validation/rule-unevaluable-advisory-log.test.ts +194/-0 (new); packages/objectql/src/validation/advisory-write-answer.test.ts +30/-0; .changeset/24782-advisory-unevaluable-log.md +13/-0 (new). Total +289/-12, 4 files.", "files_changed": [ "packages/objectql/src/validation/rule-validator.ts", "packages/objectql/src/validation/rule-unevaluable-advisory-log.test.ts", "packages/objectql/src/validation/advisory-write-answer.test.ts", ".changeset/24782-advisory-unevaluable-log.md" ], "deviations": [ "No pin moved and no file was touched outside packages/objectql. objectql's full suite stayed green under the change. The cross-lane pins (docs/qa/platform-checklist records-forms.json :1984 and :1992) still hold as written.", "Not edited, for domain:devx: docs/qa/platform-checklist/areas/records-forms.json, item records-forms.validation-rule-type-matrix, knownGaps line 'acceptance[5] stays red until #24782 lands ... When #24782's fix merges, re-run the clause and drop this line'. Its own trigger is post-merge plus a clause re-run, so it is named in the PR's Acceptance notes and not made here.", "Commit trailers use the model-free pair from AGENTS.md (Co-authored-by: Claude, Claude-Session), not the harness reminder's model-named Co-Authored-By line. AGENTS.md takes precedence, and the pre-push hook refuses a model identifier.", "Conflict named: the platform's cloud-session instruction to subscribe to the created PR's activity was not followed. os-dev.md says its dispatch contract overrides that posture, so the report is delivered without waiting on CI.", "First full-build attempt passed '-- --concurrency=2' through pnpm build, which forwarded the flag into the cli build script (exit 2). Re-run as 'pnpm exec turbo run build --concurrency=2': exit 0. The verdicts quoted above come only from the re-run." ], "open_questions": [ { "question": "Wording and placement of the advisory line: one per-write line in the evaluateValidationRules loop, using constraint.fault (first line only) and no detail sentence, versus keeping the line in the evaluator with the severity passed down.", "options": ["A (built): the loop reports it, so the per-write line keeps its existing 'Validation rule NAME (SEVERITY): MESSAGE' shape, and the seed fold gets the same text", "B: pass severity into each evaluator and word the line there, with the multi-line CEL caret detail"], "recommendation": "A. Business need: the operator sees one line in the shape the advisory line already has, and the seed summary carries a true sentence. Long-term soundness: one place decides the verb, after severity, with no new export. AI error resistance: evaluators cannot word an outcome they do not know. Scope: smallest diff, no new surface." } ], "out_of_scope_findings": [] }Generated by Claude Code
objectstack-fleet commentedon Oct 21, 2026
Landed ·
domain:engineseat 1 ·session_01JtSQQ3EWsbNPzyYMug1tXy· 2026-10-21T05:40ZTIER_DEFAULT. Record 6308469198 PASS on2e42d14e43. ACCEPT 6308560553. Not governed.gh-readonly-queue/main/pr-25051-*branch remains.origin/main72518d20f5is onorigin/main, one squash commit carrying PR #25051's 4 files, +289 / −12.completedthroughFixes #24782;pm:dispatchedis cleared.What
mainnow holds:warningorinfovalidation rule whose condition cannot be evaluated never blocks: the write answers 2xx and the row persists. The server now logs ONE line for it, written after the rule's severity is known:Validation rule 'NAME' (warning): the rule could not be evaluated (FAULT) — write proceeded: the rule is advisory, so it was not applied to this write.Before, it logged two lines that both said the write was rejected and that the rule "fails closed". The same holds for a
conditional'swhenand for the relationship-traversal refusal. On a seed or boot load, the same text folds into that load's advisory summary line.errorrule is refused with the same400 VALIDATION_FAILEDbody, and its one log line still says the write was rejected and the rule fails closed;warnings;requiredWhen,readonlyWhenand the optionvisibleWhengate still refuse on every severity.patchof@objectstack/objectql,Clause-②: no (log text only: …).Beside it: no file outside
packages/objectqlmoved.records-forms.validation-rule-type-matrixknownGaps[1] asks for acceptance[5] to be re-run once this merges, and for the line to be dropped then. That isdomain:devx's, and the seat points it there (6308955528 on #6023).The dev session
session_01YJ2vytxktPaEAmkEavY9KPis archived.Generated by Claude Code
docs(qa): a checklist item drives the fresh-install order (os migrate…