Repository navigation
feat(spec): a hook can tell the person who saved a record what the save set in motion - #28247
Conversation
…ve set in motion The single-record create and update answers declare an optional `notice`: one author-written message, info severity, distinct from `warnings`. A hook sets it through the write's own `onNotice` listener (`WriteObservabilityOptions`) on `ctx.input.options`; metadata-protocol's createData / updateData pass the listener and answer the last non-empty message. The engine admits the option on update; the client mirrors the key. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibyvb1yBsrocHdqS4Ua
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibyvb1yBsrocHdqS4Ua
…-qo9o0z # Conflicts: # packages/client/src/index.ts # packages/metadata-protocol/src/protocol.ts # packages/objectql/src/engine-unknown-option.test.ts # packages/objectql/src/engine.ts # packages/spec/src/api/protocol.zod.ts # packages/spec/src/contracts/data-engine.ts
…ract pages Regenerates the protocol reference and the api authorable-surface shard for the new answer member, and adds the changeset. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibyvb1yBsrocHdqS4Ua
📓 Docs Drift CheckThis PR changes 4 package(s): 14 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 6 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 175 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 acc7f1dddcd6b2bc1219a856422bd19516e54ffb && git checkout acc7f1dddcd6b2bc1219a856422bd19516e54ffb
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 7c3cc889f7c64d465486bb94ded8c8ea558d6945 e7c4791296d3968491bf485b3fe724838e0b7081 && git checkout -B drift-repro 7c3cc889f7c64d465486bb94ded8c8ea558d6945 && git merge --no-ff e7c4791296d3968491bf485b3fe724838e0b7081
node scripts/docs-audit/affected-docs.mjs --json 7c3cc889f7c64d465486bb94ded8c8ea558d6945
|
|
domain:spec#2 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract-review-claim: |
|
domain:spec#2 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract reviewServed-tier: Inputs: card #28000 (body, claim 6528694653, os-dev-report 6531067088), PR body, the 14-file diff against merge base 3739d8d, the head's check-runs (read 2026-10-30T19:21Z: check-runs on e7c4791: 55 total, 42 success (Auto Label, Build & E2E UI, Build Core, Build Docs, Build UI Deps, Check Changeset, Check Documentation Links, Check PR Size, Dogfood Regression Gate, Flag docs affected by code changes, Governed Surface Queue Guard, Lint & Repo Gates, Lint · CI, release & engine gates, Lint · ESLint, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · repo & tooling gates, Lint · scoped · query-options erasure, Lint · scoped · slot lookup & corpus, Lint · scoped · tooling self-tests, Live E2E UI, PR-body guards (claim · duplicate · Part-of · single-writer path), Plan Test Core shards, Spec property liveness, Test Core, Test Core (all 8), Test UI, Type Check UI, Type Check · consumer gates, Type Check · debt ledger, Type Check · source gates, Type Check · workspace, TypeScript Type Check, filter); 13 skipped path-filtered (18.x upgrade harness (opt-in), Auto Label, Check PR Size, Dogfood Regression Gate (matrix shard/3), Dogfood Verify CLI, Lint UI, Packed-tarball smoke (opt-in), Schedule gate · skip when a green push run covers the commit, Temporal Conformance (live PG + MySQL), Test UI (matrix shard/16), UI Published Artifacts); 0 failure, 0 in progress). Author ① Derived judgments
② Semver levelminor. An optional response-only member on two answers and an optional listener on the in-process write contract widen the protocol an author and a client read ( ③ Boundary flags
④ Design judgmentThe change holds against the code it cites and the scenario it is accepted against. Against the card: an author-written notice on the single-record create and update answers, one message, info severity, distinct from Implemented-by: VERDICT: PASS |
|
domain:spec#2 · os-tesla · session_01L9qibx5r5nhDMwpRs4SL6D 维护者速读
席位核对 · ACCEPT ·
|
domain:spec#2 · os-tesla · session_01L9qibx5r5nhDMwpRs4SL6D
Requested by Tesla · project thread
Fixes #28000
Clause-②: yes (widening)
Metadata-protocol: added
Skills-docs: follow-up #28253 (skills/objectstack-data/rules/lifecycle.md, to land with the server half #28251)
维护者速读
notice:一条由作者写的提示,告诉保存的人这次保存触发了什么(例如「已提交经理审批」),信息级,和warnings分开。钩子通过本次写入自带的onNotice监听器设置它,createData/updateData把最后一条非空消息答回去;客户端 SDK 同步声明这个键。record:alert横幅来补。packages/spec/src/contracts/data-engine.ts的onNotice、packages/spec/src/api/protocol.zod.ts的WRITE_ANSWER_NOTICE_DESCRIPTION、packages/metadata-protocol/src/protocol.ts的createData/updateData。pnpm --filter @objectstack/objectql exec vitest run src/protocol-save-notice-answer.test.ts,预期 14 条全绿。What changes
A
beforeInsert/beforeUpdate/afterInsert/afterUpdatehook could not tell the person saving what the save set in motion. The single-record write answer carriedwarnings(advisory validation-rule hits) and nothing else a save could say, so HotCRM shows arecord:alertbanner after the fact. This is the spec half of #28000: the answer key, and the contract a hook uses to set it.Transport. No new path. It is the one #27703 used: the engine already hands every hook the caller's own options bag on
ctx.input.options(the PHASE rule inhook.zod.ts: abefore*handler reads it as passed, anafter*handler afterbuildDriverOptionswidened it, never stripped), andcreateData/updateDataalready collect in-process listeners from it. This PR adds one more listener of that family. Without the transport the answer key would be declared with no producer.packages/spec).CreateDataResponseSchemaandUpdateDataResponseSchemadeclare an optional, response-onlynotice: string(non-empty), with one shared description: one author-written message, info severity, distinct fromwarnings, the last call wins, omitted when no hook set one, never stored, never carried from a nested write.CloneDataResponseSchemadoes not get it: the card names the single-record create and update answers.WriteObservabilityOptions(contracts/data-engine.ts) declaresonNotice?: (message: string) => void. Its docblock says the engine never calls it, how a hook reaches and calls it (with the typed cast the platform's own hooks use), that the sandboxedbodyface does not carry it, the one-message rule, and that it never crosses the wire.HookUpdateOptionsnames it among the members it inherits.content/docs/references/api/protocol.mdxandauthorable-surface/api.jsonregenerated (two answer keys each).createDataandupdateDatapassonNoticeand answernoticeomit-when-absent. The listener keeps the last message that is a non-empty string; a blank or non-string call sets nothing and never clears an earlier message. The variable is local to the request, so no other request's answer can carry it.ENGINE_UPDATE_OPTION_KEYSadmitsonNotice, becauseengine.updaterefuses unknown option keys (inserthas no such allowlist). The drift pin inengine-unknown-option.test.tsnames it.CreateDataResultandUpdateDataResultdeclarenotice?: string, mirroring the two schemas.content/docs/api/data-api.mdx(POST / PATCH) andcontent/docs/kernel/contracts/data-engine.mdx(WriteObservabilityOptions, a newonNoticesection with anos:checkexample thatcheck:skill-examplestype-checks)..changeset/28000-save-notice-answer.md:@objectstack/spec,@objectstack/metadata-protocol,@objectstack/objectqland@objectstack/client, allminor, with both declaration lines.The shape, and the alternatives not taken.
warningswith aninfoseverity.warningselements are validation findings (field,code,rule) that the console renders as a problem with the save. A notice reports an outcome, has no field or rule, and the console shows it with, or in place of, its success toast. Folding it in would make every reader ofwarningstell the two apart by a severity value.after*hook knows more than abefore*one about what the save actually started. A list would push the merge rule onto every client.HookContextmember. AHookContextmethod needs the engine to bind it at every dispatch site and the sandbox runner to marshal it, which is the server half the card files separately. Declaring one here with no producer would be a declared-never-produced key.Tests
packages/spec/src/api/write-answer-notice.test.ts(8): the member is accepted on both answers and an answer without it stays legal; a list, an object and an empty string are refused; it sits besidewarnings, never folded in; the clone answer does not declare it; the description is shared and says it is never stored and the last one wins; the listener type takes one message and may be absent.packages/objectql/src/protocol-save-notice-answer.test.ts(14) uses a realObjectQLand the realObjectStackProtocolImplementation, with hooks on both surfaces an app reaches the bag from: the rawregisterHookenvelope and a declarativehandlerbound throughbindHooksToEngine. It pins: abeforeInsertand anafterInserthook each set the create answer'snotice, and the answer conforms; no key when no hook set one; the last call wins; a blank message sets nothing and never clears an earlier one; a refused write answers its error; nothing is stored; a nested write's notice never reaches the outer answer; a second request is not told the first one's notice;beforeUpdateandafterUpdateset the update answer'snoticeand it conforms; a declarativehandlerreaches the same listener on create and on update.engine-unknown-option.test.ts:onNoticepasses onupdate, and the drift pin names it.Package suites and typecheck, on
e7c47912pnpm --filter @objectstack/objectql test: Test Files 500/500, Tests 9738/9738.pnpm --filter @objectstack/metadata-protocol test: Test Files 284 passed, 4 skipped; Tests 21860 passed, 26 skipped.pnpm --filter @objectstack/client test: Test Files 52/52, Tests 682/682.pnpm --filter @objectstack/spec test: Test Files 760/760, Tests 22573 passed, 1 todo.typecheckfor@objectstack/spec,@objectstack/metadata-protocol,@objectstack/objectqland@objectstack/client: lock VERDICT command-exit 0.origin/main(head7f5907b4): the spec contract test 8/8; the objectql transport and option tests 39/39.Ablations (one-shot, through
scripts/ablation-replace.mjs, each restored to blob == HEAD)The transport test resolves
@objectstack/metadata-protocolthroughdist/, so each leg rebuilt it and checked the marker withablation-dist-preflight.mjs(present on the mutation leg, absent on the restore leg).metadata-protocolprotocol.ts)createData: theonNoticelistener replaced by one that records nothingupdateData: theonNoticelistener replaced by one that records nothingcreateData: the blank-message guard removedThe
os:checkexample indata-engine.mdxwas mutated once to pass a number toonNotice:check:skill-exampleswent red at that line (exit 1), and green again after the restore.Acceptance notes
body(including ahandlerthe build lowers into one, which is how HotCRM's hooks run) seesctx.input.optionsas the frozen{ multi, where }projection, and a record-triggered flow has no hook context at all. Giving those two a way to set the notice (aHookContextmember the engine binds and the sandbox marshals, and a flow node) is the server half, a follow-up card.createDataandupdateDatapass the listener, as the card scopes it. A hook on a write made bycreateManyData/batchData, a clone or a flow node finds no listener and sets nothing.noticewith, or in place of, the record-named success message in AppContentcrud_success,RecordFormPageand the inline edit save) is thedomain:uifollow-up card.Verification
Head
e7c47912(branch merged withorigin/mainat3739d8d6, after #28174 landed; the one latermaincommit touches none of these files).node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackagainst this diff: 109 families. Each ran with stdin from/dev/null, and each exit code was recorded before any pipe. Reconciled with--ran: 109 derived, 108 run, 1 NOT MEASURED, 0 unrun, 0 red.check:dual-build-cjs-loads(exit 3, needs a whole-repopnpm build). CI's.node scripts/check-plugin-teardown-shape.mjs --self-test(its pinned control621a4876was missing from the shallow clone; fetched, 48 cases) andspec check:skill-examples(needed the client SDK dist; built, 277 examples type-check). The record carries the re-runs.pnpm --filter @objectstack/spec check:generated: after a rebuild of the merged source,check:docswas stale and--fixregeneratedcontent/docs/references/api/protocol.mdx; the re-check passed. The build rewroteauthorable-surface/api.json; againstmainit adds only the twonoticerows.!in the title, no(narrowing)arm).Generated by Claude Code