Skip to content

fix(objectql): an unevaluable advisory validation rule logs one line saying the write proceeded, not two saying it was rejected - #25051

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-24782-advisory-unevaluable-log
Oct 21, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-24782-advisory-unevaluable-log

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #24782

Clause-②: no (log text only: no response, status, code, outcome or export moves)

What changes

An unevaluable severity: 'warning' / 'info' validation rule never blocks the write (#4649 fails closed on error rules only). But the server logged two lines for it, and both said the write was rejected. Now it logs ONE line, written after the rule's severity is known:

Validation rule 'qa_unevaluable_warn' (warning): the rule could not be evaluated (runtime: int() type error: cannot convert to int) — write proceeded: the rule is advisory, so it was not applied to this write

An unevaluable error rule is unchanged: the same 400 VALIDATION_FAILED body, and its one log line still says — write rejected: a rule that cannot be evaluated fails closed, it is never skipped.

Log-line census (packages/objectql/src/validation/rule-validator.ts, before the edit, at d68aab6fde)

Every line that says "write rejected" or "fails closed":

Line Reached Verdict
checkPredicate fail-closed warn ("predicate failed to evaluate … fails closed") BEFORE severity: reached only through evaluateRule, which the evaluateValidationRules loop and checkConditional's branch recursion call defect on advisory rules → now logged for error rules only
checkConditional when-predicate twin BEFORE severity, same callers same
traversalScopeRefusal warn ("predicate / when-predicate could not be evaluated … write rejected") BEFORE severity, from both of the above same
advisory per-write line in evaluateValidationRules ((warning): ${violation.message}) AFTER severity, but it embedded the unevaluable violation's message ("— write rejected.") for an unevaluable rule it now carries the advisory sentence instead
requiredWhen arm ("requiredWhen for 'X' failed to evaluate … write rejected") field-level, refuses on every severity true; untouched
option visibleWhen traversal refusal (~:3112) field-level, refuses on every severity true; untouched
unevaluableRuleError / traversalScopeRefusal violation message the wire text an error rule returns untouched (out of scope)

Callers: evaluateValidationRules is the one door for the engine's insert / update / multi-row update, the delete's reference-cleanup update (referentialClearRefusal runs inside checkPredicate, so the same seam covers it), and the validate-only preview. requiredWhen / readonlyWhen / option visibleWhen use their own evaluators and are not touched.

The seam

RuleContext gains a per-rule advisory flag, set by the evaluateValidationRules loop from the TOP-LEVEL rule's severity and inherited by a conditional's branch (its outer rule's severity governs). The predicate evaluators write their fail-closed sentence to failClosedLogger(ctx): the write's logger for an error rule, none for an advisory one. The loop then reports an unevaluable advisory rule once with unevaluableAdvisoryMessage (rule, severity, the fault from constraint.fault, and the branch name when a branch faulted). No new export. The violation and its message are untouched, so blocking, the ValidationError body and the #22726 advisories (an unevaluable rule stays out) do not move.

#13889 seed / boot aggregation: the hit still folds into the load's summary line, which keeps its shape. The text it folds is the advisory sentence, not the violation's "— write rejected." message. On that path the evaluator's per-row fail-closed line no longer prints for an advisory rule, so the summary is the only line.

Pin census

  • rule-fail-closed.test.ts :213: an error rule's warn matches /write rejected/, a stable part. Still green. :216 (warning) asserts only that a warn was logged. Still green.
  • rule-validator.option-visibility.test.ts, rule-validator.test.ts:380: field-level evaluators, untouched.
  • advisory-boot-path.test.ts:316, advisory-write-answer.test.ts perWriteLine: exact per-write line for a MATCHING advisory rule. Unchanged, still green.
  • docs/qa/platform-checklist/areas/records-forms.json: :1984 quotes a matching advisory's line (unchanged); :1992 scores the absence of "write rejected" / "fails closed" (now holds). content/docs/, qa/: zero hits. packages/lint/src/validate-expressions.ts:3266 (comment) and packages/spec/src/migrations/entries/semantic/18.field-predicate-reference-traversal-refused.ts:59 quote error-path / field-predicate text, still true.
  • Moved pins: none. objectql's full suite stayed green under the change.

Verification (head 2e42d14e4, origin/main merged)

  • New rule-unevaluable-advisory-log.test.ts (8 tests, real evaluateValidationRules with a logger spy): warning and info script conditions, a conditional when, and the traversal refusal each log exactly one line naming the rule, severity and fault, saying "write proceeded", with no "write rejected" / "fails closed". The write is not refused and returns [] advisories. The seed aggregation folds the advisory text with no per-row line. 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 on a runtime object with the card's rule stores the row, answers no warnings, and logs one advisory line.
  • Ablation: rule-validator.ts restored to d68aab6fde on disk (failClosedLogger count 6 → 0). Six advisory pins went red, and the three error controls plus the other 21 stayed green (Tests 6 failed | 24 passed (30)). Restored: blob 51aa0041 equal to HEAD's, git diff HEAD empty.
  • pnpm --filter @objectstack/objectql test: 453 files / 8533 tests passed (exit 0). typecheck: exit 0 (re-run after the merge). pnpm --filter @objectstack/core test:repo: 6 files / 82 tests passed. Full turbo run build on the merged head: 76 packages, exit 0.
  • dispatch-gates.mjs --commands (no paths): 70 lines, all exit 0 on 2e42d14e4; --ran reports "70 run, 0 NOT-MEASURED (a DERIVED zero)".

Acceptance notes

Session: session_01YJ2vytxktPaEAmkEavY9KP (os-dev, dispatched by domain:engine seat 1).

🤖 Generated with Claude Code


Generated by Claude Code

…saying the write proceeded

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJ2vytxktPaEAmkEavY9KP
…e evaluator and on an engine write

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJ2vytxktPaEAmkEavY9KP
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 21, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/objectql, touching 8 documentable anchor(s).

2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/kernel/runtime-services/sharing-service.mdx (via evaluateRule (symbol, a top-level function))
  • content/docs/permissions/sharing-rules.mdx (via /sharing/rules/:idOrName/evaluate (route, bridged from symbol evaluateRule — its route source's handler names it))

⛔ 1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx (via evaluateValidationRules (symbol, a top-level function))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 18 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 157ebd008b07d0683ee1762d6c858f4ad766618e → packageMentionDocs.

Which tree this was computed on

This run read content/docs from f646083565d6dc757a6f67ac3a112897d1074f5f — the merge of head 2e42d14e43a2c36103e4857a3118b06141e73a63 into base 157ebd008b07d0683ee1762d6c858f4ad766618e, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin f646083565d6dc757a6f67ac3a112897d1074f5f && git checkout f646083565d6dc757a6f67ac3a112897d1074f5f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 157ebd008b07d0683ee1762d6c858f4ad766618e 2e42d14e43a2c36103e4857a3118b06141e73a63 && git checkout -B drift-repro 157ebd008b07d0683ee1762d6c858f4ad766618e && git merge --no-ff 2e42d14e43a2c36103e4857a3118b06141e73a63

node scripts/docs-audit/affected-docs.mjs --json 157ebd008b07d0683ee1762d6c858f4ad766618e

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 157ebd008b07d0683ee1762d6c858f4ad766618e → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 2e42d14e43
Local-runs: none

Scope: the net diff against the merge base (4 files, +289/−12):

  • packages/objectql/src/validation/rule-validator.ts;
  • the new rule-unevaluable-advisory-log.test.ts, and one engine-level pin added to advisory-write-answer.test.ts;
  • the changeset.

Read with them:

① Derived judgments

  • The verb is now decided after severity: right.
    • RuleContext gains advisory. The loop in evaluateValidationRules sets it per rule from the top-level rule's severity ({ ...ctx, advisory: severity !== 'error' }). A conditional's branch inherits it through evaluateRule(branch, ctx), so the outer rule's severity governs, as it already did for blocking.
    • failClosedLogger(ctx) hands the evaluators the write's logger for an error rule and none for an advisory one.
    • It sits at the three places that wrote the fail-closed sentence before severity was known: checkPredicate (its own line and its traversalScopeRefusal), and checkConditional's when line and its traversal refusal.
    • checkPredicate's logger feeds nothing but those two lines, so nothing else goes quiet for an advisory rule.
    • The one RuleContext literal sets advisory: false, and the interface makes the member required, so no context can miss it.
  • One advisory line, worded as what happened: right.
  • Nothing that blocks or answers moves.
  • The pins have teeth.
    • Eight unit pins cover warning and info script conditions, a conditional's when, the traversal refusal and the seed fold. Each asserts one line with the rule, the severity, the fault and write proceeded, and neither rejection phrase.
    • Three error controls assert the refusal and its one fail-closed line.
    • One engine-level pin asserts that protocol.createData stores the row, answers no warnings, and logs one line.
    • In the ablation, the 6 advisory pins go red and the 3 controls stay green. The blob was restored equal to HEAD.
    • Not pinned: the its branch 'X' wording of an unevaluable conditional branch. It is the same seam and a one-line choice. It is noted, not owed.
  • The checklist:

② Semver level

  • patch for @objectstack/objectql: right for log text, with no response, status, code, outcome or export moving.
  • The changeset and the PR body carry the claim's Clause-②: no (log text only: …) line verbatim, at a line start.
  • No new export: failClosedLogger and unevaluableAdvisoryMessage are module-local, and RuleContext is an unexported interface.

③ Boundary flags

Implemented-by: claude/issue-24782-advisory-unevaluable-log
Reviewed-by: session_01JtSQQ3EWsbNPzyYMug1tXy

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · domain:engine seat 1 · session_01JtSQQ3EWsbNPzyYMug1tXy · 2026-10-21T05:09Z · ACCEPT

Head: 2e42d14e43. It carries the log-line change, its pins, the changeset and a merge of main.

Card #24782 (p3, bug, area:records; from QA run record #24779, re-reproduced at 6288115226): claim 6307306950, dispatch 6307346629. Dev report: 6308216242.

Item Reading
Shape Draft PR → main. The body opens Fixes #24782. 4 files, +289/−12. Not governed: check-governed-merges matched 0 of 4 paths; 301 changed lines.
Scope An unevaluable warning / info validation rule logs one line: it names the rule, its severity and the fault, and says the write proceeded because the rule is advisory. Before, it logged two lines that both said the write was rejected. An unevaluable error rule is refused as before, and its one log line still says the write was rejected and the rule fails closed.
Seat's own read The verb is chosen after severity: the loop sets advisory per rule, and failClosedLogger gives the evaluators no logger for an advisory rule. The loop then reports the rule once, through the seed fold or the per-write warn. No violation, refusal body, blocking decision or #22726 advisory event moves.
Review TIER_DEFAULT. Record 6308469198 is PASS on this head.
Declaration One changeset, @objectstack/objectql patch, with Clause-②: no (log text only: …) at a line start, as on the claim and the PR body. No export is added.
Cross-lane None. Every changed file is in packages/objectql or .changeset/, and no existing pin moved. The checklist's knownGaps[1] for records-forms.validation-rule-type-matrix is a post-merge re-run by its own words; the seat points domain:devx to it at landing.
Gates Dev at 2e42d14e4: 70 derived, 70 run, all exit 0, after a full build for the two that first exited 3. The new pins pass 8/8, and the engine-level pin passes (its file 22/22). In the ablation the 6 advisory pins go red and the 3 error controls stay green; restored to HEAD. objectql passed 8533 tests, typecheck exit 0, core test:repo 82 passed, full turbo build exit 0.
CI At this read on 2e42d14e43: ci-failure GREEN, all 41 check-runs completed, none failed. git merge-tree against today's origin/main (63085c3806) is clean.

Disposition:

  • open_questions: A, as the dev settled it. The loop words the line once, after severity, in the per-write line's existing shape.
  • deviations: all accepted: no pin moved; the knownGaps line is left to domain:devx; the model-free trailers; the PR subscription is held by the seat; and the first build attempt's flag error, re-run clean.
  • out_of_scope_findings: none.

Landing: ordinary queue (pr_ready + automerge_enable through the relay). Fixes #24782 then closes the card.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 21, 2026 05:10
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 21, 2026 05:10
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 21, 2026
Merged via the queue into main with commit 72518d2 Oct 21, 2026
42 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-24782-advisory-unevaluable-log branch October 21, 2026 05:39
os-warren pushed a commit that referenced this pull request Oct 21, 2026
… apply, then the first os serve), and validation-rule-type-matrix drops its red-until-#24782 text

- cli.fresh-install-migrate-apply-first (new, r1): on an empty database
  `os migrate apply --yes` writes the organization-ownership birth marker
  (sys_migration adr-0131-organization-ownership), and the first `os serve`
  reaches its serving line with no ownership refusal. Ships in 18.1.0; a run
  against 18.0.0 records the refusal as that release's known behaviour.
  Driven live at base 688052e.
- records-forms.validation-rule-type-matrix (r6 -> r7): knownGaps[1] and
  acceptance[5]'s "Red until #24782 lands" removed after a verification
  re-run of acceptance[5] at base 688052e read as #25051 fixed it.

The history rows carry the placeholder ref #PENDING-25054; the next commit
swaps in this PR's number and changes nothing else.

Claude-Session: https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

objectql(validation): an unevaluable severity:'warning' rule logs "write rejected: … fails closed" twice while the write is accepted 201 and persists

2 participants