Repository navigation
feat(spec,objectql,metadata-protocol,client): a write answer carries its advisory validation-rule hits as warnings - #22749
Conversation
…its advisory validation-rule hits as warnings evaluateValidationRules returns the advisory (warning/info) hits of an evaluation; the engine hands them to the write's own onValidationAdvisory listener (WriteObservabilityOptions, beside onFieldsDropped), so a nested hook write never reaches the outer answer. createData, updateData and cloneData answer them as warnings, and the validate-only preview appends the same elements to each accepted row's warnings. ValidateDataIssueSchema gains optional rule and severity and is the shared element. Claude-Session: https://claude.ai/code/session_016njDy8ozy9B9Ns5Y8kAWEK Co-authored-by: Claude <noreply@anthropic.com>
…ec artifacts Pins the card's three (hit answered and write stored; an error rule refuses; no hit, no key), nested isolation (cross- and same-object hook writes), the unchanged per-write log line and the seed-load aggregation, and the preview's P2 order. Regenerates api-surface, export-origins, declaration-map, the reference docs and the strictness-ledger counts for the new schema and keys. Types the emitter's logger as the evaluator's. Claude-Session: https://claude.ai/code/session_016njDy8ozy9B9Ns5Y8kAWEK Co-authored-by: Claude <noreply@anthropic.com>
… eager loading, and the import relay Claude-Session: https://claude.ai/code/session_016njDy8ozy9B9Ns5Y8kAWEK Co-authored-by: Claude <noreply@anthropic.com>
…ory pins Claude-Session: https://claude.ai/code/session_016njDy8ozy9B9Ns5Y8kAWEK Co-authored-by: Claude <noreply@anthropic.com>
…tener Claude-Session: https://claude.ai/code/session_016njDy8ozy9B9Ns5Y8kAWEK Co-authored-by: Claude <noreply@anthropic.com>
…ite-answer-warnings
…122); the advisory pins' find double holds the caller's bound Claude-Session: https://claude.ai/code/session_016njDy8ozy9B9Ns5Y8kAWEK Co-authored-by: Claude <noreply@anthropic.com>
…ite-answer-warnings
…nAdvisoryEventSchema (775 -> 776) Claude-Session: https://claude.ai/code/session_016njDy8ozy9B9Ns5Y8kAWEK Co-authored-by: Claude <noreply@anthropic.com>
…ite-answer-warnings
… requiredPermissions text beside this branch's warnings) Claude-Session: https://claude.ai/code/session_016njDy8ozy9B9Ns5Y8kAWEK Co-authored-by: Claude <noreply@anthropic.com>
…ite-answer-warnings
📓 Docs Drift CheckThis PR changes 4 package(s): 12 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 8 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 143 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 cd771b496d89afb8c21922c8bceb7bbfc7445f18 && git checkout cd771b496d89afb8c21922c8bceb7bbfc7445f18
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a18c514965f90e09fa9432c3e72e34426ebc03d6 dd3df908b7e9d215b63a79416fe716566680815c && git checkout -B drift-repro a18c514965f90e09fa9432c3e72e34426ebc03d6 && git merge --no-ff dd3df908b7e9d215b63a79416fe716566680815c
node scripts/docs-audit/affected-docs.mjs --json a18c514965f90e09fa9432c3e72e34426ebc03d6
|
|
Generated by Claude Code |
Contract reviewServed-tier: Inputs: card #22726 (body; claim ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
…ite-answer-warnings
Contract reviewServed-tier: Delta re-review. The record of ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
…e onValidationAdvisory The WriteObservabilityOptions section said the write methods accept two in-process options and its os:check block listed two members; the in-process callout said 'both members' and read as if a remote caller had no channel for any of them. It now lists the third member and its event, adds its subsection, and says how a remote caller sees advisory hits: as the protocol answer's warnings. hook-api.ts's enumeration gains the member too. Claude-Session: https://claude.ai/code/session_016njDy8ozy9B9Ns5Y8kAWEK Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Delta re-review. The record of ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #22726
Clause-②: yes (widening)
A
severity: 'warning'or'info'validation rule never blocks a write. Until now its hit was only logged on the server, so no client could show the person who saved what the rule says. This PR gives the hit a channel to the write's caller, as the seat ruled on the card (answer6101895557: route L, preview P2). The rule semantics, the per-write log line and the seed/boot aggregation are unchanged.What changed
Evaluator (
packages/objectql/src/validation/rule-validator.ts).evaluateValidationRulesreturns the advisory hits of its evaluation, asValidationAdvisoryEvententries ([]when there are none), instead ofvoid.warnline and the seed/boot aggregation stay byte-identical.emitValidationAdvisorieshands one evaluation's hits to a write's listener. A throwing listener is logged and ignored, asonFieldsDroppedis.constraint.reason: 'unevaluable') is not a hit. Its text says "could not be evaluated ... write rejected", which is false for an advisory that never blocks, so it stays in the log only.Engine (
packages/objectql/src/engine.ts): 8 changed lines, all at the sites the seat authorized (numbered here as they were ate5899a67d6; re-anchored under Collision duty), none elsewhere::281) andENGINE_UPDATE_OPTION_KEYS(:615), with its drift pinengine-unknown-option.test.ts;:14788), the by-id update wrap (:16376) and the two multi-update wraps (:16679,:16695);:13852/:13864), which appends the hits to the accepted row'swarnings.Each wrap passes the hits to that write's own
options.onValidationAdvisory. A nested write a hook or a flow makes inside it carries its own options, so its hits cannot reach the outer answer by construction.Contract (
packages/spec).WriteObservabilityOptions.onValidationAdvisorysits besideonFieldsDropped(in-process, never serialized).ValidationAdvisoryEventSchemasits indata/data-engine.zod.ts, besideDroppedFieldsEventSchema.ValidateDataIssueSchemagains optionalruleandseverity, both taken from the event schema, so an event IS an issue.CreateDataResponseSchema,UpdateDataResponseSchemaandCloneDataResponseSchemadeclare an optionalwarningsof that element.warningsdescription now names both populations, in order: the warn-first value-shape admissions, then the advisory rule hits.ValidateDataIssueSchema(and the shared description) moved above the write answers that reference it. UnderOS_EAGER_SCHEMAS=1they would otherwise read it before it exists; a pin and its reverse verification are below.Protocol (
packages/metadata-protocol):createData,updateDataandcloneDatapass the listener and answerwarningsonly when there is a hit (omit-when-empty, likedroppedFields).validateDatarelaysengine.validate, unchanged. REST is not edited: it relays the whole answer (rest-server.tscreateres.status(201).json(result), updateres.json(result)).Client (
packages/client):CreateDataResult,UpdateDataResultandCloneDataResultdeclarewarnings?: ValidateDataIssue[]. These types are hand-written mirrors of the spec answers, which is the condition under which the claim's file surface includes them.Docs:
content/docs/data-modeling/validation.mdx. The severity table no longer says "not returned to the caller", and a new paragraph with an example answer describeswarnings, the nested-write exclusion and the preview. Andcontent/docs/kernel/contracts/data-engine.mdx: theWriteObservabilityOptionssection names the third member, its event, anonValidationAdvisorysubsection, and how a remote caller sees advisory hits (the protocol answer'swarnings).Changeset:
.changeset/22726-write-answer-warnings.md. It bumps@objectstack/spec,@objectstack/objectql,@objectstack/metadata-protocoland@objectstack/clientasminor, withClause-②: yes (widening). A widening needs no ADR-0087 disposition marker.Pins
packages/objectql/src/validation/advisory-write-answer.test.tsruns a realObjectQLengine and the realObjectStackProtocolImplementation(21 tests):inforule in evaluation order. Every answer parses with its schema and emits no undeclared key.errorrule still refuses. The refusal carriescode: 'VALIDATION_FAILED'with the rule's field entry, and nothing is stored, even though an advisory rule also trips on that same write. The update path is covered the same way.warningskey at all.beforeInserthook writes another object, and anafterInserthook writes a same-object sibling, each tripping its own advisory. The outer answer carries only the outer row's hit. When the outer row has none, it carries no key. The update path is pinned the same way through anafterUpdatehook. The nested hits are shown to have fired (their log lines are counted), so the pin cannot pass by there being nothing to exclude.SeedLoaderServiceload gives one summary line for the rule and zero per-row lines.evaluateValidationRulesreturns[]when there is no hit.Other pins:
packages/spec/src/api/write-answer-warnings.test.ts(9 tests).severity: 'error'is refused on both schemas.warningsand also accept an answer without it.OS_EAGER_SCHEMAS=1.packages/core/src/utils/import-runner-advisory-warnings.test.ts(2 tests): the import dry run still relays the verdict's rowwarningsverbatim, rule hits included. The same element objects arrive in the same order withruleandseveritykept, and an empty verdict leaves no key. This is theimport-runner.tsrelay the seat asked to pin.packages/objectql/src/engine-unknown-option.test.ts: the drift pin gainsonValidationAdvisory, plus a case showing the update path accepts the key.packages/objectql/src/protocol-data.test.ts: the clone's exact insert-options pin now names the new listener.Ablations (one-off, not committed)
Both mutations ran through
scripts/ablation-replace.mjsagainst committed HEAD, with a trap restore. Each restore was proved by blob equals HEAD and an emptygit diff HEAD.Nested isolation can fail. The emitter was mutated to route hits through an ambient "current listener", which is the rejected route without its depth guard. Expected direction: red. Observed: 3 failed / 18 passed, exactly the three PIN 4 tests.
warningskey.The anchor hit x1 to x0 and the replacement x0 to x1, with blob
52a2caf219c8mutated toe004d96ccda0and restored.The eager-load pin can fail. The create answer's factory was mutated to also read a schema declared later in the file. Expected: only the eager pin goes red. Observed: 1 failed / 8 passed,
ReferenceError: Cannot access 'ValidateDataRequestSchema' before initialization. Restored to blob587c21e14441.Verification
Every heavy run went through
scripts/pm/os-verify-lock.sh. Each reading below names the HEAD it ran on.pnpm --filter PKG typecheck, which includes each package'scheck:test-typecheck):0f0f63364a; client green on7ecb442dd8;7a9273c451, after fix(objectql): an item registered through the registry path is served with its package's_packageVersion#22724's merge.pnpm --filter PKG test):7a9273c451(after fix(objectql): an item registered through the registry path is served with its package's_packageVersion#22724 landed).9f039e5e2b.9f039e5e2b. The two pin files touched since then (type-alias-convention.pin.test.ts,write-answer-warnings.test.ts: 12 tests) and spec's typecheck are green on83c76131ff.9f039e5e2b.pnpm --filter @objectstack/spec check:generatedis green on8cfab74045(after the last merge, with spec rebuilt: all 14 artifacts current).check:dual-build-cjs-loadsmeasured green on7a9273c451(107 published require entries across 66 packages load). As an extra stand-in, every require entry of the four packages this PR'ssrcmoves (23) loads both lazily and underOS_EAGER_SCHEMAS=1.ce576d2098(the head the contract review passed;8cfab74045only adds the merge of149294c02c), every commandnode scripts/pm/dispatch-gates.mjs --commandsderives, plus the three artifact-roster gates whose roster sits under a directory this PR touches (check-changeset-fixed,check:meta-url-spelling,check:authz-resolver): 121 commands, all exit 0.--ranreconciliation: "118 derived, 118 run, 0 NOT-MEASURED, 0 UNRUN" (a derived zero: every run recorded its exit code, none a refusal). Two gates had first refused for prerequisites, and both were remedied before this run rather than skipped:check-engine-split-ratiorefused on the shallow clone (history deepened), andcheck:skill-examplesneededclient-reactbuilt. Earlier runs caught three real findings on this branch, each fixed in place:check:objectql-double-limit(the new pin file'sfinddouble ignored the caller's bound),check:spec-parsed-alias(ADR-0122: the new event alias is isomorphic, now pinned intype-alias-convention.pin.test.ts, count 775 to 776), and theprotocol-data.test.tsexact insert-options pin.packages/cliis untouched; the repo-widepnpm lintbelongs to CI.Collision duty
origin/mainwas merged five times, after the fast-forward at the start.ce576d2098brought build(spec): the migration registry is generated at build and leaves git #22706 (the migration registry built at build time). The last merge,8cfab74045, brought149294c02c(with fix(spec): render-projection-diff generates a base's git-ignored migration registry #22750) immediately before its push;engine.tsstill differs only at the same 8 lines._packageVersion#22724 landed during the round (b62260cb81). Itsengine.tshunks sit at:253,:4071-:4097and:7297-:7774. None overlaps a site of this PR (nearest: its:253against this PR's:281), and the merge was clean.origin/mainafter the merge, the same 8 lines are::281(import) and:615(option allowlist);:13883/:13895(preview capture);:14819(insert),:16407(by-id update) and:16710/:16726(multi update).engine.tshunk is:3429.Acceptance notes
batchDataandupdateManyDataanswer per-rowBatchUpdateResponseresults, a different schema family. Their per-row writes do not pass the listener, so their rows carry nowarnings.createManyDataandinsertManyDataanswer at batch level, with no row slot. This is the same reasondroppedFieldsis a batch-level union there.deleteDataevaluates no validation rules (eventsadmitsinsertandupdateonly).packages/spec/src/api/export.zod.ts,ImportRowResultSchema.warningsdescription.warnings.check:docs/check:generated), and no new verification surface.packages/clientis in the claim's own file surface ("response types, only if they are hand-written"). They are hand-written, so they are widened, and@objectstack/clientis in the changeset.packages/spec/src/data/hook-api.ts: theHookUpdateOptionsdocblock now names all threeWriteObservabilityOptionsmembers (onFieldsDropped,onValidationAdvisory,strictReadonlyWrites); comment only.warnings(objectui, the card's foreseen follow-up). Theliveness/validation.jsonseverity evidence ("warning/infoare logged and the write proceeds") is still true and was not edited.Generated by Claude Code