Repository navigation
feat(metadata-protocol): the draft save records draftChanges per key path; the save declares its surface (?surface=studio|api, copilot in-process only) - #24418
Conversation
… and draftChanges.by Claude-Session: https://claude.ai/code/session_01Q3mCs3NX4GshNUCh7Fr1b9 Co-authored-by: Claude <noreply@anthropic.com>
…aft row Claude-Session: https://claude.ai/code/session_01Q3mCs3NX4GshNUCh7Fr1b9 Co-authored-by: Claude <noreply@anthropic.com>
…t draft read serves draftChanges Claude-Session: https://claude.ai/code/session_01Q3mCs3NX4GshNUCh7Fr1b9 Co-authored-by: Claude <noreply@anthropic.com>
…lot refused Claude-Session: https://claude.ai/code/session_01Q3mCs3NX4GshNUCh7Fr1b9 Co-authored-by: Claude <noreply@anthropic.com>
…= doors Claude-Session: https://claude.ai/code/session_01Q3mCs3NX4GshNUCh7Fr1b9 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q3mCs3NX4GshNUCh7Fr1b9 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q3mCs3NX4GshNUCh7Fr1b9 Co-authored-by: Claude <noreply@anthropic.com>
…aft-changes-writer
Claude-Session: https://claude.ai/code/session_01Q3mCs3NX4GshNUCh7Fr1b9 Co-authored-by: Claude <noreply@anthropic.com>
…orphic (ADR-0122) Claude-Session: https://claude.ai/code/session_01Q3mCs3NX4GshNUCh7Fr1b9 Co-authored-by: Claude <noreply@anthropic.com>
…thoringSurfaceSchema Claude-Session: https://claude.ai/code/session_01Q3mCs3NX4GshNUCh7Fr1b9 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q3mCs3NX4GshNUCh7Fr1b9 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 6 package(s): 22 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 8 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 149 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 685bdb95570fc5492c6d8fbde0d63104b7af3efd && git checkout 685bdb95570fc5492c6d8fbde0d63104b7af3efd
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 31ce45c071bbcb6e92a730ce19ebf30dcfa361cb 49a8e27c6ab74145ea0bfbe6b9088dba3eb584ff && git checkout -B drift-repro 31ce45c071bbcb6e92a730ce19ebf30dcfa361cb && git merge --no-ff 49a8e27c6ab74145ea0bfbe6b9088dba3eb584ff
node scripts/docs-audit/affected-docs.mjs --json 31ce45c071bbcb6e92a730ce19ebf30dcfa361cb
|
…aft-changes-writer
Claude-Session: https://claude.ai/code/session_01Q3mCs3NX4GshNUCh7Fr1b9 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q3mCs3NX4GshNUCh7Fr1b9 Co-authored-by: Claude <noreply@anthropic.com>
|
Generated by Claude Code |
Contract reviewServed-tier: Inputs read: card #24395 (body; the claim 6255766595, the dispatch 6255818963, the dev's report 6257483184 and its fix-round report 6259140498, all as claims); the seat's Check-runs on ① Derived judgments
② Semver levelThree changesets ship with the diff, each The foreign changeset edit, ③ Boundary flags
Implemented-by: VERDICT: PASS Every served-set and accept-set change in ① is right as judged; the three changesets and the PR body carry the level the diff takes; the one red check is the foreign-changeset rule on a deliberate correction whose rewritten sentence is true for the release that ships both notes, and that decision (A / C′) is the maintainer's. Rendered 2026-10-19T04:30Z. Generated by Claude Code |
席位核对 ·
|
| Item | Reading |
|---|---|
| Shape | Draft PR → main. The body opens Fixes #24395. 39 files, +883 / −48. Not governed (check-governed-merges.mjs --pr 24418: no path hits the register; 931 lines, under the human-merge threshold). |
| Scope | One closed set, MetadataAuthoringSurfaceSchema, shared by SaveMetaItemRequestSchema.surface and draftChanges[].by. The draft save records per-key entries on a nullable sys_metadata.draft_changes column inside put's transaction; the strict draft read serves them. Both HTTP doors accept ?surface=studio|api and refuse copilot and every other value 400 VALIDATION_ERROR; in-process, an off-set value is refused 400 INVALID_REQUEST. No new error code. |
| Seat's own read | At the head: metaSaveRequestOptions refuses copilot by an explicit branch and anything outside {studio, api} by exact match; saveMetaItem passes draftChangesBy only when mode === 'draft' and no source is stated; put records only for state === 'draft' with that stamp; getMetaItem reads the column only when readState === 'draft' and spreads the key only when non-empty; storedDraftChanges never answers []. These match the record's ① 3 to ① 11. |
| Review | CONTRACT_REVIEW_TIER record of record 6259630019, on this head: PASS. |
| Declaration | Clause-②: yes (widening) on the claim, the PR body and each of the three changesets. |
| Cross-lane | domain:spec 6255777346, 6255780671 and addenda 6259433698, 6259437563; domain:cli 6255785533, 6255791182 and addenda 6259421806, 6259427539; domain:devx 6259442690, 6259447625. |
| Gates | Dev, on c105df36c0: 118 derived, 118 run, 117 exit 0. The one exit 1 is check-empty-changeset, the note below. |
| CI | At this read on c105df36c0: 41 runs, 35 success, 5 rostered skips, 1 failure. The failure is Check Changeset (run 117861905774), and its only file annotation is the foreign-changeset rule on .changeset/24298-draft-key-provenance.md. |
Disposition:
open_questions: 1 (query spelling, A), 2 (column home, A) and 4 (surfaceon an active save, A) accepted, as the record's ③ 11, ③ 12 and ③ 14 judge them. 3 (first-save baseline on a code-only item) accepted as shipped and stated in the describe; the follow-up card is filed only if Studio's reader over-marks (objectui#12725 item 3). 5 (the foreign changeset) is the maintainer's, below.deviations: thepackages/restclosed-set and arity rows and the eightpackages/objectqlfixture stubs, accepted (③ 1, ③ 9). The dev detached its own container's primary checkout to cut the worktree; noted for the seat's next prompt, not a defect of the diff (③ 8).out_of_scope_findings: cloud'sstageDraft/applyDraftpassingsurface: 'copilot', and objectui#12725 item 3's reader: both pointers are sent at landing. The dispatcher's405forDELETEandPOST /publishon an item is deliberate (its own comment names a card); noted, not filed.
Landing: held on one decision. The PR rewrote the pending 24298-draft-key-provenance.md sentence "No server writes the key yet", which this PR makes false in the same release (record ②; blocker note 6258430779). The maintainer chooses:
- A: the correction stays here, confirmed on this PR as a deliberate correction;
- C′: the note is restored from base and the correction is stated in this PR's own changeset (the seat's lean). A Version Packages cut that lands first would otherwise conflict with the edited file.
After the ruling: the ordinary queue (pr_ready + automerge_enable through the relay). A C′ head that moves only .changeset/ keeps this record; the seat re-checks the delta before it queues.
Generated by Claude Code
…aft-changes-writer
…ased 24298 note is left as main has it Claude-Session: https://claude.ai/code/session_01Q3mCs3NX4GshNUCh7Fr1b9 Co-authored-by: Claude <noreply@anthropic.com>
席位核对 · delta ·
|
| Item | Reading |
|---|---|
| Why the round | The Version Packages cut 725a5b1311 (#24168, @objectstack/spec 18.0.0-next.4) published 24298-draft-key-provenance.md and moved it to .changeset/pre/. Its sentence "No server writes the key yet" was true for next.4, which ships no writer. That closed the A / C′ question without a ruling and made this PR dirty. |
| Net diff, seat's own read | Against its base, the PR's net file list on 49a8e27c6a equals the one on c105df36c0, less .changeset/24298-draft-key-provenance.md: 38 files, +882 / −47. Per file, the net patch is byte-identical except .changeset/24395-draft-changes-surface.md, which gains one sentence: "18.0.0-next.4's note said no server writes draftChanges yet; from this release the draft save writes it." packages/spec/authorable-surface/api.json nets to the same one line, api/SaveMetaItemRequest:surface. The released note in .changeset/pre/ is main's, untouched. |
| Record | The only path that moved is a changeset, a prose face, so the CONTRACT_REVIEW_TIER record 6259630019 (PASS) carries to this head. |
| Gates | Dev, on 49a8e27c6a: 118 derived, 118 run, 118 exit 0. check-empty-changeset --base origin/main exit 0; check:generated exit 0. |
| Landing | The ordinary queue (pr_ready + automerge_enable through the relay) once CI on 49a8e27c6a is read green. |
Generated by Claude Code
Fixes #24395
Clause-②: yes (widening)
What this does
GetMetaItemResponseSchema.draftChangeslanded in@objectstack/spec(#24383) with no writer. This PR is the writer. Its first deliverable is a save request that declares its authoring surface.MetadataAuthoringSurfaceSchema(copilot | studio | api) is now one exported schema.SaveMetaItemRequestSchema.surfaceuses it, and so doesdraftChanges[].by(the inline enum is gone, same values). An absentsurfacestampsapi.?surface=studio|apionRestServer'sPUT /meta/:type/:nameand on the runtime dispatcher's. It joins the closed query setMETA_ITEM_SAVE_PARAMSand its arity list, and is read once inmetaSaveRequestOptionsbeside?mode=. Both doors answer400 VALIDATION_ERRORfor?surface=copilot(only an in-process tool may state it), for any other value (a case variant included, never folded toapi), and for a repeated?surface=. No new error code: in-process, a value outside the set is400 INVALID_REQUEST, the save door's existing request-envelope refusal.SysMetadataRepository.putwithstate: 'draft'records the entries insideresolveWriteHead's transaction. It compares the new body with the body it replaces: the draft row, else the active row the draft's publish would replace (that package's row, else the package-less one), else nothing, which records[]. A path is object keys only, and an array is one leaf. The keep rule: every entry at or under a changed path is replaced, so no two entries share a path. A save that changes no path leaves the column as it was.actorisopts.actor ?? null, the value the history event carries.atis the write's ownnow(toISOString()). The pure rules live inpackages/metadata-protocol/src/draft-changes.ts.saveMetaItempassesdraftChangesBy: surface ?? 'api'only on an authoring draft save. Amigrate-storedrewrite (the only caller that states asource) records nothing, and neither does an active save.getMetaItemwithstate: 'draft'from a stored row) addsdraftChangesbesideitem, taken from the row it serves. It is absent on the active read, the cached branch,?preview=draft, and a draft row that recorded none, and never[]. Neither HTTP door needed a transport change (review 6253694122 ③ 4 held: measured below).sys_metadata.draft_changescolumn (SysMetadataObject, JSON text), on the draft row and never insidemetadata. Publish (promoteDraft's drain) and discard (discardDraftInItsScope) delete the row, and the entries go with it.Where it landed outside the claim's file surface, and why
packages/rest/src/meta-item-read-gate.ts(META_ITEM_SAVE_PARAMS) andpackages/rest/src/meta-item-write-routes.ts(the REST door's arity list). Both doors' closed query sets live there; without them?surface=is refused as an unknown parameter (measured before).packages/metadata-protocol/src/draft-changes.ts(new), holding the recorder's pure functions, and its test.packages/platform-objects/src/apps/translations/*.generated.ts: the new column's label, regenerated bynode scripts/check-i18n-bundles.mjs --write. The zh-CN, ja-JP and es-ES values are hand-translated, which is allowed for values.packages/spec/src/type-alias-convention.pin.test.ts: the ADR-0122 isomorphic pin for the new enum (check:spec-parsed-aliasasked for it), with the count moving from 788 to 789..changeset/24298-draft-key-provenance.md, a deliberate correction of a pending release note, which needs a person's confirmation. Its bullet said "No server writes the key yet". That sentence becomes false the moment this PR ships, and both notes are pending for the same release. I rewrote that one sentence.check-empty-changesetstays red on it by design, and its own text says this class is not to be restored from base. The seat or maintainer confirms or rejects the correction.content/docs/references/api/protocol.mdx,content/docs/references/index.mdx,packages/spec/{api-surface,authorable-surface,declaration-map,export-origins,json-schema.manifest}/api.json(check:generated --fixplus the spec build).protocol.packageless-save-address.test.ts, which reuses that file's ledgered engine double rather than registering a new one. The both-doors block is appended topackages/runtime/src/domains/meta-save-preconditions-parity.test.ts, which runs over a real SQL store.Verification (M, booted)
REST door, booted
pnpm dev:crm --fresh --database file:persist.db(SQLite), before and after:d79dc4883c)PUT ?mode=draftdraftChanges[{path:[],by:"api",actor:"Voj5…",at:"…Z"}]?surface=studio/apiSupported parameters: force, mode, package.)by:"studio"/by:"api"?surface=copilotVALIDATION_ERROR, nothing written?surface=STUDIO, a repeatedsurfaceVALIDATION_ERROR(value / arity)[["columns"],api]only[["columns"],api], [["label"],studio][["label"],studio], [["columns"],studio](replaced at the path, one entry)?preview=draft[["label"],studio]POST …/publish→ rows[('active', None)]; next draft records only["description"]DELETE ?state=draft→ rows[('active', None)]; next draft starts cleanExisting deployment gains the column: the
beforeboot createdsys_metadatawithoutdraft_changes(pragma table_infolisted 24 columns, nodraft_changes). Booting the after-build on the same file added it, with no manual step, and the pre-writer row readsNULL.Dispatcher (a real
createHonoAppcatch-all over the real protocol), on SqliteWasm and on the memory driver, with an in-processsaveMetaItem: before, every?surface=was 400 unknown (Supported parameters: mode, package.) and no read carried the key. After: none,studioandapistampby, whilecopilot,Studioandbogusare 400VALIDATION_ERROR. In-processsurface: 'copilot'stampsby:"copilot"with its actor, andsurface: 'robot'is refusedINVALID_REQUEST400. The copilot first save over an active row gives[label, columns]by copilot, and then a studio save oflabelgives[columns:copilot, label:studio]. The active read and?preview=draftare absent. After publish and after discard, the rows areactive:nullonly and the next draft starts clean. The memory driver gives the same results.Tests (merged with
origin/mainat78652c09, which carries #24401): full suites, eachVERDICT command-exit 0throughos-verify-lock:@objectstack/metadata-protocol: 247 files, 20686 tests.@objectstack/spec: 762 files, 22361 tests, re-run after the alias fix.@objectstack/metadata-coreand@objectstack/platform-objects.@objectstack/rest: 303 files, 6365 tests.@objectstack/runtime: 424 files, 7442 tests.Typecheck exit 0 for all six touched packages.
pnpm --filter @objectstack/spec check:generatedpasses after a rebuild on the merged tree.Ablations (
scripts/ablation-replace.mjs: each mutation proven on disk, restored by blob hash equal to HEAD withgit diff HEADempty; the subjects resolve throughsrc, so there is no dist hop):if (state === 'draft' && opts.draftChangesBy !== undefined)→if (false && …)) turned 4 pins red in the protocol block.entries.filter(…isAtOrUnder…)→[...entries]) turned 4 red: 3 unit pins and the protocol's two-save pin.copilotrefusal (both guards: the explicit branch and the closed set) turned exactly the REST?surface=copilotrefusal pin red.Gates, on the final head
a3925372f:node scripts/pm/dispatch-gates.mjs --commandswith no paths derives 116 families. All 116 ran, each exit code captured before any pipe, and--ranreconciles them: 116 run, 0 NOT-MEASURED (a derived zero), 115 exit 0.check-empty-changeset(exit 1), the 24298 note correction above.distpredated the pin-test edit). Afterpnpm --filter @objectstack/spec buildeach one exited 0:check:api-surface,check:dual-source-exports,check:entry-nameability,check:exported-any,check:generatedandcheck:skill-examples.check:spec-parsed-aliaswent red on an earlier head and was fixed: the alias becamez.input, with its isomorphic pin.Acceptance notes
sys_metadataactive row) records[], the whole body, per the claim's "else nothing". ThedraftChangesdescribe now says "the stored published row" so declared equals enforced. For Studio this means a copilot draft of a packaged view marks every key as the copilot's. It is raised in the report's open questions.?surface=, but records nothing, since there is no draft row to record on.stageDraft/applyDraftpasssurface: 'copilot', and objectui's Studio sends?surface=studioand reads the key (Studio copilot: one right slot that shows either the properties or the copilot — properties follow the selection and mark the copilot's drafts; the copilot folds to a strip while properties show; change card v2; the 变更 badge in sync; no 源码 tab; pillar labels kept objectui#12725 item 3).🤖 Generated with Claude Code
https://claude.ai/code/session_01Q3mCs3NX4GshNUCh7Fr1b9
Generated by Claude Code